/**
 * US-21 — Tactic selection (TA-id tags).
 * Covers AC-21.4 → AC-21.7 (API + UI).
 * AC-21.1/2/3 (model + migration + serialization) tested via API assertions.
 */
import { test, expect } from '@playwright/test';
import {
  adminToken,
  createEngagement,
  deleteEngagement,
  deleteUserByUsername,
  ensureUser,
  login,
  makeClient,
} from '../fixtures/api';
import { seedTokenInStorage } from '../fixtures/auth';

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

interface Simulation { id: number; status: string; tactics: { id: string; name: string }[]; [key: string]: unknown; }

async function createSimulation(token: string, engagementId: number, name = 'US-21 sim'): Promise<Simulation> {
  const r = await makeClient(token).post(`/engagements/${engagementId}/simulations`, { name });
  if (r.status !== 201) throw new Error(`create sim: ${r.status}`);
  return r.data as Simulation;
}

async function deleteSimulation(token: string, simId: number): Promise<void> {
  await makeClient(token).delete(`/simulations/${simId}`);
}

test.describe('US-21 — tactic selection', () => {
  let redteamToken: string;
  let socToken: string;
  let engagementId: number;

  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;
    const eng = await createEngagement(redteamToken, {
      name: 'US-21 Engagement',
      start_date: '2026-01-01',
    });
    engagementId = eng.id;
  });

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

  // AC-21.1/2/3 — model + serialization
  test('AC-21.3 — new simulation has tactics=[] in serialisation', async () => {
    const sim = await createSimulation(redteamToken, engagementId, 'AC-21.3 empty');
    expect(Array.isArray(sim.tactics)).toBe(true);
    expect(sim.tactics).toHaveLength(0);
    await deleteSimulation(redteamToken, sim.id);
  });

  // AC-21.4 — PATCH tactic_ids validation
  test('AC-21.4 — PATCH tactic_ids: valid TA-id stored, enriched name in response', async () => {
    const sim = await createSimulation(redteamToken, engagementId, 'AC-21.4 valid');
    const r = await makeClient(redteamToken).patch(`/simulations/${sim.id}`, {
      tactic_ids: ['TA0007', 'TA0001'],
    });
    expect(r.status).toBe(200);
    expect(Array.isArray(r.data.tactics)).toBe(true);
    expect(r.data.tactics).toHaveLength(2);
    const disc = r.data.tactics.find((t: { id: string }) => t.id === 'TA0007');
    expect(disc).toBeTruthy();
    expect(disc.name).toBe('Discovery');
    await deleteSimulation(redteamToken, sim.id);
  });

  test('AC-21.4 — PATCH tactic_ids: unknown TA-id → 400', async () => {
    const sim = await createSimulation(redteamToken, engagementId, 'AC-21.4 unknown');
    const r = await makeClient(redteamToken).patch(`/simulations/${sim.id}`, {
      tactic_ids: ['TA9999'],
    });
    expect(r.status).toBe(400);
    expect(r.data.error).toMatch(/unknown tactic id.*TA9999/i);
    await deleteSimulation(redteamToken, sim.id);
  });

  test('AC-21.4 — PATCH tactic_ids: dedup preserves order', async () => {
    const sim = await createSimulation(redteamToken, engagementId, 'AC-21.4 dedup');
    const r = await makeClient(redteamToken).patch(`/simulations/${sim.id}`, {
      tactic_ids: ['TA0007', 'TA0001', 'TA0007'],
    });
    expect(r.status).toBe(200);
    const ids = r.data.tactics.map((t: { id: string }) => t.id);
    expect(ids).toEqual(['TA0007', 'TA0001']);
    await deleteSimulation(redteamToken, sim.id);
  });

  // AC-21.5 — SOC gate + auto-transition
  test('AC-21.5 — SOC cannot PATCH tactic_ids → 403', async () => {
    const sim = await createSimulation(redteamToken, engagementId, 'AC-21.5 soc block');
    // Advance to review_required so SOC has access
    await makeClient(redteamToken).patch(`/simulations/${sim.id}`, { name: 'trigger' });
    await makeClient(redteamToken).post(`/simulations/${sim.id}/transition`, { to: 'review_required' });

    const r = await makeClient(socToken).patch(`/simulations/${sim.id}`, { tactic_ids: ['TA0007'] });
    expect(r.status).toBe(403);
    await deleteSimulation(redteamToken, sim.id);
  });

  test('AC-21.5 — non-empty tactic_ids triggers auto-transition pending→in_progress', async () => {
    const sim = await createSimulation(redteamToken, engagementId, 'AC-21.5 auto-transition');
    expect(sim.status).toBe('pending');
    const r = await makeClient(redteamToken).patch(`/simulations/${sim.id}`, { tactic_ids: ['TA0007'] });
    expect(r.status).toBe(200);
    expect(r.data.status).toBe('in_progress');
    await deleteSimulation(redteamToken, sim.id);
  });

  test('AC-21.5 — empty tactic_ids does NOT trigger auto-transition', async () => {
    const sim = await createSimulation(redteamToken, engagementId, 'AC-21.5 no-trigger');
    expect(sim.status).toBe('pending');
    const r = await makeClient(redteamToken).patch(`/simulations/${sim.id}`, { tactic_ids: [] });
    expect(r.status).toBe(200);
    expect(r.data.status).toBe('pending');
    await deleteSimulation(redteamToken, sim.id);
  });

  // AC-21.6 — matrix modal tactic header clickable
  test('AC-21.6 — clicking tactic header in modal toggles tactic selection', async ({
    page,
    context,
  }) => {
    const sim = await createSimulation(redteamToken, engagementId, 'AC-21.6 tactic click');

    await seedTokenInStorage(context, redteamToken);
    await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`);

    await page.getByLabel(/open mitre matrix/i).click();
    const dialog = page.getByRole('dialog');
    await expect(dialog).toBeVisible({ timeout: 10_000 });

    // Wait for matrix to load — tactic header title: "Discovery (TA0007) — click to tag this tactic"
    const discoveryHeader = dialog.locator('button[title*="TA0007"]');
    await expect(discoveryHeader).toBeVisible({ timeout: 10_000 });
    await discoveryHeader.click();

    // Apply button shows at least 1 selection (the tactic)
    await expect(dialog.getByRole('button', { name: /apply \d+/i })).toBeVisible();

    // Click again to deselect
    await discoveryHeader.click();

    await deleteSimulation(redteamToken, sim.id);
  });

  test('AC-21.6 — Apply from modal includes tactic in result (auto-save)', async ({
    page,
    context,
  }) => {
    const sim = await createSimulation(redteamToken, engagementId, 'AC-21.6 apply tactic');

    await seedTokenInStorage(context, redteamToken);
    await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`);

    await page.getByLabel(/open mitre matrix/i).click();
    const dialog = page.getByRole('dialog');
    await expect(dialog).toBeVisible({ timeout: 10_000 });

    // Tactic header title: "Discovery (TA0007) — click to tag this tactic"
    const discoveryBtn = dialog.locator('button[title*="TA0007"]');
    await expect(discoveryBtn).toBeVisible({ timeout: 10_000 });
    await discoveryBtn.click();

    const applyBtn = dialog.getByRole('button', { name: /apply \d+/i });
    await expect(applyBtn).toBeVisible();
    await applyBtn.click();

    // Modal closes
    await expect(dialog).not.toBeVisible({ timeout: 5_000 });

    // Tactic chip appears after auto-save
    await expect(page.getByTestId('mitre-tactic-tag')).toBeVisible({ timeout: 8_000 });
    await expect(page.getByTestId('techniques-tag-list')).toContainText('TA0007');

    await deleteSimulation(redteamToken, sim.id);
  });

  // AC-21.7 — tactic chips in MitreTechniquesField
  test('AC-21.7 — tactic chips display TA-id and have × for removal', async ({
    page,
    context,
  }) => {
    const sim = await createSimulation(redteamToken, engagementId, 'AC-21.7 tactic chip');
    // Seed via API
    await makeClient(redteamToken).patch(`/simulations/${sim.id}`, { tactic_ids: ['TA0007'] });

    await seedTokenInStorage(context, redteamToken);
    await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`);

    const tacticTag = page.getByTestId('mitre-tactic-tag');
    await expect(tacticTag).toBeVisible();
    await expect(tacticTag).toContainText('TA0007');

    // Title attribute has id — name
    const title = await tacticTag.getAttribute('title');
    expect(title).toMatch(/TA0007/);
    expect(title).toMatch(/Discovery/);

    // × button for removal
    await expect(page.getByRole('button', { name: /remove TA0007/i })).toBeVisible();

    await deleteSimulation(redteamToken, sim.id);
  });

  test('AC-21.7 — removing tactic chip auto-saves', async ({
    page,
    context,
  }) => {
    const sim = await createSimulation(redteamToken, engagementId, 'AC-21.7 remove tactic');
    await makeClient(redteamToken).patch(`/simulations/${sim.id}`, { tactic_ids: ['TA0007'] });

    await seedTokenInStorage(context, redteamToken);
    await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`);

    await expect(page.getByTestId('mitre-tactic-tag')).toBeVisible();
    await page.getByRole('button', { name: /remove TA0007/i }).click();

    await expect(page.getByText(/techniques updated/i)).toBeVisible({ timeout: 5_000 });
    await expect(page.getByTestId('mitre-tactic-tag')).not.toBeVisible({ timeout: 3_000 });

    await deleteSimulation(redteamToken, sim.id);
  });

  test('AC-21.7 — tactic chips visually distinct from technique chips (different class)', async ({
    page,
    context,
  }) => {
    const sim = await createSimulation(redteamToken, engagementId, 'AC-21.7 style');
    await makeClient(redteamToken).patch(`/simulations/${sim.id}`, {
      tactic_ids: ['TA0007'],
      technique_ids: ['T1059'],
    });

    await seedTokenInStorage(context, redteamToken);
    await page.goto(`/engagements/${engagementId}/simulations/${sim.id}/edit`);

    const tacticTag = page.getByTestId('mitre-tactic-tag');
    const techTag = page.getByTestId('mitre-technique-tag');
    await expect(tacticTag).toBeVisible();
    await expect(techTag).toBeVisible();

    // Tactic: bg-primary (filled) vs technique: bg-primary-soft
    const tacticCls = await tacticTag.getAttribute('class');
    const techCls = await techTag.getAttribute('class');
    expect(tacticCls).toMatch(/bg-primary/);
    // Technique should NOT have the solid bg-primary (just bg-primary-soft)
    expect(techCls).toMatch(/bg-primary-soft/);
    // They should differ visually
    expect(tacticCls).not.toBe(techCls);

    await deleteSimulation(redteamToken, sim.id);
  });

  // NIT code-reviewer: +N suffix in SimulationList MITRE column
  test('AC-21.7 — SimulationList MITRE column shows first id + "+N" for mixed tactics+techniques', async ({
    page,
    context,
  }) => {
    const sim = await createSimulation(redteamToken, engagementId, 'AC-21.7 +N suffix');
    await makeClient(redteamToken).patch(`/simulations/${sim.id}`, {
      tactic_ids: ['TA0007'],
      technique_ids: ['T1059', 'T1078'],
    });

    await seedTokenInStorage(context, redteamToken);
    await page.goto(`/engagements/${engagementId}`);

    // The row for this simulation should show "TA0007 +2" in the MITRE column
    // (1 tactic TA0007 is first, then +2 for T1059 and T1078)
    const simRow = page.getByRole('row').filter({ hasText: 'AC-21.7 +N suffix' });
    await expect(simRow).toBeVisible({ timeout: 5_000 });
    await expect(simRow).toContainText('TA0007 +2');

    await deleteSimulation(redteamToken, sim.id);
  });
});
