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

/**
 * História 34 — listas, filtros e formulários do painel (P5.6.S3).
 *
 * Prova a ergonomia **comum** de todas as telas de conteúdo: filtro por `GET` (compartilhável),
 * resumo dos filtros ativos com remoção individual, contagem “x–y de z”, estado vazio com ação,
 * “alterações não salvas” (`beforeunload`) e envio duplo descartado sem remover o submitter.
 *
 * Nada é persistido: o formulário da home é interceptado em `page.route` (o `PUT` nunca chega ao
 * servidor) e a listagem é derivada da semente demo. Roda no `cms-desktop` e no `cms-mobile`.
 */

test.describe('CMS — listas, filtros e formulários', () => {
  test('o filtro é GET, abre os avançados ativos e resume/remove cada filtro', async ({ page }) => {
    await signIn(page);

    // URL compartilhável: o estado inteiro vive na query (recarregar mantém o recorte).
    await page.goto('/noticias?status=publicado');

    const filters = page.locator('[data-cms-filters]');
    await expect(filters).toHaveAttribute('method', 'get');
    await expect(filters.locator('details')).toHaveAttribute('open', '');

    await expect(filters.getByText('Filtros ativos')).toBeVisible();
    await expect(filters.locator('[data-cms-filter-count]')).toHaveText(/Exibindo \d+–\d+ de \d+/);

    // A busca é sempre visível: mora **fora** do `<details>`.
    await expect(filters.locator('input[name="q"]')).toBeVisible();
    await expect(filters.locator('details input[name="q"]')).toHaveCount(0);

    // Cabeçalho fixo quando a tabela rola (CSS aplicado de verdade, não só a classe).
    await expect(page.locator('.cms-table-scroll thead th').first()).toHaveCSS('position', 'sticky');

    // A ação de linha é rotulada no desktop e `sr-only` no ponteiro grosso — o nome acessível
    // existe nos dois casos (o teste roda nas duas larguras). O `sr-only` é recortado por
    // `clip-path`, não `display: none`: a caixa existe, então a asserção é o estilo computado.
    const actionLabel = page.locator('tbody .cms-action-label').first();
    const clipped = await actionLabel.evaluate((element) => getComputedStyle(element).clipPath !== 'none');
    const isDesktop = await page.evaluate(() => matchMedia('(min-width: 64rem)').matches);
    expect(clipped).toBe(!isDesktop);
    await expect(page.locator('tbody a.btn').first()).toHaveAccessibleName('Editar');

    // A linha inteira não é clique: uma célula sem link não navega.
    const before = page.url();
    await page.locator('tbody tr').first().locator('td').nth(2).click({ position: { x: 4, y: 4 } });
    await expect(page).toHaveURL(before);

    await expectNoA11yViolations(page);

    // Remover o filtro volta para a rota **sem** a chave (não perde a tela).
    await filters.getByRole('link', { name: /Remover filtro: Situação/ }).click();
    await expect(page).toHaveURL(/\/noticias$/);
    await expect(page.locator('[data-cms-filters]').getByText('Filtros ativos')).toHaveCount(0);
    await expect(page.locator('[data-cms-filters] [data-cms-filter-count]')).toHaveText(/de \d+/);
  });

  test('a busca sem resultado mostra estado vazio com ação primária e limpar filtros', async ({ page }) => {
    await signIn(page);
    await page.goto('/noticias');

    await page.locator('#field-q').fill('e2e-nada-encontrado');
    await page.getByRole('button', { name: 'Filtrar' }).click();

    await expect(page).toHaveURL(/q=e2e-nada-encontrado/);

    const empty = page.locator('[data-cms-empty]');
    await expect(empty).toBeVisible();
    await expect(empty).toContainText('Nenhuma notícia encontrada.');
    await expect(empty.getByRole('link', { name: 'Nova notícia' })).toBeVisible();
    await expectNoA11yViolations(page);

    await empty.getByRole('link', { name: 'Limpar filtros' }).click();
    await expect(page).toHaveURL(/\/noticias$/);
    await expect(page.locator('[data-cms-empty]')).toHaveCount(0);
  });

  test('sem JavaScript o filtro avançado abre no <details> e a listagem continua funcionando', async ({ browser }) => {
    const context = await browser.newContext({ javaScriptEnabled: false, locale: 'pt-BR' });
    const page = await context.newPage();

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

      const details = page.locator('[data-cms-filters] details');
      await expect(details).toBeVisible();
      await expect(details).not.toHaveAttribute('open', '');

      // `<details>` é nativo: abre no clique e o campo continua sendo enviado.
      await details.locator('summary').click();
      await expect(details).toHaveAttribute('open', '');

      await page.locator('select[name="status"]').selectOption('publicado');
      await page.getByRole('button', { name: 'Filtrar' }).click();

      await expect(page).toHaveURL(/status=publicado/);
      await expect(page.locator('[data-cms-filters]').getByText('Filtros ativos')).toBeVisible();
      // O axe injeta regras via JS: a auditoria automática roda no projeto com JS (acima).
    } finally {
      await context.close();
    }
  });

  test('o formulário mostra "alterações não salvas" e pede confirmação ao sair', async ({ page }) => {
    await signIn(page);
    await page.goto('/noticias/criar');

    const form = page.locator('form[data-dirty-guard]');
    const indicator = form.locator('[data-dirty-indicator]');

    await expect(indicator).toBeHidden();

    // Digitação real (evento de teclado): o `beforeunload` do Chromium exige ativação do usuário.
    await page.locator('#field-title').click();
    await page.keyboard.type('Rascunho não salvo');

    await expect(indicator).toBeVisible();
    await expect(indicator).toHaveText('Alterações não salvas');
    await expect(form).toHaveAttribute('data-dirty-guard', '');

    const dialogs: string[] = [];
    page.on('dialog', async (dialog) => {
      dialogs.push(dialog.type());
      await dialog.dismiss();
    });

    await page.close({ runBeforeUnload: true });

    await expect.poll(() => dialogs.length).toBeGreaterThan(0);
    expect(dialogs[0]).toBe('beforeunload');
  });

  test('o envio duplo é descartado e o submitter com name/value continua no payload', async ({ page }) => {
    await signIn(page);
    await page.goto('/home');

    const form = page.locator('form[data-dirty-guard]').first();
    await expect(form).toBeVisible();

    const gate = Promise.withResolvers<void>();

    let posts = 0;
    const bodies: string[] = [];

    await page.route('**/home', async (route) => {
      const request = route.request();

      if (request.method() !== 'POST') {
        await route.continue();

        return;
      }

      posts += 1;
      bodies.push(request.postData() ?? '');
      await gate.promise;
      // `204` encerra a navegação sem trocar o documento: o teste continua na mesma página.
      await route.fulfill({ status: 204 });
    });

    // Dois submits no **mesmo** task: o primeiro segue, o segundo é descartado pelo handler do
    // shell. O listener registrado aqui roda depois do handler do documento, então enxerga o
    // `defaultPrevented` já decidido.
    //
    // As asserções de DOM são feitas **dentro** do evaluate: com a navegação retida pelo `route`,
    // qualquer locator do Playwright ficaria esperando a navegação terminar.
    const outcome = await page.evaluate(() => {
      const target = document.querySelector('form[data-dirty-guard]');
      const button = target?.querySelector('button[name="action"][value="save"]');

      if (!(target instanceof HTMLFormElement) || !(button instanceof HTMLButtonElement)) {
        throw new Error('formulário ou submitter ausente');
      }

      const seen: boolean[] = [];
      document.addEventListener('submit', (event) => {
        seen.push(event.defaultPrevented);
      });

      target.requestSubmit(button);
      target.requestSubmit(button);

      const status = target.querySelector('[data-form-submitting]');
      const dirty = target.querySelector('[data-dirty-indicator]');

      return {
        seen,
        busy: target.getAttribute('aria-busy'),
        statusVisible: status instanceof HTMLElement && !status.hidden,
        dirtyHidden: dirty instanceof HTMLElement && dirty.hidden,
        enabled: !button.disabled,
        connected: button.isConnected,
        submitterName: button.name,
        submitterValue: button.value,
      };
    });

    expect(outcome).toEqual({
      seen: [false, true],
      busy: 'true',
      statusVisible: true,
      dirtyHidden: true,
      enabled: true,
      connected: true,
      submitterName: 'action',
      submitterValue: 'save',
    });

    // Um único `POST`: o segundo evento foi descartado antes de chegar à rede.
    await expect.poll(() => posts).toBe(1);

    gate.resolve();

    expect(posts).toBe(1);
    expect(bodies[0]).toContain('action=save');
  });
});
