/**
 * Table-driven privilege sweep over EVERY host- and admin-gated route.
 *
 * Before this file, exactly THREE authorization assertions existed in the whole suite (guest →
 * /host/event/close, guest → GET /admin/config, host → GET /admin/config). Sixteen of nineteen
 * privileged routes had no test proving a guest is turned away — including:
 *
 *   POST /host/users/{id}/pin-reset  → reset any guest's PIN, then log in as them via /recover.
 *                                      That is account takeover, and nothing guarded it.
 *   DELETE /host/upload/{id}         → delete any guest's photo (unrecoverable after the party).
 *   POST /host/event/open            → retire the released keepsake and reopen uploads to everyone.
 *   PATCH /admin/config              → only GET was 403-tested; a guest WRITING config could
 *                                      disable quotas and rate limits outright.
 *
 * The point of a table is that adding a route to the router and forgetting to protect it should
 * make a test fail. So this asserts the WHOLE privileged surface, not a sample of it.
 *
 * Deliberately NOT covered here: the read-only-ban model (a banned user keeps read access and can
 * still download the keepsake) is BY DESIGN, and is asserted in 07-adversarial/authorization-deep.
 */
import { test, expect } from '../../fixtures/test';
import { BASE } from '../../helpers/env';

type Method = 'GET' | 'POST' | 'PATCH' | 'DELETE';
interface Route {
  method: Method;
  path: (victimId: string) => string;
  name: string;
  body?: unknown;
  /** admin-only routes must also reject a HOST, not just a guest. */
  adminOnly?: boolean;
}

const ROUTES: Route[] = [
  // ── Host surface ────────────────────────────────────────────────────────
  { method: 'GET', path: () => '/api/v1/host/event', name: 'GET /host/event' },
  { method: 'POST', path: () => '/api/v1/host/event/close', name: 'POST /host/event/close' },
  { method: 'POST', path: () => '/api/v1/host/event/open', name: 'POST /host/event/open' },
  {
    method: 'POST',
    path: () => '/api/v1/host/gallery/release',
    name: 'POST /host/gallery/release',
  },
  { method: 'POST', path: () => '/api/v1/host/export/rebuild', name: 'POST /host/export/rebuild' },
  { method: 'GET', path: () => '/api/v1/host/users', name: 'GET /host/users' },
  {
    method: 'POST',
    path: (v) => `/api/v1/host/users/${v}/ban`,
    name: 'POST /host/users/{id}/ban',
    body: {},
  },
  {
    method: 'POST',
    path: (v) => `/api/v1/host/users/${v}/unban`,
    name: 'POST /host/users/{id}/unban',
    body: {},
  },
  {
    method: 'PATCH',
    path: (v) => `/api/v1/host/users/${v}/role`,
    name: 'PATCH /host/users/{id}/role',
    body: { role: 'host' },
  },
  {
    method: 'POST',
    path: (v) => `/api/v1/host/users/${v}/pin-reset`,
    name: 'POST /host/users/{id}/pin-reset',
    body: {},
  },
  {
    method: 'GET',
    path: () => '/api/v1/host/pin-reset-requests',
    name: 'GET /host/pin-reset-requests',
  },
  {
    method: 'DELETE',
    path: (v) => `/api/v1/host/pin-reset-requests/${v}`,
    name: 'DELETE /host/pin-reset-requests/{id}',
  },
  { method: 'DELETE', path: (v) => `/api/v1/host/upload/${v}`, name: 'DELETE /host/upload/{id}' },
  { method: 'DELETE', path: (v) => `/api/v1/host/comment/${v}`, name: 'DELETE /host/comment/{id}' },

  // ── Admin surface ───────────────────────────────────────────────────────
  { method: 'GET', path: () => '/api/v1/admin/stats', name: 'GET /admin/stats', adminOnly: true },
  { method: 'GET', path: () => '/api/v1/admin/config', name: 'GET /admin/config', adminOnly: true },
  {
    method: 'PATCH',
    path: () => '/api/v1/admin/config',
    name: 'PATCH /admin/config',
    adminOnly: true,
    body: { quota_enabled: 'false' },
  },
  {
    method: 'GET',
    path: () => '/api/v1/admin/export/jobs',
    name: 'GET /admin/export/jobs',
    adminOnly: true,
  },
];

async function call(route: Route, jwt: string, victimId: string): Promise<number> {
  const res = await fetch(BASE + route.path(victimId), {
    method: route.method,
    headers: {
      Authorization: `Bearer ${jwt}`,
      ...(route.body !== undefined ? { 'Content-Type': 'application/json' } : {}),
    },
    ...(route.body !== undefined ? { body: JSON.stringify(route.body) } : {}),
  });
  return res.status;
}

test.describe('Authorization sweep — every privileged route', () => {
  for (const route of ROUTES) {
    test(`a GUEST is refused: ${route.name}`, async ({ guest }) => {
      const attacker = await guest('AuthzAttacker');
      const victim = await guest('AuthzVictim');

      const status = await call(route, attacker.jwt, victim.userId);

      // 403 exactly. NOT "some 4xx" — a 404 would mean the request got past the role gate and
      // merely failed to find the resource, which is precisely the vacuity this suite has been
      // bitten by: it would still pass with the RequireHost/RequireAdmin extractor deleted.
      expect(status, `${route.name} must reject a guest with 403, got ${status}`).toBe(403);
    });
  }

  for (const route of ROUTES.filter((r) => r.adminOnly)) {
    test(`a HOST is refused: ${route.name}`, async ({ host, guest }) => {
      const victim = await guest('AuthzVictim');

      const status = await call(route, host.jwt, victim.userId);

      expect(
        status,
        `${route.name} is admin-only and must reject a host with 403, got ${status}`
      ).toBe(403);
    });
  }

  // The truncate endpoint wipes every table AND the media directory. It is test-mode only, but it
  // is reachable over HTTP and gated solely by RequireAdmin — so it deserves the same proof.
  test('a GUEST is refused: POST /admin/__truncate (wipes the whole event)', async ({ guest }) => {
    const attacker = await guest('TruncateAttacker');
    const res = await fetch(`${BASE}/api/v1/admin/__truncate`, {
      method: 'POST',
      headers: { Authorization: `Bearer ${attacker.jwt}` },
    });
    expect(res.status).toBe(403);
  });

  test('a HOST is refused: POST /admin/__truncate', async ({ host }) => {
    const res = await fetch(`${BASE}/api/v1/admin/__truncate`, {
      method: 'POST',
      headers: { Authorization: `Bearer ${host.jwt}` },
    });
    expect(res.status).toBe(403);
  });

  // An unauthenticated caller must not get further than an authenticated-but-unprivileged one.
  test('an ANONYMOUS caller is refused every privileged route', async () => {
    const victim = '00000000-0000-0000-0000-000000000000';
    for (const route of ROUTES) {
      const res = await fetch(BASE + route.path(victim), {
        method: route.method,
        headers: route.body !== undefined ? { 'Content-Type': 'application/json' } : {},
        ...(route.body !== undefined ? { body: JSON.stringify(route.body) } : {}),
      });
      expect(
        [401, 403],
        `${route.name} must reject an anonymous caller, got ${res.status}`
      ).toContain(res.status);
    }
  });
});
