/**
 * US-26 — Admin/redteam creates and manages simulation templates.
 * Covers AC-26.3 → AC-26.8 (API CRUD + UI).
 * AC-26.1/2 (model + migration) tested implicitly via API assertions.
 */
import { test, expect } from '@playwright/test';
import {
  adminToken,
  deleteUserByUsername,
  ensureUser,
  login,
  makeClient,
} from '../fixtures/api';
import { seedTokenInStorage } from '../fixtures/auth';

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

interface Template {
  id: number;
  name: string;
  description: string | null;
  commands: string | null;
  prerequisites: string | null;
  techniques: { id: string; name: string }[];
  tactics: { id: string; name: string }[];
  created_at: string;
  updated_at: string | null;
  created_by: { id: number; username: string };
}

async function createTemplate(
  token: string,
  payload: { name: string; description?: string; commands?: string; technique_ids?: string[]; tactic_ids?: string[] },
): Promise<Template> {
  // Delete first if already exists (idempotent for retry safety)
  const list = await makeClient(token).get('/templates');
  if (list.status === 200) {
    const existing = (list.data as Template[]).find((t) => t.name === payload.name);
    if (existing) await makeClient(token).delete(`/templates/${existing.id}`);
  }
  const r = await makeClient(token).post('/templates', payload);
  if (r.status !== 201) throw new Error(`create template: ${r.status} ${JSON.stringify(r.data)}`);
  return r.data as Template;
}

async function deleteTemplate(token: string, id: number): Promise<void> {
  await makeClient(token).delete(`/templates/${id}`);
}

test.describe('US-26 — templates CRUD', () => {
  let redteamToken: string;
  let socToken: string;

  test.beforeAll(async () => {
    await ensureUser(REDTEAM_USER, PASS, 'redteam');
    await ensureUser(SOC_USER, PASS, 'soc');
    redteamToken = (await login(REDTEAM_USER, PASS)).token;
    socToken = (await login(SOC_USER, PASS)).token;
  });

  test.afterAll(async () => {
    try {
      const tok = await adminToken();
      for (const u of [REDTEAM_USER, SOC_USER]) await deleteUserByUsername(tok, u);
    } catch { /* noop */ }
  });

  // AC-26.3 — GET /api/templates
  test('AC-26.3 — GET /api/templates returns list sorted by name ASC', async () => {
    const t1 = await createTemplate(redteamToken, { name: 'US26 Zebra template' });
    const t2 = await createTemplate(redteamToken, { name: 'US26 Alpha template' });

    const r = await makeClient(redteamToken).get('/templates');
    expect(r.status).toBe(200);
    expect(Array.isArray(r.data)).toBe(true);

    const names = (r.data as Template[])
      .filter((t) => ['US26 Zebra template', 'US26 Alpha template'].includes(t.name))
      .map((t) => t.name);
    expect(names).toEqual(['US26 Alpha template', 'US26 Zebra template']);

    await deleteTemplate(redteamToken, t1.id);
    await deleteTemplate(redteamToken, t2.id);
  });

  test('AC-26.3 — GET /api/templates serializes techniques + tactics', async () => {
    const t = await createTemplate(redteamToken, {
      name: 'US26 mitre template',
      tactic_ids: ['TA0007'],
    });

    const r = await makeClient(redteamToken).get('/templates');
    expect(r.status).toBe(200);
    const found = (r.data as Template[]).find((x) => x.id === t.id);
    expect(found).toBeTruthy();
    expect(Array.isArray(found!.techniques)).toBe(true);
    expect(Array.isArray(found!.tactics)).toBe(true);
    expect(found!.tactics[0].id).toBe('TA0007');
    expect(found!.tactics[0].name).toBe('Discovery');
    expect(found!.created_by.username).toBe(REDTEAM_USER);

    await deleteTemplate(redteamToken, t.id);
  });

  test('AC-26.3 — SOC GET /api/templates → 403', async () => {
    const r = await makeClient(socToken).get('/templates');
    expect(r.status).toBe(403);
  });

  // AC-26.4 — POST /api/templates
  test('AC-26.4 — POST creates template with all fields', async () => {
    const r = await makeClient(redteamToken).post('/templates', {
      name: 'US26 full template',
      description: 'test desc',
      commands: 'cmd1\ncmd2',
      prerequisites: 'prereq1',
      tactic_ids: ['TA0001'],
    });
    expect(r.status).toBe(201);
    expect(r.data.name).toBe('US26 full template');
    expect(r.data.description).toBe('test desc');
    expect(r.data.commands).toBe('cmd1\ncmd2');
    expect(r.data.prerequisites).toBe('prereq1');
    expect(r.data.tactics).toHaveLength(1);
    expect(r.data.tactics[0].id).toBe('TA0001');
    expect(r.data.updated_at).toBeNull();

    await deleteTemplate(redteamToken, r.data.id as number);
  });

  test('AC-26.4 — POST name empty → 400', async () => {
    const r = await makeClient(redteamToken).post('/templates', { name: '' });
    expect(r.status).toBe(400);
    expect(r.data.error).toMatch(/name/i);
  });

  test('AC-26.4 — POST duplicate name → 409', async () => {
    const t = await createTemplate(redteamToken, { name: 'US26 dup name' });
    const r = await makeClient(redteamToken).post('/templates', { name: 'US26 dup name' });
    expect(r.status).toBe(409);
    expect(r.data.error).toMatch(/template name already exists/i);
    await deleteTemplate(redteamToken, t.id);
  });

  test('AC-26.4 — POST unknown tactic_id → 400', async () => {
    const r = await makeClient(redteamToken).post('/templates', {
      name: 'US26 bad tactic',
      tactic_ids: ['TA9999'],
    });
    expect(r.status).toBe(400);
    expect(r.data.error).toMatch(/unknown tactic id.*TA9999/i);
  });

  test('AC-26.4 — POST technique_ids as string (not list) → 400 (isinstance guard)', async () => {
    const r = await makeClient(redteamToken).post('/templates', {
      name: 'US26 bad technique_ids type',
      technique_ids: 'T1059',
    });
    expect(r.status).toBe(400);
    expect(r.data.error).toMatch(/technique_ids must be a list/i);
  });

  test('AC-26.4 — SOC POST → 403', async () => {
    const r = await makeClient(socToken).post('/templates', { name: 'soc template attempt' });
    expect(r.status).toBe(403);
  });

  // AC-26.5 — GET /api/templates/<tid>
  test('AC-26.5 — GET /api/templates/:id returns 200 with full data', async () => {
    const t = await createTemplate(redteamToken, { name: 'US26 get single' });
    const r = await makeClient(redteamToken).get(`/templates/${t.id}`);
    expect(r.status).toBe(200);
    expect(r.data.id).toBe(t.id);
    expect(r.data.name).toBe('US26 get single');
    await deleteTemplate(redteamToken, t.id);
  });

  test('AC-26.5 — GET /api/templates/:id unknown → 404', async () => {
    const r = await makeClient(redteamToken).get('/templates/999999');
    expect(r.status).toBe(404);
  });

  test('AC-26.5 — SOC GET /api/templates/:id → 403', async () => {
    const t = await createTemplate(redteamToken, { name: 'US26 soc get single' });
    const r = await makeClient(socToken).get(`/templates/${t.id}`);
    expect(r.status).toBe(403);
    await deleteTemplate(redteamToken, t.id);
  });

  // AC-26.6 — PATCH /api/templates/<tid>
  test('AC-26.6 — PATCH updates fields and sets updated_at', async () => {
    const t = await createTemplate(redteamToken, { name: 'US26 patch me' });
    expect(t.updated_at).toBeNull();

    const r = await makeClient(redteamToken).patch(`/templates/${t.id}`, {
      description: 'patched desc',
      commands: 'new cmd',
      tactic_ids: ['TA0007'],
    });
    expect(r.status).toBe(200);
    expect(r.data.description).toBe('patched desc');
    expect(r.data.commands).toBe('new cmd');
    expect(r.data.tactics[0].id).toBe('TA0007');
    expect(r.data.updated_at).toBeTruthy();

    await deleteTemplate(redteamToken, t.id);
  });

  test('AC-26.6 — PATCH name conflict → 409', async () => {
    const t1 = await createTemplate(redteamToken, { name: 'US26 conflict A' });
    const t2 = await createTemplate(redteamToken, { name: 'US26 conflict B' });
    const r = await makeClient(redteamToken).patch(`/templates/${t2.id}`, { name: 'US26 conflict A' });
    expect(r.status).toBe(409);
    expect(r.data.error).toMatch(/template name already exists/i);
    await deleteTemplate(redteamToken, t1.id);
    await deleteTemplate(redteamToken, t2.id);
  });

  test('AC-26.6 — PATCH same name (no-op rename) → 200', async () => {
    const t = await createTemplate(redteamToken, { name: 'US26 same name' });
    const r = await makeClient(redteamToken).patch(`/templates/${t.id}`, { name: 'US26 same name' });
    expect(r.status).toBe(200);
    await deleteTemplate(redteamToken, t.id);
  });

  test('AC-26.6 — SOC PATCH → 403', async () => {
    const t = await createTemplate(redteamToken, { name: 'US26 soc patch target' });
    const r = await makeClient(socToken).patch(`/templates/${t.id}`, { description: 'hacked' });
    expect(r.status).toBe(403);
    await deleteTemplate(redteamToken, t.id);
  });

  // AC-26.7 — DELETE /api/templates/<tid>
  test('AC-26.7 — DELETE returns 204 and template no longer GETable', async () => {
    const t = await createTemplate(redteamToken, { name: 'US26 delete me' });
    const del = await makeClient(redteamToken).delete(`/templates/${t.id}`);
    expect(del.status).toBe(204);
    const r = await makeClient(redteamToken).get(`/templates/${t.id}`);
    expect(r.status).toBe(404);
  });

  test('AC-26.7 — SOC DELETE → 403', async () => {
    const t = await createTemplate(redteamToken, { name: 'US26 soc delete target' });
    const r = await makeClient(socToken).delete(`/templates/${t.id}`);
    expect(r.status).toBe(403);
    await deleteTemplate(redteamToken, t.id);
  });

  test('AC-26.7 — DELETE template does NOT cascade to instantiated simulations', async () => {
    const tok = await adminToken();
    // Create engagement
    const engR = await makeClient(tok).post('/engagements', {
      name: 'US26 cascade eng',
      start_date: '2026-01-01',
    });
    expect(engR.status).toBe(201);
    const engId = engR.data.id as number;

    // Create template with distinct RT fields
    const tmpl = await createTemplate(redteamToken, {
      name: 'US26 cascade template',
      description: 'cascade test desc',
      commands: 'cascade cmd',
      tactic_ids: ['TA0007'],
    });

    // Instantiate simulation from template
    const simR = await makeClient(redteamToken).post(`/engagements/${engId}/simulations`, {
      template_id: tmpl.id,
    });
    expect(simR.status).toBe(201);
    const simId = simR.data.id as number;

    // Delete the template
    const del = await makeClient(redteamToken).delete(`/templates/${tmpl.id}`);
    expect(del.status).toBe(204);

    // Simulation must still exist with RT fields copied at instantiation time
    const simCheck = await makeClient(redteamToken).get(`/simulations/${simId}`);
    expect(simCheck.status).toBe(200);
    expect(simCheck.data.name).toBe('US26 cascade template');
    expect(simCheck.data.description).toBe('cascade test desc');
    expect(simCheck.data.commands).toBe('cascade cmd');

    // Cleanup
    await makeClient(tok).delete(`/simulations/${simId}`);
    await makeClient(tok).delete(`/engagements/${engId}`);
  });

  // AC-26.8 — UI /admin/templates page
  test('AC-26.8 — /admin/templates page is accessible to redteam, shows table + New button', async ({
    page,
    context,
  }) => {
    const t = await createTemplate(redteamToken, { name: 'US26 UI list template' });

    await seedTokenInStorage(context, redteamToken);
    await page.goto('/admin/templates');

    // Page title or heading
    await expect(page.getByRole('heading', { name: /templates/i })).toBeVisible({ timeout: 5_000 });

    // Table with template row
    await expect(page.getByRole('row', { name: /US26 UI list template/i })).toBeVisible();

    // "New" link in header (list is non-empty — empty state link says "New template")
    const newLink = page.getByRole('link', { name: /^new$/i }).first();
    await expect(newLink).toBeVisible();

    // Edit button on the row
    await expect(page.getByRole('link', { name: /edit/i }).first()).toBeVisible();

    await deleteTemplate(redteamToken, t.id);
  });

  test('AC-26.8 — /admin/templates shows Delete button per row', async ({
    page,
    context,
  }) => {
    const t = await createTemplate(redteamToken, { name: 'US26 UI delete btn' });

    await seedTokenInStorage(context, redteamToken);
    await page.goto('/admin/templates');

    await expect(page.getByRole('row', { name: /US26 UI delete btn/i })).toBeVisible();
    await expect(page.getByRole('button', { name: /delete/i }).first()).toBeVisible();

    await deleteTemplate(redteamToken, t.id);
  });

  test('AC-26.8 — SOC cannot access /admin/templates (redirected)', async ({
    page,
    context,
  }) => {
    await seedTokenInStorage(context, socToken);
    await page.goto('/admin/templates');
    // ProtectedRoute redirects SOC to /engagements
    await expect(page).toHaveURL(/\/engagements/, { timeout: 5_000 });
  });

  test('AC-26.8 — "New" link navigates to /admin/templates/new', async ({
    page,
    context,
  }) => {
    await seedTokenInStorage(context, redteamToken);
    await page.goto('/admin/templates');
    // Header "New" link (when list is non-empty)
    await page.getByRole('link', { name: /^new$/i }).first().click();
    await expect(page).toHaveURL(/\/admin\/templates\/new/);
  });

  test('AC-26.8 — TemplateFormPage saves template and redirects to edit', async ({
    page,
    context,
  }) => {
    await seedTokenInStorage(context, redteamToken);
    await page.goto('/admin/templates/new');

    await page.locator('#tpl-name').fill('US26 form create');
    await page.getByRole('button', { name: /save/i }).click();

    // Redirects to /admin/templates/:id/edit after creation
    await expect(page).toHaveURL(/\/admin\/templates\/\d+\/edit/, { timeout: 5_000 });

    // Clean up — get id from URL
    const url = page.url();
    const match = url.match(/\/admin\/templates\/(\d+)\/edit/);
    if (match) await deleteTemplate(redteamToken, parseInt(match[1]));
  });
});
