import { expect, test } from '@playwright/test';
import { expectNoA11yViolations } from './support/axe';
import { EMAIL, openNavGroup, openUserMenu, signIn, signOut } from './support/cms';

/**
 * História do CMS (ADR-037 + ADR-036): acesso por sessão nativa e shell como dado.
 *
 * Pré-condição: `php artisan db:seed` (usuário do painel). O domínio é próprio (`cms.localhost`), o que
 * isola cookie/sessão do portal.
 */

test.describe('CMS — acesso e shell', () => {
  test('visitante é conduzido ao login (sem navegação exposta)', async ({ page }) => {
    await page.goto('/');

    await expect(page).toHaveURL(/\/login$/);
    await expect(page.getByRole('heading', { name: 'Entrar' })).toBeVisible();

    // Anônimo não vê a navegação do painel (ADR-037).
    await expect(page.locator('#cms-sidebar')).toHaveCount(0);
    await expectNoA11yViolations(page);
  });

  test('o cartão de login centraliza no desktop (regressão: sidebar desligada não reserva coluna)', async ({ page }) => {
    await page.goto('/login');

    const card = page.locator('.cms-auth__card');
    await expect(card).toBeVisible();
    await expect(page.locator('html')).toHaveAttribute('data-cms-sidebar', 'none');

    const box = await card.boundingBox();
    const viewport = page.viewportSize();
    expect(box, 'cartão de login sem caixa').not.toBeNull();
    expect(viewport, 'viewport ausente').not.toBeNull();

    const b = box as NonNullable<typeof box>;
    const vp = viewport as NonNullable<typeof viewport>;

    // O cartão ocupa a largura de projeto (24rem ≈ 384px), não a coluna de 18rem (288px).
    expect(b.width).toBeGreaterThan(300);

    // E fica centrado na viewport (antes: comprimido na coluna `left`).
    const dx = Math.abs(b.x + b.width / 2 - vp.width / 2);
    const dy = Math.abs(b.y + b.height / 2 - vp.height / 2);
    expect(dx, `cartão fora do centro horizontal (dx=${dx}px)`).toBeLessThan(vp.width * 0.02);
    expect(dy, `cartão fora do centro vertical (dy=${dy}px)`).toBeLessThan(vp.height * 0.06);
  });

  test('credencial inválida fica no login com erro anunciado', async ({ page }) => {
    await page.goto('/login');
    await page.getByLabel('E-mail').fill(EMAIL);
    await page.getByLabel('Senha').fill('senha-errada-e2e');
    await page.getByRole('button', { name: 'Entrar' }).click();

    await expect(page.locator('#cms-login-error')).toHaveRole('alert');
    await expect(page.locator('#cms-login-error')).toHaveText('Credenciais inválidas.');
    await expect(page).toHaveURL(/\/login$/);
  });

  test('login abre o painel com as regiões do shell e passa no axe', async ({ page }) => {
    await signIn(page);

    const html = page.locator('html');

    // Shell como dado (ADR-036): posição/regiões expostas em atributo.
    await expect(html).toHaveAttribute('data-cms', '');
    await expect(html).toHaveAttribute('data-cms-sidebar', 'left');

    // Ordem do DOM = ordem visual (WCAG 2.4.3): a navegação vem antes do conteúdo.
    const sidebar = page.locator('#cms-sidebar');
    const main = page.locator('main#cms-content');
    await expect(sidebar).toBeVisible();
    await expect(main).toBeVisible();
    // Espelha o portal (`support/portal.ts`): o skip-link precisa de um `<main>` focável (WCAG 2.4.1).
    await expect(main).toHaveAttribute('tabindex', '-1');

    const sidebarBox = await sidebar.boundingBox();
    const mainBox = await main.boundingBox();
    expect(sidebarBox, 'sidebar sem caixa').not.toBeNull();
    expect(mainBox, 'conteúdo sem caixa').not.toBeNull();
    expect((sidebarBox as NonNullable<typeof sidebarBox>).x).toBeLessThan(
      (mainBox as NonNullable<typeof mainBox>).x,
    );

    // Alvos mínimos do shell (WCAG 2.5.8: 24px; 44px em ponteiro grosso — gate sniff do portal).
    // Grupos fechados não expõem links: o grupo da rota atual está aberto e o dashboard sempre
    // visível — mede-se exatamente o que o operador alcança (P5.6.S2).
    for (const link of await page.locator('.cms-sidebar__nav a:visible').all()) {
      const box = await link.boundingBox();
      expect(box, 'link do menu sem caixa').not.toBeNull();
      expect((box as NonNullable<typeof box>).height).toBeGreaterThanOrEqual(24);
    }

    await expectNoA11yViolations(page);
  });

  test('o modo orbital usa a mesma navegação num popover nativo', async ({ page }) => {
    test.skip(
      process.env.CMS_SIDEBAR_POSITION !== 'orbital',
      'cenário opt-in: rode o e2e com CMS_SIDEBAR_POSITION=orbital',
    );

    await signIn(page);

    await expect(page.locator('html')).toHaveAttribute('data-cms-sidebar', 'orbital');
    await expect(page.locator('#cms-sidebar')).toHaveCount(0);

    const trigger = page.locator('[popovertarget="cms-orbit-nav"]');
    await expect(trigger).toBeVisible();
    await trigger.click();

    const menu = page.locator('#cms-orbit-nav');
    await expect(menu).toBeVisible();
    await expect(menu.getByRole('link', { name: 'Marcos do Projeto' })).toBeVisible();
    await expectNoA11yViolations(page);
  });

  test('a sidebar leva à tela do módulo e a tela passa no axe', async ({ page }) => {
    await signIn(page);

    // Navegação do manifesto (ADR-033/036): grupo → módulo → tela. Grupos fora da rota atual
    // nascem fechados (P5.6.S2): o operador abre o grupo antes de seguir o link.
    await expect(page.locator('.cms-sidebar__nav summary').first()).toBeVisible();
    await openNavGroup(page, 'Desenvolvimento');
    await page.getByRole('link', { name: 'Marcos do Projeto' }).click();

    await expect(page).toHaveURL(/\/demo\/milestones$/);
    await expect(page.getByRole('heading', { name: 'Marcos do projeto', level: 1 })).toBeVisible();
    await expect(page.locator('table caption')).toBeVisible();

    await expectNoA11yViolations(page);
  });

  test('navegação rápida abre com Ctrl+K, filtra e fecha com Esc', async ({ page }) => {
    await signIn(page);

    await page.keyboard.press('Control+k');

    const palette = page.locator('#cms-palette');
    await expect(palette).toBeVisible();

    const input = palette.getByRole('searchbox');
    await input.fill('marcos');
    await expect(palette.locator('[data-cms-palette-item]:visible')).toHaveCount(1);

    // Teclado brasileiro digita sem acento: "noticias" precisa achar "Notícias". O índice vem
    // normalizado do servidor e o termo digitado passa pela **mesma** regra no cliente.
    await input.fill('noticias');
    await expect(palette.locator('[data-cms-palette-item]:visible')).toHaveCount(1);
    await expect(palette.locator('[data-cms-palette-item]:visible')).toContainText('Notícias');

    // E o inverso também: digitando **com** acento o destino continua sendo encontrado.
    await input.fill('notícias');
    await expect(palette.locator('[data-cms-palette-item]:visible')).toHaveCount(1);

    await input.fill('zzzz');
    await expect(palette.locator('[data-cms-palette-empty]')).toBeVisible();

    await page.keyboard.press('Escape');
    await expect(palette).toBeHidden();
  });

  test('o cabeçalho traz marca, portal, busca e menu de usuário', async ({ page }) => {
    await signIn(page);

    // Marca visível e nomeada.
    await expect(page.locator('.cms-header__title')).toBeVisible();

    // Busca: o gatilho visível abre a **mesma** paleta de navegação (sem rota `/busca`).
    const search = page.locator('[data-cms-palette-trigger]');
    await expect(search).toContainText('Pesquisar');
    await search.click();
    await expect(page.locator('#cms-palette')).toBeVisible();
    await page.keyboard.press('Escape');
    await expect(page.locator('#cms-palette')).toBeHidden();

    // Portal: nova aba, `noopener` e aviso textual da mudança de contexto.
    const portal = page.getByRole('link', { name: /Ir para o Portal/ });
    await expect(portal).toHaveAttribute('target', '_blank');
    await expect(portal).toHaveAttribute('rel', 'noopener');
    await expect(portal).toContainText('Abre em nova aba');

    // Menu de usuário nativo: identidade, perfil e saída no mesmo lugar.
    await openUserMenu(page);

    const menu = page.locator('[data-cms-user-menu]');
    await expect(menu.locator('summary')).toBeVisible();
    await expect(menu.locator('.cms-user__email')).toContainText('@');
    await expect(menu.getByRole('link', { name: 'Meu perfil' })).toBeVisible();
    await expect(menu.getByRole('button', { name: 'Sair' })).toBeVisible();

    await expectNoA11yViolations(page);
  });

  test('o auto-colapso fecha grupos que não são o atual e não fecha o grupo sob o foco', async ({ page }) => {
    // Relógio controlado: nenhuma espera de tempo real (ADR-001 — teste determinístico).
    await page.clock.install();

    await signIn(page);
    await page.goto('/noticias');

    const delay = Number(await page.locator('html').getAttribute('data-cms-nav-auto-collapse'));
    test.skip(delay === 0, 'auto-colapso desligado por configuração (CMS_NAV_AUTO_COLLAPSE_MS=0).');

    // O grupo da rota atual nasce aberto e é o único aberto.
    const current = page.locator('.cms-sidebar__nav details[data-cms-nav-current]');
    await expect(current).toHaveCount(1);
    await expect(current).toHaveJSProperty('open', true);

    const other = page.locator('.cms-sidebar__nav details[data-cms-nav-group="Desenvolvimento"]');
    await expect(other).toHaveCount(1);
    await expect(other).toHaveJSProperty('open', false);

    // Abre um grupo fora da rota atual: o foco fica no `summary` (grupo contém o foco).
    await other.locator('summary').click();
    await expect(other).toHaveJSProperty('open', true);

    // Ponteiro fora da navegação e tempo sem interação.
    await page.mouse.move(0, 0);
    await page.clock.runFor(delay + 200);

    await expect(other, 'o grupo com foco não pode ser fechado sob o operador').toHaveJSProperty('open', true);
    await expect(current, 'o grupo da rota atual nunca é fechado').toHaveJSProperty('open', true);

    // Fora do foco, o mesmo grupo é recolhido no próximo ciclo.
    await page.locator('main h1').click();
    await page.mouse.move(0, 0);
    await page.clock.runFor(delay + 200);

    await expect(other).toHaveJSProperty('open', false);
    await expect(current).toHaveJSProperty('open', true);

    // O auto-colapso **não** grava por cima da escolha manual: ao recarregar, o grupo volta aberto.
    await page.reload();
    await expect(other).toHaveJSProperty('open', true);
    await expect(current).toHaveJSProperty('open', true);
  });

  test('sair encerra a sessão e volta ao login', async ({ page }) => {
    await signIn(page);

    await signOut(page);

    await expect(page).toHaveURL(/\/login$/);
    await page.goto('/');
    await expect(page).toHaveURL(/\/login$/);
  });

  test('o painel funciona sem JavaScript (formulário nativo)', async ({ browser }) => {
    // ADR-010: caminho crítico (login) precisa funcionar com JS desligado.
    const context = await browser.newContext({ javaScriptEnabled: false, locale: 'pt-BR' });
    const page = await context.newPage();

    await signIn(page);
    await expect(page.locator('main#cms-content')).toBeVisible();

    // Sem JS o menu de usuário continua abrindo (`<details>` nativo) e a saída está lá.
    const menu = page.locator('[data-cms-user-menu]');
    await menu.locator('summary').click();
    await expect(menu.getByRole('button', { name: 'Sair' })).toBeVisible();

    // Sem JS o grupo da navegação abre nativamente e o link do módulo continua seguível.
    const group = page.locator('.cms-sidebar__nav details[data-cms-nav-group="Desenvolvimento"]');
    await group.locator('summary').click();
    await expect(group).toHaveJSProperty('open', true);
    await expect(group.getByRole('link', { name: 'Marcos do Projeto' })).toBeVisible();

    // O rodapé oferece o retorno ao conteúdo por âncora nativa.
    await expect(page.locator('.cms-footer a[href="#cms-content"]')).toBeVisible();

    // Sem JS o menu rápido ainda **abre** e a lista continua clicável (só não filtra).
    await page.locator('[data-cms-palette-trigger]').click();
    await expect(page.locator('#cms-palette')).toBeVisible();
    // O painel + os módulos do manifesto (Demo, Notícias…) — a lista continua clicável sem JS.
    expect(await page.locator('[data-cms-palette-item]').count()).toBeGreaterThanOrEqual(2);
    await page.keyboard.press('Escape');

    await context.close();
  });
});
