import { expect, test, type Page } from '@playwright/test';

/**
 * História 11 — Orçamento de Core Web Vitals (plano 14 §2/§5).
 *
 * Mede o CWV que é **reprodutível no CI**: o **CLS** (layout shift não depende de rede/CPU, ao
 * contrário de LCP/INP — que são de campo/RUM) e a **ausência de recursos que bloqueiam a
 * renderização** (o portal é mínimo JS, ADR-010). Orçamento de bytes/`.br` e contagem de queries
 * ficam no PHPUnit (`PerformanceBudgetTest`); aqui é o comportamento no navegador real.
 */
declare global {
  interface Window {
    __cls?: number;
  }
}

const CLS_BUDGET = 0.1;

/**
 * Espera o assentamento **determinístico**: webfont carregada (`document.fonts.ready`) e dois
 * frames de pintura. Substitui o sleep fixo, que num CI lento lia o CLS antes do shift
 * do `font-display: swap` e dava falso verde (ADR-001).
 */
async function settleLayout(page: Page): Promise<void> {
  await page.evaluate(async () => {
    await document.fonts.ready;
    await new Promise<void>((resolve) => {
      requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
    });
  });
}

/** Instala o observador de CLS **antes** da navegação (captura shifts do first paint inclusive). */
async function collectCls(page: Page): Promise<void> {
  await page.addInitScript(() => {
    window.__cls = 0;

    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        const shift = entry as PerformanceEntry & { value: number; hadRecentInput: boolean };

        if (!shift.hadRecentInput) {
          window.__cls = (window.__cls ?? 0) + shift.value;
        }
      }
    }).observe({ type: 'layout-shift', buffered: true });
  });
}

test.describe('Core Web Vitals — orçamento', () => {
  test('home: CLS dentro do orçamento e sem recursos bloqueantes', async ({ page }) => {
    await collectCls(page);
    await page.goto('/', { waitUntil: 'load' });
    await settleLayout(page);

    const cls = await page.evaluate(() => window.__cls ?? 0);
    expect(cls, `CLS da home = ${cls}`).toBeLessThanOrEqual(CLS_BUDGET);

    const blockingScripts = await page.$$eval('head script[src]', (scripts) =>
      scripts
        .filter((script) => {
          const el = script as HTMLScriptElement;

          return el.type !== 'module' && !el.defer && !el.async;
        })
        .map((script) => (script as HTMLScriptElement).src),
    );
    expect(blockingScripts, 'scripts que bloqueiam a renderização').toEqual([]);

    // Orçamento por cenário: 1 folha da Vite (sempre) + no máximo 1 `theme.css` (só quando o
    // painel configura um token válido). O tema é uma folha legítima, não uma regressão.
    const stylesheets = await page.$$eval('link[rel="stylesheet"]', (links) =>
      links.map((link) => new URL((link as HTMLLinkElement).href).pathname),
    );
    const viteSheets = stylesheets.filter((path) => path.startsWith('/build/'));
    const themeSheets = stylesheets.filter((path) => path === '/theme.css');

    expect(viteSheets, 'folhas da Vite no `<head>`').toHaveLength(1);
    expect(themeSheets.length, '`theme.css` duplicado').toBeLessThanOrEqual(1);
    expect(stylesheets.length, 'folhas de estilo no `<head>`').toBeLessThanOrEqual(1 + themeSheets.length);
  });

  test('notícias: CLS dentro do orçamento', async ({ page }) => {
    await collectCls(page);
    await page.goto('/noticias', { waitUntil: 'load' });
    await settleLayout(page);

    const cls = await page.evaluate(() => window.__cls ?? 0);
    expect(cls, `CLS de /noticias = ${cls}`).toBeLessThanOrEqual(CLS_BUDGET);
  });

  test('fallback métrico casa a caixa de linha do webfont (anti-CLS, ADR-011)', async ({ page }) => {
    await page.goto('/', { waitUntil: 'load' });

    // O fallback (`@font-face` local com `size-adjust`/`*-override`) precisa reproduzir as
    // **métricas** do Atkinson; senão o texto "pula" quando o webfont chega (`font-display: swap`).
    const metrics = await page.evaluate(async () => {
      await document.fonts.load('100px "Atkinson Hyperlegible Next"');
      await document.fonts.ready;

      const measure = (family: string) => {
        const el = document.createElement('span');
        // CSSOM (não `style="…"`): o CSP estrito do portal bloqueia estilo inline (ADR-022).
        el.style.fontFamily = family;
        el.style.fontSize = '100px';
        el.style.lineHeight = 'normal';
        el.style.position = 'absolute';
        el.style.visibility = 'hidden';
        el.style.whiteSpace = 'nowrap';
        el.style.display = 'inline-block';
        el.textContent = 'Hxmg AVWmp';
        document.body.appendChild(el);
        const rect = el.getBoundingClientRect();
        el.remove();

        return { height: rect.height, width: rect.width };
      };

      return {
        web: measure('"Atkinson Hyperlegible Next"'),
        fallback: measure('"Atkinson Fallback"'),
      };
    });

    expect(
      Math.abs(metrics.web.height - metrics.fallback.height),
      `caixa de linha: web=${metrics.web.height} fallback=${metrics.fallback.height}`,
    ).toBeLessThanOrEqual(1);

    expect(
      Math.abs(metrics.web.width - metrics.fallback.width),
      `largura: web=${metrics.web.width} fallback=${metrics.fallback.width}`,
    ).toBeLessThanOrEqual(4);
  });
});
