import { devices, expect, test, type Page } from '@playwright/test';
import { expectNoA11yViolations } from './support/axe';
import { signIn, signInSupport } from './support/cms';
import { uploadMedia } from './support/media';

/**
 * História 32 — Banner com vigência, widget real no slot e módulo inativo (P5.S4.2/S5.2/S6.1).
 *
 * O operador cria três peças no slot `main.after` — **vigente** (sem janela), **futura**
 * (`starts_at` em 2030) e **expirada** (`ends_at` em 2020) — e o perfil `paginas.portal.show`
 * coloca o widget `destaques.banner` nesse slot. O portal mostra **só** a vigente, com o `alt` da
 * peça e `<picture>` com dimensões (CLS ≤ 0,1); desativar o módulo `Destaques` (ADR-034) esvazia o
 * slot sem quebrar a página (200) e reativá-lo devolve o banner — o estado global é restaurado no
 * `finally`, como nas histórias 18/23/26.
 *
 * Pré-condições: `make up` + `php artisan db:seed` (perfil demo com `destaques.*`/`paginas.*`) +
 * `php artisan migrate` + assets + storage/fila (o upload do catálogo usa o service Media).
 *
 * Dados próprios: imagem (alt único), página publicada (slug único) e três peças por execução. O
 * perfil usa um escopo fixo e exclusivo (`paginas.portal.show`) e fica **inativo** no fim; a página
 * é **arquivada**. O arquivo roda no projeto `cms-serial` (um worker, antes dos demais): o perfil e
 * a ativação de módulo são estado compartilhado.
 */

declare global {
  interface Window {
    __cls?: number;
  }
}

const PORTAL_ORIGIN = (process.env.BASE_URL ?? 'http://localhost:50700').replace(/\/$/, '');

const CLS_BUDGET = 0.1;

/** Espera o assentamento determinístico (webfont + dois frames) — nunca sleep (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()));
    });
  });
}

/** Observador de CLS instalado **antes** da navegação (captura os shifts do first paint). */
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 });
  });
}

interface Piece {
  readonly title: string;
  readonly alt: string;
  readonly startsAt?: string;
  readonly endsAt?: string;
}

/** Cria uma peça de destaque pela UI (o slot é o das três peças do teste); devolve a rota de edição. */
async function createPiece(page: Page, mediaId: string, piece: Piece): Promise<string> {
  await page.goto('/destaques/criar');
  await page.getByRole('textbox', { name: 'Português (Brasil)', exact: true }).fill(piece.title);
  await page.getByRole('textbox', { name: 'Imagem (id do catálogo de mídia)' }).fill(mediaId);
  await page.getByRole('textbox', { name: 'Texto alternativo' }).fill(piece.alt);
  await page.locator('select[name="slot"]').selectOption('main.after');

  if (piece.startsAt !== undefined) {
    await page.locator('input[name="starts_at"]').fill(piece.startsAt);
  }

  if (piece.endsAt !== undefined) {
    await page.locator('input[name="ends_at"]').fill(piece.endsAt);
  }

  // Envio por teclado (submissão implícita nativa): determinístico nos dois viewports.
  await page.getByRole('textbox', { name: 'Texto alternativo' }).press('Enter');
  await expect(page.getByText('Destaque criado.')).toBeVisible();

  return new URL(page.url()).pathname;
}

/** Arquiva as peças criadas (arquivar é terminal: não voltam ao slot em execuções futuras). */
async function archivePiece(page: Page, editPath: string): Promise<void> {
  await page.goto(editPath);

  const archive = page.getByRole('button', { name: 'Arquivar destaque' });

  if ((await archive.count()) === 0) {
    return;
  }

  await archive.click();

  await Promise.all([
    page.waitForURL(/\/destaques$/),
    page.locator('#destaque-archive-form button[type="submit"]').click(),
  ]);
  await expect(page.getByText('Destaque arquivado.')).toBeVisible();
}

/**
 * Garante o perfil `paginas.portal.show` com exatamente **uma** colocação: `destaques.banner` no
 * slot `main.after`. O `main.after` está entre os slots de todos os widgets de conteúdo que a
 * suíte usa, então a linha reaproveitada de uma execução anterior sempre oferece a opção.
 */
async function saveBannerProfile(page: Page): Promise<void> {
  await page.goto('/layout');

  const row = page.locator('tbody tr', { hasText: 'paginas.portal.show' });

  if ((await row.count()) > 0) {
    await row.getByRole('link', { name: 'Editar' }).click();
  } else {
    await page.goto('/layout/criar');
  }

  await page.getByLabel('Grupo de URL').fill('paginas.portal.show');

  const active = page.locator('input[name="is_active"]');

  if (!(await active.isChecked())) {
    await active.check();
  }

  // Zera todas as linhas (execução anterior/spec 30) — linha em branco é ignorada pelo request.
  const widgets = page.locator('select[name$="[widget]"]');

  for (let index = 0; index < (await widgets.count()); index += 1) {
    await widgets.nth(index).selectOption('');
  }

  await page.locator('select[name="placements[0][widget]"]').selectOption('destaques.banner');
  await page.locator('select[name="placements[0][slot]"]').selectOption('main.after');
  await page.locator('input[name="placements[0][ordem]"]').fill('1');

  const save = page.getByRole('button', { name: 'Salvar' });
  await save.focus();
  await save.press('Enter');
  await expect(page.getByText('Perfil de layout salvo.')).toBeVisible();
}

/**
 * Devolve o perfil ao estado **neutro**: inativo e sem colocações. O formulário renderiza as opções
 * de slot por linha a partir do widget **salvo** (não há JS que reaja à troca); deixar
 * `destaques.banner` na linha 0 esconderia `main.right` da próxima spec que reaproveita o escopo
 * (história 30). Perfil vazio é estado válido e não afeta o portal.
 */
async function resetProfile(page: Page): Promise<void> {
  await page.goto('/layout');

  const row = page.locator('tbody tr', { hasText: 'paginas.portal.show' });

  if ((await row.count()) === 0) {
    return;
  }

  await row.getByRole('link', { name: 'Editar' }).click();

  const active = page.locator('input[name="is_active"]');

  if (await active.isChecked()) {
    await active.uncheck();
  }

  const widgets = page.locator('select[name$="[widget]"]');

  for (let index = 0; index < (await widgets.count()); index += 1) {
    await widgets.nth(index).selectOption('');
  }

  const save = page.getByRole('button', { name: 'Salvar' });
  await save.focus();
  await save.press('Enter');
  await expect(page.getByText('Perfil de layout salvo.')).toBeVisible();
}

/**
 * Normaliza resíduos de execuções anteriores: peças `E2E …` deixadas **ativas** por uma rodada
 * interrompida consumiriam o teto do slot (`config('destaques.slot_limit')`) e deslocariam a peça
 * deste teste. Desativar preserva o registro (ADR-019) e tira a peça do site.
 */
async function deactivateStalePieces(page: Page): Promise<void> {
  await page.goto('/destaques?q=E2E');

  for (let guard = 0; guard < 20; guard += 1) {
    const row = page
      .locator('tbody tr')
      .filter({ hasText: /E2E (vigente|futuro|expirado) \d+/ })
      .filter({ has: page.getByRole('button', { name: 'Desativar', exact: true }) })
      .first();

    if ((await row.count()) === 0) {
      return;
    }

    await Promise.all([
      page.waitForURL(/\/destaques(\?.*)?$/),
      row.getByRole('button', { name: 'Desativar', exact: true }).click(),
    ]);
  }

  throw new Error('Resíduos de destaque do e2e não foram neutralizados.');
}

/** Alterna a ativação do módulo `Destaques` pelo painel de suporte (ADR-034). */
async function toggleDestaquesModule(page: Page, label: 'Ativar' | 'Desativar', reason: string): Promise<void> {
  const row = page.locator('tr[data-module="Destaques"]');

  await row.getByRole('button', { name: label, exact: true }).click();
  await row.getByLabel('Motivo').fill(reason);

  await Promise.all([
    page.waitForURL(/\/suporte\/modulos$/),
    row.locator('form').getByRole('button', { name: label, exact: true }).click(),
  ]);
}

test.describe('Portal — banner com vigência e módulo inativo', () => {
  test('só a peça vigente aparece, com alt; módulo desligado esvazia o slot', async ({
    browser,
    page,
  }, testInfo) => {
    test.setTimeout(120_000);

    const stamp = Date.now();
    const suffix = `${testInfo.project.name}-${stamp}`;
    const slug = `e2e-banner-${suffix}`;
    const pageTitle = `Página com banner ${suffix}`;
    const alt = `Imagem do banner ${stamp}`;

    const pieces: readonly Piece[] = [
      { title: `E2E vigente ${stamp}`, alt: `Alt vigente ${stamp}` },
      { title: `E2E futuro ${stamp}`, alt: `Alt futuro ${stamp}`, startsAt: '2030-01-02T00:00', endsAt: '2030-03-04T00:00' },
      { title: `E2E expirado ${stamp}`, alt: `Alt expirado ${stamp}`, startsAt: '2020-01-02T00:00', endsAt: '2020-03-04T00:00' },
    ];

    await signIn(page);
    await deactivateStalePieces(page);

    const mediaId = await uploadMedia(page, alt);

    const piecePaths: string[] = [];

    for (const piece of pieces) {
      piecePaths.push(await createPiece(page, mediaId, piece));
    }

    // Página própria publicada como destino do perfil.
    await page.goto('/paginas/criar');
    await page.getByRole('textbox', { name: 'Português (Brasil)', exact: true }).fill(pageTitle);
    const slugField = page.getByRole('textbox', { name: 'Endereço (slug)' });
    await slugField.fill(slug);
    await slugField.press('Enter');
    await expect(page.getByText('Página criada.')).toBeVisible();

    const editPath = new URL(page.url()).pathname;
    const publish = page.getByRole('button', { name: 'Publicar página' });
    await publish.focus();
    await publish.press('Enter');
    await expect(page.getByText('Página publicada.')).toBeVisible();

    const url = `${PORTAL_ORIGIN}/${slug}?e2e=banner-${suffix}`;

    try {
      await saveBannerProfile(page);
      await expectNoA11yViolations(page);

      // 1. Vigência: só a peça vigente entra; futuro e expirado ficam fora do HTML.
      await collectCls(page);
      const response = await page.goto(url);
      expect(response?.status(), 'GET página com banner').toBe(200);
      await settleLayout(page);

      const widget = page.locator('[data-widget="destaques.banner"]');
      await expect(widget).toHaveCount(1);

      // Escopo por marcador único: peças de execuções anteriores (se uma falhou antes de arquivar)
      // também estão vigentes no slot — a asserção é sobre **esta** execução.
      await expect(
        page.getByRole('heading', { level: 2, name: `E2E vigente ${stamp}` }),
      ).toHaveCount(1);

      for (const hidden of [`E2E futuro ${stamp}`, `E2E expirado ${stamp}`]) {
        await expect(page.getByRole('heading', { level: 2, name: hidden })).toHaveCount(0);
      }

      // `alt` obrigatório + dimensões (anti-CLS, ADR-026) na imagem da peça vigente.
      const image = widget.locator(`img[alt="Alt vigente ${stamp}"]`);
      await expect(image).toHaveCount(1);
      await expect(image).toHaveAttribute('width');
      await expect(image).toHaveAttribute('height');
      expect(
        await widget.locator('img:not([alt]), img[alt=""]').count(),
        'toda imagem do banner tem alt',
      ).toBe(0);

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

      await expectNoA11yViolations(page);

      // 2. Mesmo conteúdo no viewport móvel (público, sem repetir login/escrita).
      const mobile = await browser.newContext({ ...devices['Pixel 7'] });

      try {
        const mobilePage = await mobile.newPage();
        expect((await mobilePage.goto(url))?.status(), 'GET página (mobile)').toBe(200);
        await expect(mobilePage.locator('[data-widget="destaques.banner"]')).toHaveCount(1);
        await expect(
          mobilePage.locator(`[data-widget="destaques.banner"] img[alt="Alt vigente ${stamp}"]`),
        ).toHaveCount(1);
        await expectNoA11yViolations(mobilePage);
      } finally {
        await mobile.close();
      }

      // 3. Módulo inativo (ADR-034): o registry não resolve a fonte, o slot fica vazio e a página
      //    continua 200 — sem `<aside>`/wrapper órfão e sem 500.
      await page.context().clearCookies();
      await signInSupport(page);

      try {
        await page.goto('/suporte/modulos');
        await toggleDestaquesModule(page, 'Desativar', 'Janela de manutenção (e2e)');
        await expect(page.getByRole('alert')).toContainText('Módulo desativado.');

        // Query própria: o HTML público é `public, max-age=300` (ADR-022) e o navegador serviria o
        // corpo anterior sem revalidar — a prova do módulo inativo é sobre o request **novo**.
        const withoutModule = await page.goto(`${url}-off`);
        expect(withoutModule?.status(), 'GET página com módulo inativo').toBe(200);
        await expect(page.locator('[data-widget="destaques.banner"]')).toHaveCount(0);
        await expect(page.locator('.destaques-banner')).toHaveCount(0);
        await expect(page.getByRole('heading', { level: 1, name: pageTitle })).toBeVisible();
        await expectNoA11yViolations(page);
      } finally {
        await page.goto('/suporte/modulos');

        const row = page.locator('tr[data-module="Destaques"]');

        if ((await row.getByRole('button', { name: 'Ativar', exact: true }).count()) > 0) {
          await toggleDestaquesModule(page, 'Ativar', 'Fim da janela (e2e)');
          await expect(page.getByRole('alert')).toContainText('Módulo ativado.');
        }

        // Reativado, o banner volta imediatamente (sem optimize/reload do FPM).
        const restored = await page.goto(`${url}-on`);
        expect(restored?.status()).toBe(200);
        await expect(page.locator('[data-widget="destaques.banner"]')).toHaveCount(1);
      }
    } finally {
      // Ordem de limpeza: volta ao admin, desativa o perfil, arquiva as peças e a página.
      await page.context().clearCookies();
      await signIn(page);
      await resetProfile(page);

      for (const piecePath of piecePaths) {
        await archivePiece(page, piecePath);
      }

      await page.goto(editPath);

      const archive = page.getByRole('button', { name: 'Arquivar página' });

      if ((await archive.count()) > 0) {
        await archive.focus();
        await archive.press('Enter');
        await expect(page.getByText('Página arquivada.')).toBeVisible();
      }
    }
  });
});
