/**
 * Regression guard — likes, comments and comment deletions are rate limited.
 *
 * These were the only mutating endpoints in the app with no limit at all. Every other write path
 * -- upload, join, recover, export, admin login -- carried one; `social.rs` carried none, so the
 * coverage was asymmetric rather than deliberately open.
 *
 * Severity is genuinely low for an invited-guest event, and the amplification worry is contained:
 * a like does fan an SSE broadcast to every connected client, but the export regeneration a
 * comment deletion triggers is debounced (REGEN_DEBOUNCE 20s) and superseded workers are inert. So
 * this closes the gap for symmetry, and the ceiling is set well above anything a real guest
 * produces -- it bounds a script, not an enthusiastic double-tapper.
 *
 * The bucket is shared across all three actions on purpose: separate buckets would let a caller
 * triple the aggregate write rate just by alternating between them. That is what the second test
 * pins, and it is the part most likely to be lost in a refactor.
 *
 * Keyed per USER, not per IP — at a venue every guest is behind one NAT, so an IP key would hand
 * the whole party one bucket. Third test.
 */
import { test, expect } from '../../fixtures/test';
import { seedUpload } from '../../helpers/seed';
import { BASE } from '../../helpers/env';

const like = (jwt: string, uploadId: string) =>
  fetch(`${BASE}/api/v1/upload/${uploadId}/like`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${jwt}` },
  });

const comment = (jwt: string, uploadId: string, body: string) =>
  fetch(`${BASE}/api/v1/upload/${uploadId}/comments`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ body }),
  });

test.describe('Social — rate limit', () => {
  test('a burst of likes past the ceiling returns 429 with Retry-After', async ({
    api,
    adminToken,
    guest,
  }) => {
    await api.patchConfig(adminToken, {
      rate_limits_enabled: 'true',
      social_rate_enabled: 'true',
      social_rate_per_min: '3',
    });

    const g = await guest('Tapper');
    const uploadId = await seedUpload(g.jwt);

    // Sequential, not parallel: a toggle flips state, so ordering matters for the assertion.
    const statuses: number[] = [];
    for (let i = 0; i < 5; i++) statuses.push((await like(g.jwt, uploadId)).status);

    expect(statuses.slice(0, 3), 'the first three are within the ceiling').toEqual([200, 200, 200]);
    expect(statuses.slice(3), 'everything past it is refused').toEqual([429, 429]);

    const limited = await like(g.jwt, uploadId);
    expect(limited.status).toBe(429);
    expect(
      limited.headers.get('retry-after'),
      'a 429 without Retry-After tells the client nothing about when to come back'
    ).toBeTruthy();
  });

  test('likes and comments share one bucket', async ({ api, adminToken, guest }) => {
    // THE assertion. Per-action buckets would let a caller triple the aggregate write rate by
    // alternating, which defeats the point of having a ceiling at all.
    await api.patchConfig(adminToken, {
      rate_limits_enabled: 'true',
      social_rate_enabled: 'true',
      social_rate_per_min: '2',
    });

    const g = await guest('Mixer');
    const uploadId = await seedUpload(g.jwt);

    expect((await like(g.jwt, uploadId)).status).toBe(200);
    expect((await comment(g.jwt, uploadId, 'schön!')).status).toBe(201);
    // Two writes spent, whichever endpoints they went to.
    expect(
      (await comment(g.jwt, uploadId, 'noch eins')).status,
      'a comment must consume the same budget a like does'
    ).toBe(429);
    expect((await like(g.jwt, uploadId)).status).toBe(429);
  });

  test('one guest hitting the ceiling does not block another', async ({
    api,
    adminToken,
    guest,
  }) => {
    // Keyed per user, not per IP. Every request in this suite comes from one address, which is
    // exactly the venue-NAT shape that made the /join and /feed limits turn guests away.
    await api.patchConfig(adminToken, {
      rate_limits_enabled: 'true',
      social_rate_enabled: 'true',
      social_rate_per_min: '2',
    });

    const noisy = await guest('Noisy');
    const quiet = await guest('Quiet');
    const uploadId = await seedUpload(noisy.jwt);

    for (let i = 0; i < 3; i++) await like(noisy.jwt, uploadId);
    expect((await like(noisy.jwt, uploadId)).status).toBe(429);

    expect(
      (await like(quiet.jwt, uploadId)).status,
      'a second guest behind the same IP must have their own budget'
    ).toBe(200);
  });

  test('flipping social_rate_enabled off bypasses the limit', async ({
    api,
    adminToken,
    guest,
  }) => {
    // The toggle has to actually be honoured, or the admin switch is decorative — the failure
    // mode two other per-area toggles already shipped with.
    await api.patchConfig(adminToken, {
      rate_limits_enabled: 'true',
      social_rate_enabled: 'false',
      social_rate_per_min: '2',
    });

    const g = await guest('Unlimited');
    const uploadId = await seedUpload(g.jwt);

    const statuses: number[] = [];
    for (let i = 0; i < 6; i++) statuses.push((await like(g.jwt, uploadId)).status);
    expect(statuses.every((s) => s === 200)).toBe(true);
  });
});
