/**
 * US-20 — MITRE matrix: attack.mitre.org look + no horizontal scroll.
 * Covers AC-20.1 (max-w-[98vw]) and AC-20.4 (no horizontal scroll via boundingBox).
 * AC-20.5 (sub-technique expand preserved) spot-checked.
 */
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 = 'us20-redteam';
const PASS = 'us20-pass-strong';

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

async function createSimulation(token: string, engagementId: number, name = 'US-20 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-20 — MITRE matrix fits modal', () => {
  let redteamToken: string;
  let engagementId: number;

  test.beforeAll(async () => {
    await ensureUser(REDTEAM_USER, PASS, 'redteam');
    redteamToken = (await login(REDTEAM_USER, PASS)).token;
    const eng = await createEngagement(redteamToken, {
      name: 'US-20 Engagement',
      start_date: '2026-01-01',
    });
    engagementId = eng.id;
  });

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

  test('AC-20.1 — modal max-width is 98vw (fits within viewport)', async ({
    page,
    context,
  }) => {
    const sim = await createSimulation(redteamToken, engagementId, 'AC-20.1 width');

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

    // Open matrix via the grid icon button
    await page.getByLabel(/open mitre matrix/i).click();

    const dialog = page.getByRole('dialog');
    await expect(dialog).toBeVisible({ timeout: 10_000 });

    const dialogBox = await dialog.boundingBox();
    const viewportWidth = page.viewportSize()!.width;

    expect(dialogBox).toBeTruthy();
    // Modal must not exceed viewport width (98vw)
    expect(dialogBox!.width).toBeLessThanOrEqual(viewportWidth * 0.99);
    // Modal must be visible (has meaningful width)
    expect(dialogBox!.width).toBeGreaterThan(600);

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

  test('AC-20.4 — matrix body has NO horizontal scroll at 1280px viewport', async ({
    page,
    context,
  }) => {
    const sim = await createSimulation(redteamToken, engagementId, 'AC-20.4 no scroll');

    // Force 1280×720 viewport (default in playwright.config.ts)
    await page.setViewportSize({ width: 1280, height: 720 });

    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 });

    // The matrix body container must not overflow horizontally.
    // We check scrollWidth <= clientWidth on the overflow body element.
    const hasHorizontalScroll = await page.evaluate(() => {
      // Find the element with overflow-y-auto / overflow-x-hidden
      const dialogs = document.querySelectorAll('[role="dialog"]');
      for (const d of dialogs) {
        // The body is the flex-1 scrollable div inside the dialog
        const body = d.querySelector('.overflow-y-auto, .overflow-x-hidden');
        if (body) {
          return body.scrollWidth > body.clientWidth + 2; // 2px tolerance
        }
      }
      return false;
    });

    expect(hasHorizontalScroll).toBe(false);

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

  test('AC-20.4 — all 12 tactic columns visible without scrolling at 1280px', async ({
    page,
    context,
  }) => {
    const sim = await createSimulation(redteamToken, engagementId, 'AC-20.4 tactics visible');

    await page.setViewportSize({ width: 1280, height: 720 });
    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 });

    // All 12 canonical tactics must be in the DOM (use first() to avoid strict mode violation
    // when tactic name appears in multiple technique titles, e.g. "Execution" appears in
    // technique sub-names).
    const expectedTactics = [
      'Initial Access', 'Execution', 'Persistence', 'Privilege Escalation',
      'Defense Evasion', 'Credential Access', 'Discovery', 'Lateral Movement',
      'Collection', 'Command and Control', 'Exfiltration', 'Impact',
    ];
    for (const tactic of expectedTactics) {
      await expect(dialog.getByText(tactic, { exact: false }).first()).toBeVisible();
    }

    // The dialog itself must not have a scrollbar (overflow-x-hidden)
    const dialogBox = await dialog.boundingBox();
    const viewportWidth = page.viewportSize()!.width;
    expect(dialogBox!.x + dialogBox!.width).toBeLessThanOrEqual(viewportWidth + 2);

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

  test('AC-20.5 — sub-technique expand/collapse still works after layout overhaul', async ({
    page,
    context,
  }) => {
    const sim = await createSimulation(redteamToken, engagementId, 'AC-20.5 expand');

    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 });

    // Expand T1059 via chevron
    const expandBtn = dialog.getByRole('button', { name: /expand T1059/i });
    await expect(expandBtn).toBeVisible();
    await expandBtn.click();

    // T1059.001 visible after expand
    await expect(dialog).toContainText('T1059.001');

    // Collapse
    await dialog.getByRole('button', { name: /collapse T1059/i }).click();
    await expect(dialog).not.toContainText('T1059.001');

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

  // NIT code-reviewer + AC-15.5 regression: Tab focus-trap cycle in MitreMatrixModal
  test('AC-15.5 regression — MitreMatrixModal Tab key cycles focus, Shift+Tab reverses', async ({
    page,
    context,
  }) => {
    const sim = await createSimulation(redteamToken, engagementId, 'AC-15.5 focus trap');

    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 });

    // Collect all focusable elements in the dialog
    const focusableCount = await dialog.evaluate((el) => {
      const focusables = el.querySelectorAll(
        'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
      );
      return focusables.length;
    });
    expect(focusableCount).toBeGreaterThan(1);

    // Focus the last focusable element
    await dialog.evaluate((el) => {
      const focusables = Array.from(el.querySelectorAll<HTMLElement>(
        'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
      ));
      focusables[focusables.length - 1].focus();
    });

    // Tab from last → should wrap to first (focus trap)
    await page.keyboard.press('Tab');
    const activeAfterTab = await dialog.evaluate((el) => {
      const focusables = Array.from(el.querySelectorAll<HTMLElement>(
        'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
      ));
      return focusables.indexOf(document.activeElement as HTMLElement);
    });
    // After Tab from last, focus should be on first (index 0)
    expect(activeAfterTab).toBe(0);

    // Shift+Tab from first → should wrap to last
    await page.keyboard.press('Shift+Tab');
    const activeAfterShiftTab = await dialog.evaluate((el) => {
      const focusables = Array.from(el.querySelectorAll<HTMLElement>(
        'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
      ));
      return focusables.indexOf(document.activeElement as HTMLElement);
    });
    // After Shift+Tab from first, focus should be on last
    expect(activeAfterShiftTab).toBe(focusableCount - 1);

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