/**
 * Phase 2 adversarial — deeper authorization escalation paths.
 *
 * Complements the foundational 403/401 checks in 05-admin/authorization.spec.ts
 * with cross-user and banned-user scenarios that span multiple resources.
 */
import { test, expect } from '../../fixtures/test';
import { seedUpload, seedComment, listComments, findFeedRow } from '../../helpers/seed';
import { BASE } from '../../helpers/env';

test.describe('Adversarial — deep authorization', () => {
  // IDOR: user B must not be able to delete user A's REAL comment. This exercises the
  // ownership guard (`comment.user_id != auth.user_id` → 403) — the previous version fired
  // at the all-zeros UUID, which 404s at the lookup BEFORE that guard runs, so it never
  // tested authorization at all.
  test("user B cannot delete user A's comment (real resource → 403, comment survives)", async ({
    guest,
  }) => {
    const a = await guest('CommentOwnerA');
    const b = await guest('AttackerB');

    const uploadId = await seedUpload(a.jwt);
    const commentId = await seedComment(a.jwt, uploadId, 'A owns this');

    const res = await fetch(`${BASE}/api/v1/comment/${commentId}`, {
      method: 'DELETE',
      headers: { Authorization: `Bearer ${b.jwt}` },
    });
    // Must be 403 specifically — the comment exists and is in B's event, so a 404 would
    // mean the ownership check was skipped/reordered.
    expect(res.status).toBe(403);

    // No state change: the comment is still there.
    const after = await listComments(a.jwt, uploadId);
    expect(after.some((c: any) => c.id === commentId)).toBe(true);

    // Control: the real owner CAN delete it (proves the 403 was about identity, not a broken route).
    const ownerDel = await fetch(`${BASE}/api/v1/comment/${commentId}`, {
      method: 'DELETE',
      headers: { Authorization: `Bearer ${a.jwt}` },
    });
    expect(ownerDel.status).toBe(204);
  });

  // IDOR: user B must not be able to delete user A's REAL upload.
  test("user B cannot delete user A's upload (403, upload survives)", async ({ guest, db }) => {
    const a = await guest('UploadOwnerA');
    const b = await guest('AttackerB2');

    const uploadId = await seedUpload(a.jwt);
    expect(await db.countUploadsForUser(a.userId)).toBe(1);

    const res = await fetch(`${BASE}/api/v1/upload/${uploadId}`, {
      method: 'DELETE',
      headers: { Authorization: `Bearer ${b.jwt}` },
    });
    expect(res.status).toBe(403);

    // No state change: A's upload is still present (not soft-deleted).
    expect(await db.countUploadsForUser(a.userId)).toBe(1);
  });

  // IDOR: user B must not be able to edit (re-caption / re-tag) user A's upload.
  test("user B cannot edit user A's upload caption (403, caption unchanged)", async ({ guest }) => {
    const a = await guest('UploadOwnerA2');
    const b = await guest('AttackerB3');

    // seedUpload marks compression done, so the upload is feed-visible for the read-back.
    const uploadId = await seedUpload(a.jwt, { caption: 'original caption' });

    const res = await fetch(`${BASE}/api/v1/upload/${uploadId}`, {
      method: 'PATCH',
      headers: { Authorization: `Bearer ${b.jwt}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({ caption: 'hacked by B' }),
    });
    expect(res.status).toBe(403);

    // No state change: the caption A set is intact.
    const feedRes = await fetch(`${BASE}/api/v1/feed`, {
      headers: { Authorization: `Bearer ${a.jwt}` },
    });
    const row = findFeedRow(await feedRes.json(), uploadId);
    expect(row?.caption).toBe('original caption');
  });

  // These two fire at a REAL upload, and demand exactly 403.
  //
  // They used to POST to the all-zeros UUID and accept `[403, 404]`. Both handlers check
  // `user.is_banned` BEFORE they look the upload up (social.rs) — so the 404 came from the
  // *lookup*, not the guard. Delete the ban check entirely and the request still 404s on the
  // nonexistent upload, and both tests still passed. They were the only coverage of
  // ban-blocks-like and ban-blocks-comment, and they guarded nothing.
  test('banned user cannot toggle a like', async ({ api, host, guest }) => {
    const target = await guest('BannedLike');
    const uploadId = await seedUpload(host.jwt, { caption: 'likeable' });
    await api.banUser(host.jwt, target.userId);

    const res = await fetch(`${BASE}/api/v1/upload/${uploadId}/like`, {
      method: 'POST',
      headers: { Authorization: `Bearer ${target.jwt}` },
    });
    expect(res.status, 'a banned user must be Forbidden, not merely miss the resource').toBe(403);
  });

  test('banned user cannot post a comment', async ({ api, host, guest }) => {
    const target = await guest('BannedComment');
    const uploadId = await seedUpload(host.jwt, { caption: 'commentable' });
    await api.banUser(host.jwt, target.userId);

    const res = await fetch(`${BASE}/api/v1/upload/${uploadId}/comments`, {
      method: 'POST',
      headers: { Authorization: `Bearer ${target.jwt}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({ body: 'should be rejected' }),
    });
    expect(res.status, 'a banned user must be Forbidden, not merely miss the resource').toBe(403);

    // ...and nothing was written.
    expect(await listComments(host.jwt, uploadId)).toHaveLength(0);
  });

  test('banned user can still read the feed (read-only access preserved)', async ({
    api,
    host,
    guest,
  }) => {
    const target = await guest('BannedRead');
    await api.banUser(host.jwt, target.userId);

    const res = await fetch(`${BASE}/api/v1/feed`, {
      headers: { Authorization: `Bearer ${target.jwt}` },
    });
    // The journey docs explicitly state banned users keep read access.
    expect(res.status).toBe(200);
  });

  test("host cannot delete another host's session via /api/v1/session", async ({
    api,
    host,
    guest,
  }) => {
    const otherHost = await guest('OtherHost');
    // Promote so they have a host JWT to play with.
    await api.setRole(host.jwt, otherHost.userId, 'host');

    // The /session DELETE endpoint deletes the caller's own session by token hash.
    // A host cannot pass another host's token here (no way to authenticate as them),
    // so this is structurally safe — we assert by trying to delete with the wrong
    // Authorization header and checking that only the caller's session is gone.
    await api.logout(host.jwt);
    const stillWorks = await fetch(`${BASE}/api/v1/me/context`, {
      headers: { Authorization: `Bearer ${otherHost.jwt}` },
    });
    expect(stillWorks.status).toBe(200);
  });

  // Privilege escalation, tested against REAL targets.
  //
  // The old version PATCHed the all-zeros UUID and accepted `[400, 403, 404]`. The role whitelist
  // rejects "admin" with a 400 before the target is ever looked up — so adding `"admin"` to the
  // whitelist would make the request 404 on the nonexistent user instead, which was in the accepted
  // list. It never promoted anyone, never targeted *oneself*, and could not detect escalation.
  test('a host cannot promote a real guest to admin', async ({ api, host, guest, adminToken }) => {
    const victim = await guest('EscalationTarget');

    const res = await fetch(`${BASE}/api/v1/host/users/${victim.userId}/role`, {
      method: 'PATCH',
      headers: { Authorization: `Bearer ${host.jwt}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({ role: 'admin' }),
    });
    expect(res.status).not.toBe(204);
    expect(res.status).not.toBe(200);

    // The assertion that actually matters: nobody got promoted.
    const users = await api.listUsers(adminToken);
    const row = users.find((u: any) => u.id === victim.userId);
    expect(row?.role, 'the guest must NOT have become an admin').not.toBe('admin');
  });

  test('a host cannot promote THEMSELVES to admin', async ({ api, host, adminToken }) => {
    const res = await fetch(`${BASE}/api/v1/host/users/${host.userId}/role`, {
      method: 'PATCH',
      headers: { Authorization: `Bearer ${host.jwt}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({ role: 'admin' }),
    });
    expect(res.status).not.toBe(204);
    expect(res.status).not.toBe(200);

    const users = await api.listUsers(adminToken);
    const me = users.find((u: any) => u.id === host.userId);
    expect(me?.role, 'the host must NOT have self-promoted to admin').not.toBe('admin');
  });
});
