/**
 * US-3 — admin manages user accounts.
 * Covers AC-3.1 → AC-3.7. RBAC matrix exercised at the API for each verb,
 * and the /admin/users page is exercised in the UI for create + role-gate.
 */
import { test, expect } from '@playwright/test';
import {
  adminToken,
  deleteUserByUsername,
  ensureUser,
  login,
  makeClient,
} from '../fixtures/api';
import { seedTokenInStorage } from '../fixtures/auth';

const REDTEAM_USER = 'us3-redteam';
const SOC_USER = 'us3-soc';
const PASS = 'us3-pass-strong';

test.describe('US-3 — user admin', () => {
  test.beforeAll(async () => {
    await ensureUser(REDTEAM_USER, PASS, 'redteam');
    await ensureUser(SOC_USER, PASS, 'soc');
  });

  test.afterAll(async () => {
    try {
      const token = await adminToken();
      for (const u of [
        REDTEAM_USER,
        SOC_USER,
        'us3-created-via-api',
        'us3-patched',
        'us3-deleted',
        'us3-ui-newuser',
      ]) {
        await deleteUserByUsername(token, u);
      }
    } catch {
      /* best-effort */
    }
  });

  test('AC-3.1 — GET /api/users returns list for admin', async () => {
    const token = await adminToken();
    const client = makeClient(token);
    const r = await client.get('/users');
    expect(r.status).toBe(200);
    expect(Array.isArray(r.data)).toBe(true);
    const sample = (r.data as Array<Record<string, unknown>>).find(
      (u) => u.username === REDTEAM_USER,
    );
    expect(sample).toBeTruthy();
    expect(sample).toMatchObject({ role: 'redteam' });
    expect(sample!.password_hash).toBeUndefined();
    expect(sample!.id).toBeDefined();
    expect(sample!.created_at).toBeDefined();
  });

  test('AC-3.2 — POST /api/users creates user (201); 400 on duplicate or short password', async () => {
    const token = await adminToken();
    const client = makeClient(token);

    const created = await client.post('/users', {
      username: 'us3-created-via-api',
      password: 'longenough8',
      role: 'soc',
    });
    expect(created.status).toBe(201);
    expect(created.data).toMatchObject({
      username: 'us3-created-via-api',
      role: 'soc',
    });
    expect(created.data.password_hash).toBeUndefined();

    const dup = await client.post('/users', {
      username: 'us3-created-via-api',
      password: 'longenough8',
      role: 'soc',
    });
    expect(dup.status).toBe(400);

    const short = await client.post('/users', {
      username: 'us3-shortpw-x',
      password: 'short7',
      role: 'soc',
    });
    expect(short.status).toBe(400);
  });

  test('AC-3.3 — PATCH /api/users/<id> updates role and/or password', async () => {
    const token = await adminToken();
    const client = makeClient(token);

    // Seed a fresh user.
    const created = await client.post('/users', {
      username: 'us3-patched',
      password: 'initialpass8',
      role: 'soc',
    });
    expect(created.status).toBe(201);
    const id = created.data.id as number;

    // PATCH role.
    const r1 = await client.patch(`/users/${id}`, { role: 'redteam' });
    expect(r1.status).toBe(200);
    expect(r1.data.role).toBe('redteam');

    // PATCH password — must be usable for login.
    const r2 = await client.patch(`/users/${id}`, { password: 'newstrongpass8' });
    expect(r2.status).toBe(200);
    const { token: rotated } = await login('us3-patched', 'newstrongpass8');
    expect(rotated).toBeTruthy();
  });

  test('AC-3.4 — DELETE /api/users/<id> returns 204; refuses last admin (409)', async () => {
    const token = await adminToken();
    const client = makeClient(token);

    // Create a disposable redteam, delete it → 204.
    const created = await client.post('/users', {
      username: 'us3-deleted',
      password: 'disposable8',
      role: 'redteam',
    });
    expect(created.status).toBe(201);
    const id = created.data.id as number;

    const del = await client.delete(`/users/${id}`);
    expect(del.status).toBe(204);

    // Verify gone.
    const list = await client.get('/users');
    const found = (list.data as Array<{ id: number }>).find((u) => u.id === id);
    expect(found).toBeUndefined();

    // Last-admin protection — list admins and try to delete the only one.
    const all = await client.get('/users');
    const admins = (all.data as Array<{ id: number; role: string }>).filter(
      (u) => u.role === 'admin',
    );
    if (admins.length === 1) {
      const r = await client.delete(`/users/${admins[0].id}`);
      expect(r.status).toBe(409);
    } else {
      // If suite added extra admins, demote-then-delete protection still applies:
      // we attempt a hypothetical demote of one admin (PATCH to redteam) and the
      // last one must be refused.
      // Iterate: keep deleting admins one by one until 1 remains, then assert 409.
      // (Skipped in well-isolated runs because typical state = 1 admin.)
      const ids = admins.map((a) => a.id);
      while (ids.length > 1) {
        const victim = ids.pop()!;
        const r = await client.delete(`/users/${victim}`);
        expect(r.status).toBe(204);
      }
      const finalId = ids[0];
      const r = await client.delete(`/users/${finalId}`);
      expect(r.status).toBe(409);
    }
  });

  test('AC-3.5 — redteam and soc receive 403 on user-admin endpoints', async () => {
    for (const role of ['redteam', 'soc'] as const) {
      const username = role === 'redteam' ? REDTEAM_USER : SOC_USER;
      const { token } = await login(username, PASS);
      const client = makeClient(token);

      const list = await client.get('/users');
      expect(list.status, `${role} GET /users`).toBe(403);

      const post = await client.post('/users', {
        username: `${role}-attempt`,
        password: 'whatever8x',
        role: 'soc',
      });
      expect(post.status, `${role} POST /users`).toBe(403);

      const patch = await client.patch('/users/1', { role: 'soc' });
      expect(patch.status, `${role} PATCH /users/1`).toBe(403);

      const del = await client.delete('/users/999');
      expect(del.status, `${role} DELETE /users/999`).toBe(403);
    }
  });

  test('AC-3.6 — /admin/users page lists users + allows create + reset-password + delete', async ({
    page,
    context,
  }) => {
    const token = await adminToken();
    await seedTokenInStorage(context, token);

    await page.goto('/admin/users');
    await expect(page.getByRole('heading', { name: /user accounts/i })).toBeVisible();

    // Create new user via the form.
    const newName = 'us3-ui-newuser';
    await page.fill('#new-username', newName);
    await page.fill('#new-password', 'uistrongpw8');
    await page.selectOption('#new-role', 'soc');
    await page.getByRole('button', { name: /^create$/i }).click();

    // Row appears.
    const row = page.getByRole('row', { name: new RegExp(newName) });
    await expect(row).toBeVisible();

    // Reset password flow opens a sub-form.
    await row.getByRole('button', { name: /reset password/i }).click();
    await page.fill(`input[id^="reset-"]`, 'rotatedpass8');
    await page.getByRole('button', { name: /save password/i }).click();
    await expect(page.getByTestId('toast').filter({ hasText: /password reset/i })).toBeVisible();

    // Delete row (confirm dialog).
    page.once('dialog', (d) => d.accept());
    await row.getByRole('button', { name: /^delete$/i }).click();
    await expect(page.getByRole('row', { name: new RegExp(newName) })).toHaveCount(0, {
      timeout: 5_000,
    });
  });

  test('AC-3.7 — redteam/soc visiting /admin/users → redirected to /engagements + "Accès refusé" toast', async ({
    page,
    context,
  }) => {
    const { token } = await login(SOC_USER, PASS);
    await seedTokenInStorage(context, token);

    await page.goto('/admin/users');
    await page.waitForURL(/\/engagements\b/, { timeout: 5_000 });
    await expect(
      page.getByTestId('toast').filter({ hasText: /accès refusé/i }),
    ).toBeVisible();
  });
});
