/**
 * US-29 — Admin/redteam exports an engagement in Markdown, CSV, PDF.
 *
 * Strategy: seed one engagement with 2 simulations via the API, then drive the
 * ExportEngagementButton dropdown in Chromium.  Downloads are captured via
 * page.waitForEvent('download') and read back with fs.readFile.
 *
 * AC covered: 29.1 — 29.6
 */
import * as fs from 'fs/promises';
import { test, expect } from '@playwright/test';
import {
  adminToken,
  createEngagement,
  deleteEngagement,
  deleteUserByUsername,
  ensureUser,
  login,
  makeClient,
  type Engagement,
} from '../fixtures/api';

const CSV_HEADER_COLS = [
  'Scénario',
  'Test',
  'Source de log',
  'Commentaires SOC',
  'Exécution',
  'Logs remontés au SIEM',
  'Cyber incident',
];
import { seedTokenInStorage } from '../fixtures/auth';

const ADMIN_USER = 'us29-admin';
const REDTEAM_USER = 'us29-redteam';
const PASS = 'us29-pass-strong!';

interface Simulation {
  id: number;
  name: string;
}

/**
 * RFC-4180 row counter.
 * Walks char-by-char tracking quoting so that newlines inside quoted cells
 * don't count as row breaks.  Returns the total row count (including header)
 * plus helper strings for assertions.
 */
function countCsvRows(csv: string): {
  count: number;
  headerLine: string;
  dataText: string;
} {
  let inQuote = false;
  let rowCount = 0;
  let lineStart = 0;
  let headerLine = '';

  for (let i = 0; i < csv.length; i++) {
    const ch = csv[i];
    if (ch === '"') {
      if (inQuote && csv[i + 1] === '"') {
        i++; // escaped double-quote inside quoted cell
      } else {
        inQuote = !inQuote;
      }
    } else if ((ch === '\n' || ch === '\r') && !inQuote) {
      if (ch === '\r' && csv[i + 1] === '\n') i++;
      const line = csv.slice(lineStart, i).trim();
      if (line.length > 0) {
        if (rowCount === 0) headerLine = line;
        rowCount++;
      }
      lineStart = i + 1;
    }
  }
  // trailing row without final newline
  const tail = csv.slice(lineStart).trim();
  if (tail.length > 0) {
    if (rowCount === 0) headerLine = tail;
    rowCount++;
  }

  const dataText = rowCount > 1 ? csv.slice(headerLine.length + 1) : '';
  return { count: rowCount, headerLine, dataText };
}

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

test.describe('US-29 — Export formats (admin + redteam)', () => {
  let adminTok: string;
  let redteamTok: string;
  let engagement: Engagement;

  test.beforeAll(async () => {
    await ensureUser(ADMIN_USER, PASS, 'admin');
    await ensureUser(REDTEAM_USER, PASS, 'redteam');
    adminTok = (await login(ADMIN_USER, PASS)).token;
    redteamTok = (await login(REDTEAM_USER, PASS)).token;

    engagement = await createEngagement(adminTok, {
      name: 'US29 Export Engagement',
      description: 'Export test engagement',
      start_date: '2026-01-15',
      status: 'active',
    });
    await createSimulation(adminTok, engagement.id, 'US29 Sim Alpha');
    await createSimulation(adminTok, engagement.id, 'US29 Sim Beta');
  });

  test.afterAll(async () => {
    try {
      await deleteEngagement(adminTok, engagement.id);
      const rootTok = await adminToken();
      for (const u of [ADMIN_USER, REDTEAM_USER]) await deleteUserByUsername(rootTok, u);
    } catch { /* noop */ }
  });

  // AC-29.1 — Export dropdown opens
  test('AC-29.1 — admin: Export dropdown opens with 3 format items', async ({
    page,
    context,
  }) => {
    await seedTokenInStorage(context, adminTok);
    await page.goto(`/engagements/${engagement.id}`);

    const dropdownWrapper = page.locator('[data-testid="export-dropdown"]');
    await expect(dropdownWrapper).toBeVisible({ timeout: 10_000 });

    // Click the Export button to open dropdown
    await dropdownWrapper.locator('button').first().click();

    await expect(page.getByRole('menuitem', { name: /markdown/i }).or(
      page.locator('[role="menuitem"]').filter({ hasText: /markdown/i })
    ).first()).toBeVisible({ timeout: 5_000 });

    await expect(page.getByText(/csv/i).first()).toBeVisible();
    await expect(page.getByText(/pdf/i).first()).toBeVisible();
  });

  // AC-29.2 — Markdown download
  test('AC-29.2 — admin: Markdown download contains engagement name and simulation names', async ({
    page,
    context,
  }) => {
    await seedTokenInStorage(context, adminTok);
    await page.goto(`/engagements/${engagement.id}`);

    const dropdownWrapper = page.locator('[data-testid="export-dropdown"]');
    await expect(dropdownWrapper).toBeVisible({ timeout: 10_000 });
    await dropdownWrapper.locator('button').first().click();

    const [download] = await Promise.all([
      page.waitForEvent('download'),
      page.locator('[role="menuitem"]').filter({ hasText: /^markdown$/i }).first().click(),
    ]);

    const filePath = await download.path();
    expect(filePath).toBeTruthy();

    const content = await fs.readFile(filePath!, 'utf-8');

    // Must contain engagement name and start date in the header section
    expect(content).toContain('US29 Export Engagement');
    expect(content).toContain('2026-01-15');
    // Must use the 7-column GFM table layout
    expect(content).toContain('| Scénario |');
    // Simulation names appear in the Scénario column
    expect(content).toContain('US29 Sim Alpha');
    expect(content).toContain('US29 Sim Beta');

    // Suggested filename from Content-Disposition must end in .md
    const suggestedName = download.suggestedFilename();
    expect(suggestedName).toMatch(/\.md$/);
  });

  // AC-29.3 — CSV download: N+1 rows (1 header + N simulations)
  test('AC-29.3 — admin: CSV download has N+1 rows (header + 2 sim rows)', async ({
    page,
    context,
  }) => {
    await seedTokenInStorage(context, adminTok);
    await page.goto(`/engagements/${engagement.id}`);

    const dropdownWrapper = page.locator('[data-testid="export-dropdown"]');
    await expect(dropdownWrapper).toBeVisible({ timeout: 10_000 });
    await dropdownWrapper.locator('button').first().click();

    const [download] = await Promise.all([
      page.waitForEvent('download'),
      page.locator('[role="menuitem"]').filter({ hasText: /^csv$/i }).first().click(),
    ]);

    const filePath = await download.path();
    expect(filePath).toBeTruthy();

    const raw = await fs.readFile(filePath!, 'utf-8');

    // Count RFC-4180 rows: walk char-by-char, track quoting state so that
    // newlines embedded inside quoted cells don't count as row breaks.
    const rows = countCsvRows(raw);
    // 1 header + 2 simulation rows
    expect(rows.count).toBe(3);

    // Header must be exactly the 7 FR columns
    const headerCells = rows.headerLine.split(',').map((c) => c.trim().replace(/^"|"$/g, ''));
    expect(headerCells).toEqual(CSV_HEADER_COLS);

    // Scénario column (index 0) contains simulation names
    expect(rows.dataText).toContain('US29 Sim Alpha');
    expect(rows.dataText).toContain('US29 Sim Beta');

    const suggestedName = download.suggestedFilename();
    expect(suggestedName).toMatch(/\.csv$/);
  });

  // AC-29.4 — PDF download
  test('AC-29.4 — admin: PDF download has %PDF magic bytes and size > 1 KB', async ({
    page,
    context,
  }) => {
    await seedTokenInStorage(context, adminTok);
    await page.goto(`/engagements/${engagement.id}`);

    const dropdownWrapper = page.locator('[data-testid="export-dropdown"]');
    await expect(dropdownWrapper).toBeVisible({ timeout: 10_000 });
    await dropdownWrapper.locator('button').first().click();

    const [download] = await Promise.all([
      page.waitForEvent('download'),
      page.locator('[role="menuitem"]').filter({ hasText: /^pdf$/i }).first().click(),
    ]);

    const filePath = await download.path();
    expect(filePath).toBeTruthy();

    const buf = await fs.readFile(filePath!);
    // Magic bytes: %PDF
    expect(buf.slice(0, 4).toString('ascii')).toBe('%PDF');
    // Size > 1 KB
    expect(buf.byteLength).toBeGreaterThan(1024);

    const suggestedName = download.suggestedFilename();
    expect(suggestedName).toMatch(/\.pdf$/);
  });

  // AC-29.5 — Redteam: all 3 formats work
  test('AC-29.5 — redteam: Markdown download works', async ({ page, context }) => {
    await seedTokenInStorage(context, redteamTok);
    await page.goto(`/engagements/${engagement.id}`);

    const dropdownWrapper = page.locator('[data-testid="export-dropdown"]');
    await expect(dropdownWrapper).toBeVisible({ timeout: 10_000 });
    await dropdownWrapper.locator('button').first().click();

    const [download] = await Promise.all([
      page.waitForEvent('download'),
      page.locator('[role="menuitem"]').filter({ hasText: /^markdown$/i }).first().click(),
    ]);
    const filePath = await download.path();
    const content = await fs.readFile(filePath!, 'utf-8');
    expect(content).toContain('US29 Export Engagement');
  });

  test('AC-29.5 — redteam: CSV download works', async ({ page, context }) => {
    await seedTokenInStorage(context, redteamTok);
    await page.goto(`/engagements/${engagement.id}`);

    const dropdownWrapper = page.locator('[data-testid="export-dropdown"]');
    await expect(dropdownWrapper).toBeVisible({ timeout: 10_000 });
    await dropdownWrapper.locator('button').first().click();

    const [download] = await Promise.all([
      page.waitForEvent('download'),
      page.locator('[role="menuitem"]').filter({ hasText: /^csv$/i }).first().click(),
    ]);
    const filePath = await download.path();
    const raw = await fs.readFile(filePath!, 'utf-8');
    const rows = countCsvRows(raw);
    expect(rows.count).toBeGreaterThanOrEqual(3);
  });

  test('AC-29.5 — redteam: PDF download works', async ({ page, context }) => {
    await seedTokenInStorage(context, redteamTok);
    await page.goto(`/engagements/${engagement.id}`);

    const dropdownWrapper = page.locator('[data-testid="export-dropdown"]');
    await expect(dropdownWrapper).toBeVisible({ timeout: 10_000 });
    await dropdownWrapper.locator('button').first().click();

    const [download] = await Promise.all([
      page.waitForEvent('download'),
      page.locator('[role="menuitem"]').filter({ hasText: /^pdf$/i }).first().click(),
    ]);
    const filePath = await download.path();
    const buf = await fs.readFile(filePath!);
    expect(buf.slice(0, 4).toString('ascii')).toBe('%PDF');
  });

  // AC-29.6 — Filename convention: engagement-<id>-<slug>-YYYYMMDD.{ext}
  test('AC-29.6 — filename matches engagement-<id>-<slug>-YYYYMMDD pattern', async ({
    page,
    context,
  }) => {
    await seedTokenInStorage(context, adminTok);
    await page.goto(`/engagements/${engagement.id}`);

    const dropdownWrapper = page.locator('[data-testid="export-dropdown"]');
    await expect(dropdownWrapper).toBeVisible({ timeout: 10_000 });
    await dropdownWrapper.locator('button').first().click();

    const [download] = await Promise.all([
      page.waitForEvent('download'),
      page.locator('[role="menuitem"]').filter({ hasText: /^markdown$/i }).first().click(),
    ]);

    const suggestedName = download.suggestedFilename();
    // Pattern: engagement-<id>-<slug>-YYYYMMDD.md
    const filenamePattern = new RegExp(
      `^engagement-${engagement.id}-[a-z0-9-]+-\\d{8}\\.md$`,
    );
    expect(suggestedName).toMatch(filenamePattern);
  });
});
