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

/**
 * História do CMS de Notícias (F4.I2): criar com o editor rico (AST), publicar e ver o resultado
 * semântico no portal (ADR-028/ADR-026). Pré-condição: `php artisan db:seed`.
 */
/** O portal vive no domínio sem o prefixo `cms.` (área própria — ADR-005/037). */
function portalUrl(cmsBase: string, path: string): string {
  const url = new URL(cmsBase);
  url.hostname = url.hostname.replace(/^cms\./, '');

  return new URL(path, url.origin).toString();
}


test.describe('CMS — Notícias', () => {
  test('cria com o editor rico, publica e aparece no portal', async ({ page }, testInfo) => {
    const slug = `e2e-noticia-${Date.now()}`;
    const title = `Notícia E2E ${slug}`;
    let created = false;

    try {
      await signIn(page);

      // Lista → nova notícia.
      await page.goto('/noticias');
      await expect(page.getByRole('heading', { name: 'Notícias', level: 1 })).toBeVisible();
      await expectNoA11yViolations(page);

      await page.getByRole('link', { name: 'Nova notícia' }).click();
      await expect(page).toHaveURL(/\/noticias\/criar$/);
      await expectNoA11yViolations(page);

      // Campos padrão do formulário.
      await page.locator('#field-title').fill(title);
      await page.locator('#field-slug').fill(slug);
      await page.locator('select[name="status"]').selectOption('publicado');

      // Conteúdo no editor rico — a aba pt_BR já vem ativa.
      const editor = page.locator('[data-rich-editor-ready] .richtext-editor').first();
      await editor.click();
      await page.keyboard.type('Parágrafo criado no editor rico do CMS.');

      // Ctrl+K é atalho do painel **fora** de campos de texto/editor: dentro do ProseMirror ele
      // não pode sequestrar a combinação (convenção de teclado — B5).
      await page.keyboard.press('Control+k');
      await expect(page.locator('#cms-palette')).toBeHidden();

      // A **capa** é o outro caminho de mídia do módulo (ADR-026) e sai do S3 real: um upload que
      // não persiste daria `<picture>` apontando para o vazio — a imagem quebrada ninguém veria.
      const cover = Buffer.from(
        'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
        'base64',
      );

      await page.locator('input[name="cover"]').setInputFiles({
        name: 'capa.png',
        mimeType: 'image/png',
        buffer: cover,
      });

      await page.getByRole('button', { name: 'Salvar' }).click();
      created = true;

      await expect(page).toHaveURL(new RegExp(`/noticias/[0-9a-f-]+/editar$`));
      // O aviso de sucesso é **flash** e qualquer requisição intercalada (favicon) o consome — a
      // asserção determinística vive no PHPUnit (`NoticiaCmsTest`), aqui fica o **resultado**.
      await expectNoA11yViolations(page);

      // O portal renderiza o HTML semântico a partir do AST.
      const portal = portalUrl(testInfo.project.use.baseURL as string, `/noticias/${slug}`);

      await page.goto(portal);
      await expect(page.getByRole('heading', { name: title, level: 1 })).toBeVisible();
      await expect(
        page.getByText('Parágrafo criado no editor rico do CMS.'),
      ).toBeVisible();

      // A capa processada é servida de verdade (mesma origem → `media.stream`) com as dimensões
      // intrínsecas (CLS 0) — e não o placeholder institucional.
      const coverPicture = page.locator('article picture').first();
      await expect(coverPicture).toBeVisible();
      await expect(coverPicture.locator('img')).toHaveAttribute('width', '1');
      await expect(coverPicture.locator('img')).toHaveAttribute('height', '1');
      await expect
        .poll(async () => coverPicture.locator('img').evaluate((element) => (element as HTMLImageElement).naturalWidth))
        .toBeGreaterThan(0);

      await expectNoA11yViolations(page);
    } finally {
      if (created) {
        await archiveNoticia(page, slug);
      }
    }
  });

  test('o formulário do editor funciona sem JS (textarea do AST)', async ({ browser }, testInfo) => {
    // ADR-010: sem JS o `<textarea>` é o campo do formulário (o autor edita o JSON do AST).
    const context = await browser.newContext({ javaScriptEnabled: false, locale: 'pt-BR' });
    const page = await context.newPage();
    const slug = `e2e-sem-js-${Date.now()}`;
    let created = false;

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

      await page.locator('#field-title').fill(`Notícia sem JS ${slug}`);
      await page.locator('#field-slug').fill(slug);
      await page.locator('select[name="status"]').selectOption('publicado');

      const ast = JSON.stringify({
        type: 'doc',
        content: [
          {
            type: 'paragraph',
            content: [{ type: 'text', text: 'Conteúdo salvo sem JavaScript.' }],
          },
        ],
      });

      await page.locator('textarea[name="content[pt_BR]"]').fill(ast);
      await page.getByRole('button', { name: 'Salvar' }).click();
      created = true;

      await expect(page.getByRole('alert')).toContainText('Notícia salva.');

      const portal = portalUrl(testInfo.project.use.baseURL as string, `/noticias/${slug}`);
      await page.goto(portal);
      await expect(page.getByText('Conteúdo salvo sem JavaScript.')).toBeVisible();
    } finally {
      if (created) {
        await archiveNoticia(page, slug);
      }
      await context.close();
    }
  });

  test('o fallback sem editor (textarea do AST) passa no axe', async ({ page }) => {
    // Com JS o editor esconde o `textarea` e o axe o ignora — o caminho de degradação (ADR-010)
    // só é auditável com o bundle do editor bloqueado: aqui o campo cru fica visível e precisa de
    // nome acessível como qualquer outro campo do formulário.
    await page.route('**/editor-*.js', (route) => route.abort());

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

    await expect(page.locator('textarea[name="content[pt_BR]"]')).toBeVisible();
    await expectNoA11yViolations(page);
  });

  test('avisa quando a imagem passa do recomendado, sem bloquear o upload', async ({ page }) => {
    // Soft limit (2 MiB para imagem): o arquivo entra, mas o autor vê o alerta persistente
    // (`role="status"`) — a borda/PHP aceitam até o hard (20 MiB); o limite é por tipo (ADR-026).
    await signIn(page);
    await page.goto('/noticias/criar');

    const editor = page.locator('[data-rich-editor-ready] .richtext-editor').first();
    await editor.click();
    await page.getByRole('button', { name: 'Inserir imagem' }).click();

    const png = Buffer.from(
      'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
      'base64',
    );
    const big = Buffer.concat([png, Buffer.alloc(2_100_000)]); // > 2 MiB (soft), < 20 MiB (hard)

    await page.locator('[data-rich-editor-media-dialog] input[type="file"]').setInputFiles({
      name: 'grande.png',
      mimeType: 'image/png',
      buffer: big,
    });
    await page.locator('[data-rich-editor-media-dialog] input[name="alt"]').fill('Imagem grande');

    const uploadUrl = await page.locator('[data-rich-editor]').first().getAttribute('data-upload-url');
    expect(uploadUrl).toBeTruthy();

    const uploaded = page.waitForResponse(
      (response) => response.url() === uploadUrl && response.request().method() === 'POST',
      { timeout: 20_000 },
    );

    await page.locator('[data-rich-editor-media-dialog] [data-dialog-apply]').click();

    expect((await uploaded).ok()).toBe(true);

    const notice = page.locator('[data-rich-editor-notice]');
    await expect(notice).toBeVisible({ timeout: 20_000 });
    await expect(notice).toContainText('recomendado');
    await expect(notice).toContainText('2 MB');
  });

  test('a legenda e o texto alternativo usam <dialog> nativo (sem prompt bloqueante)', async ({ page }) => {
    await signIn(page);
    await page.goto('/noticias/criar');

    const editor = page.locator('[data-rich-editor-ready] .richtext-editor').first();
    await editor.click();

    // Insere uma imagem 1×1 pelo diálogo de mídia (buffer em memória — sem fixture no disco).
    await page.getByRole('button', { name: 'Inserir imagem' }).click();

    const png = Buffer.from(
      'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
      'base64',
    );

    await page.locator('[data-rich-editor-media-dialog] input[type="file"]').setInputFiles({
      name: 'pixel.png',
      mimeType: 'image/png',
      buffer: png,
    });
    await page.locator('[data-rich-editor-media-dialog] input[name="alt"]').fill('Imagem de teste');

    // O upload vai ao S3 e volta, e a imagem é um **segundo** round-trip (stream same-origin → S3):
    // a espera é pelo **evento** (as duas respostas), não por timeout — sob carga o round-trip passa
    // dos 7s default do `expect` e o teste virava flaky (ADR-001).
    const uploadUrl = await page.locator('[data-rich-editor]').first().getAttribute('data-upload-url');
    expect(uploadUrl).toBeTruthy();

    // A digestão é assíncrona (fila) e o RustFS pode levar alguns segundos sob carga do `make check`;
    // o default de 10s (`actionTimeout`) vira flake. A espera é pelo **evento**, com folga explícita.
    const MEDIA_TIMEOUT = 20_000;
    const uploaded = page.waitForResponse(
      (response) => response.url() === uploadUrl && response.request().method() === 'POST',
      { timeout: MEDIA_TIMEOUT },
    );
    const streamed = page.waitForResponse(
      (response) => response.url().includes('/midia/permanent/'),
      { timeout: MEDIA_TIMEOUT },
    );

    await page.locator('[data-rich-editor-media-dialog] [data-dialog-apply]').click();

    expect((await uploaded).ok()).toBe(true);
    expect((await streamed).ok()).toBe(true);

    const figure = editor.locator('figure[data-rich-media]').first();
    await expect(figure).toBeVisible();

    // A mídia precisa ser **servida** de verdade: um upload que não persiste (bucket ausente →
    // fallback silencioso) passaria só com o markup e a imagem quebrada ninguém veria.
    const image = figure.locator('img');
    await expect(image).toHaveAttribute('src', /\/midia\/permanent\//);
    await expect
      .poll(async () => image.evaluate((element) => (element as HTMLImageElement).naturalWidth))
      .toBeGreaterThan(0);

    await figure.click();

    // A legenda abre em `<dialog>` nativo (o prompt do navegador travaria o teste).
    await page.getByRole('button', { name: 'Legenda da imagem' }).click();

    const dialog = page.locator('[data-rich-editor-text-dialog]');
    const captionButton = page.getByRole('button', { name: 'Legenda da imagem' });
    await expect(dialog).toBeVisible();

    // O título e o rótulo vêm de `data-i18n-*`: sem o atributo o `t()` cai no **nome cru da chave**
    // (`media_caption`) e o autor lê a chave, não o texto.
    await expect(dialog.locator('[data-text-dialog-title]')).toHaveText('Legenda da imagem');
    await expect(dialog.locator('[data-text-dialog-label]')).toHaveText('Legenda da imagem');
    await expectNoA11yViolations(page);

    await dialog.locator('input[name="value"]').fill('Legenda E2E');
    await dialog.locator('[data-dialog-apply]').click();
    await expect(dialog).toBeHidden();

    // Foco: aplicar leva o autor de volta ao editor (o `focus()` do TipTap é agendado e vence);
    // fechar sem aplicar devolve o foco ao gatilho — Cancelar e `Esc`.
    await expect(editor).toBeFocused();
    await expect(editor.locator('figcaption')).toHaveText('Legenda E2E');

    await figure.click();
    await captionButton.click();
    await dialog.locator('[data-dialog-close]').click();
    await expect(dialog).toBeHidden();
    await expect(captionButton).toBeFocused();

    await captionButton.click();
    await page.keyboard.press('Escape');
    await expect(dialog).toBeHidden();
    await expect(captionButton).toBeFocused();

    // O **mesmo** diálogo serve o alt: o título precisa trocar junto (chave `media_alt`).
    await figure.click();
    await page.getByRole('button', { name: 'Texto alternativo' }).click();
    await expect(dialog).toBeVisible();
    await expect(dialog.locator('[data-text-dialog-title]')).toHaveText('Texto alternativo');
    await expect(dialog.locator('[data-text-dialog-label]')).toHaveText('Texto alternativo (descreva a imagem)');

    await dialog.locator('input[name="value"]').fill('Imagem de teste (alt)');
    await dialog.locator('[data-dialog-apply]').click();
    await expect(dialog).toBeHidden();
    await expect(figure.locator('img')).toHaveAttribute('alt', 'Imagem de teste (alt)');
  });
});
