import { expect, test } from '@playwright/test';
import { gotoExpecting } from './support/portal';

/**
 * @intent História 14 — cabeçalhos de segurança do edge (`histories/14-security-headers.md`).
 *
 * A CSP é do **edge** (`web`/Caddy — spec 16), estática e cacheável (ADR-020/ADR-022): sem
 * `unsafe-inline`/`unsafe-eval`, sem CDN. O app não a emite nem gera nonce. O embed de notícia é a
 * única rota que relaxa `frame-ancestors`.
 */
// O `request` herda o `actionTimeout: 10s`; sob carga do `make check` (FPM/DB/fila no mesmo host)
// um GET pode passar disso sem significar regressão. O teste é de cabeçalho, não de latência.
const REQUEST_TIMEOUT = 30_000;

test.describe('História 14 — Cabeçalhos de segurança (edge)', () => {
  test('a CSP do portal é estrita e sem CDN (ADR-020/ADR-022)', async ({ request }) => {
    const response = await request.get('/', { timeout: REQUEST_TIMEOUT });
    const csp = response.headers()['content-security-policy'] ?? '';

    expect(csp, 'CSP ausente no portal').toContain("default-src 'self'");
    expect(csp).not.toContain('unsafe-inline');
    expect(csp).not.toContain('unsafe-eval');
    expect(csp).not.toContain('cdn.jsdelivr.net');
    expect(csp).not.toContain('fonts.googleapis.com');
    expect(csp).not.toContain('fonts.gstatic.com');
    expect(csp).toContain("frame-ancestors 'self'");

    // Headers estáticos do ADR-020 que não são CSP.
    expect(response.headers()['x-frame-options']).toBe('SAMEORIGIN');
    expect(response.headers()['x-xss-protection']).toBe('0');
    expect(response.headers()['permissions-policy']).toContain('payment=()');
    expect(response.headers()['permissions-policy']).toContain('usb=()');
  });

  test('o HTML público não carrega token/nonce por request (ETag estável)', async ({ request }) => {
    const first = await (await request.get('/', { timeout: REQUEST_TIMEOUT })).text();
    const second = await (await request.get('/', { timeout: REQUEST_TIMEOUT })).text();

    expect(first).not.toContain('nonce="');
    expect(first).not.toContain('csrf-token');
    expect(second).toEqual(first);
  });

  test('o embed de notícia pode ser embutido externamente (frame-ancestors *)', async ({
    request,
  }) => {
    const response = await request.get('/noticias/embed/test', { timeout: REQUEST_TIMEOUT });

    expect(response.status()).toBe(200);
    expect(response.headers()['content-security-policy'] ?? '').toContain('frame-ancestors *');
    // `@embed` é a única rota sem XFO: o XFO bloquearia o iframe mesmo com `frame-ancestors *`.
    expect(response.headers()['x-frame-options']).toBeUndefined();
  });

  test('o conteúdo público é cacheável com ETag/304 e sem Set-Cookie (ADR-022)', async ({
    request,
  }) => {
    const response = await request.get('/', { timeout: REQUEST_TIMEOUT });
    const headers = response.headers();

    expect(headers['cache-control'] ?? '').toContain('public');
    expect(headers['cache-control'] ?? '').toContain('stale-while-revalidate');
    expect(headers['vary'] ?? '').toContain('Cookie');
    expect(headers['set-cookie'], 'conteúdo público não abre sessão').toBeUndefined();

    const etag = headers['etag'];
    // O Caddy anexa o sufixo da codificação (`-gzip`/`-br`/`-zstd`) ao ETag forte (RFC 9110 §8.8.3).
    expect(etag).toMatch(/^"[0-9a-f]{64}(?:-(?:gzip|br|zstd))?"$/);

    const notModified = await request.get('/', { headers: { 'If-None-Match': etag! }, timeout: REQUEST_TIMEOUT });
    expect(notModified.status()).toBe(304);
  });

  test('o CMS exige Trusted Types (ADR-020)', async ({ request }) => {
    const portalBase = test.info().project.use.baseURL as string;
    const cmsBase = process.env.CMS_BASE_URL ?? new URL(portalBase).origin.replace('://', '://cms.');

    const response = await request.get(`${cmsBase}/login`, { timeout: REQUEST_TIMEOUT });
    const csp = response.headers()['content-security-policy'] ?? '';

    expect(csp, 'CSP ausente no CMS').toContain("require-trusted-types-for 'script'");
    expect(csp).toContain("trusted-types 'allow-duplicates' ProseMirrorClipboard");
    expect(csp).not.toContain('unsafe-inline');
    expect(response.headers()['x-frame-options']).toBe('SAMEORIGIN');
  });

  test('VLibras carrega sob demanda sem violar a CSP (ADR-002/025)', async ({ page }) => {
    await page.addInitScript(() => {
      const target = window as unknown as Record<string, unknown>;
      target.__cspViolations = [];
      target.__vlibrasWidget = false;
      target.VLibras = { Widget: () => { target.__vlibrasWidget = true; } };

      document.addEventListener('securitypolicyviolation', (event) => {
        (target.__cspViolations as string[]).push(`${event.violatedDirective} ${event.blockedURI}`);
      });
    });

    await page.route('https://www.vlibras.gov.br/**', (route) =>
      route.fulfill({ status: 200, contentType: 'application/javascript', body: '/* stub */' }),
    );

    await gotoExpecting(page, '/', 200);
    const request = page.waitForRequest('https://www.vlibras.gov.br/**', { timeout: 10_000 });

    await page.getByRole('button', { name: 'VLibras', exact: true }).click();

    expect(await request).toBeTruthy();
    await expect
      .poll(() => page.evaluate(() => (window as unknown as Record<string, unknown>).__vlibrasWidget))
      .toBe(true);
    expect(await page.evaluate(() => (window as unknown as Record<string, unknown>).__cspViolations)).toEqual([]);
  });

  test('o Google Analytics opt-in não é bloqueado pela CSP (ADR-002/027)', async ({ page }) => {
    await page.addInitScript(() => {
      const target = window as unknown as Record<string, unknown>;
      target.__cspViolations = [];
      const original = document.querySelector.bind(document);
      (document as unknown as Record<string, unknown>).querySelector = (selector: string) =>
        selector === 'meta[name="gtag-id"]'
          ? { content: 'G-TEST' }
          : original(selector);

      document.addEventListener('securitypolicyviolation', (event) => {
        (target.__cspViolations as string[]).push(`${event.violatedDirective} ${event.blockedURI}`);
      });
    });

    await page.route('https://www.googletagmanager.com/**', (route) =>
      route.fulfill({ status: 200, contentType: 'application/javascript', body: '/* stub */' }),
    );

    const request = page.waitForRequest('https://www.googletagmanager.com/**', { timeout: 10_000 });
    await gotoExpecting(page, '/', 200);

    expect(await request).toBeTruthy();
    expect(await page.evaluate(() => (window as unknown as Record<string, unknown>).__cspViolations)).toEqual([]);
  });

  test('os assets do Vite são imutáveis (ADR-022)', async ({ request }) => {
    const html = await (await request.get('/', { timeout: REQUEST_TIMEOUT })).text();
    const asset = /\/build\/assets\/[^"']+\.(?:js|css)/.exec(html)?.[0];

    expect(asset, 'nenhum asset do Vite no HTML').toBeTruthy();

    const response = await request.get(asset!, { timeout: REQUEST_TIMEOUT });
    expect(response.headers()['cache-control'] ?? '').toContain('immutable');
  });
});
