/**
 * CAMPANHA PELO NÚMERO OFICIAL, COM MODELO APROVADO (#1330, fatia 4; migration 0710).
 *
 * Pela tela, como quem opera faria: escolhe o número oficial, o campo de texto
 * dá lugar aos modelos aprovados daquele número; escolhe o modelo, diz de onde
 * sai cada variável, vê a prévia; conta o público (quem não tem o dado fica de
 * fora, com o motivo); salva, prepara a lista e confere o detalhe.
 *
 * O ENVIO de teste só roda com o servidor apontado para um dublê do provedor
 * intermediado — é ambiente do `next start`, não da spec, e por isso esse caso
 * pula no CI (o resto roda). Para rodá-lo local:
 *
 *   ZERNIO_API_BASE_URL=http://127.0.0.1:3997 ZERNIO_ACCOUNT_ID=ACC-E2E-CAMPANHA \
 *   ZERNIO_API_KEY=sk_e2e pnpm playwright test tests/e2e/campanha-com-modelo-aprovado.spec.ts
 *
 * O dublê recebe o pedido e a spec confere o que chegou: o nome e o idioma do
 * modelo, a lista de valores na ordem do contrato e a imagem do cabeçalho.
 *
 * ENVIO RÁPIDO (migration 0711): o número oficial oferece "Rápido" no ritmo; a
 * confirmação diz em quantos minutos sai; e — com o dublê — UMA rodada do cron
 * manda a lista inteira, cada um com o próprio nome, e conclui a campanha.
 */
import * as fs from "node:fs";
import * as http from "node:http";
import * as path from "node:path";

import { expect, test, type Page } from "./helpers/test";
import { createClient } from "@supabase/supabase-js";

import { carregarEnvLocal } from "../../scripts/lib/env-de-teste";

const PORTA = 3997;
const URL_DO_DUBLE = `http://127.0.0.1:${PORTA}`;
const CONTA = "ACC-E2E-CAMPANHA";
const TEM_DUBLE = process.env.ZERNIO_API_BASE_URL === URL_DO_DUBLE && process.env.ZERNIO_ACCOUNT_ID === CONTA;

const CREDS_PATH = path.join(process.cwd(), ".e2e-creds.json");
const EVIDENCIA = process.env.E2E_EVIDENCIA ?? path.join(process.cwd(), "evidence/campanha-com-modelo-aprovado");

interface Creds {
  password: string;
  org_id: string;
  users: Record<string, { email: string }>;
}

const SUFIXO = String(Date.now()).slice(-7);
const ETIQUETA = `e2e-campanha-modelo-${SUFIXO}`;
const ETIQUETA_RAPIDA = `e2e-campanha-rapida-${SUFIXO}`;
const NOMES_DA_RAPIDA = ["Ana", "Bea", "Carla"];
const ETIQUETA_BROADCAST = `e2e-campanha-broadcast-${SUFIXO}`;
const NOMES_DO_BROADCAST = ["Dora", "Eva", "Flor"];
const ETIQUETA_RETOMADA = `e2e-campanha-retomada-${SUFIXO}`;
const NOMES_DA_RETOMADA = ["Gina", "Hilda", "Iris"];
const TRANSMISSAO = `BC-E2E-${SUFIXO}`;
const MODELO = "oferta_e2e_campanha";
const PAUSADO = "pausado_e2e_campanha";
const IMAGEM_SALVA = "https://example.com/e2e/cafeteira.jpg";
const NUMERO_DO_CANAL = `Número oficial E2E ${SUFIXO}`;
const NOME_DA_CAMPANHA = `Campanha com modelo E2E ${SUFIXO}`;
const COMPONENTES = [
  { type: "HEADER", format: "IMAGE" },
  { type: "BODY", text: "Olá {{1}}! A {{2}} sai por R$ 189,90 com frete grátis. Quer que a gente separe uma?" },
  { type: "FOOTER", text: "Loja Exemplo" },
  { type: "BUTTONS", buttons: [{ type: "QUICK_REPLY", text: "Sim, quero" }, { type: "QUICK_REPLY", text: "Não, obrigado" }] },
];

const recebidos: { metodo: string; caminho: string; corpo: Record<string, unknown> | null }[] = [];
let servidor: http.Server | null = null;

/**
 * O broadcast que o dublê guarda: os telefones recebidos e o estado (migration
 * 0712). Como no provedor, os telefones só crescem: sair da lista exige apagar o
 * rascunho. `recusarOEnvio` faz o `send` recusar uma vez, com os telefones já lá.
 */
const transmissao = { id: TRANSMISSAO, criadas: 0, telefones: [] as string[], estado: "draft", recusarOEnvio: false };

/**
 * As respostas de broadcast seguem a especificação publicada do provedor e a
 * prova real de 16/10/2026 (`profileId` é objeto; o relatório traz
 * `platformIdentifier` só com dígitos).
 */
function respostaDeBroadcast(metodo: string, caminho: string, corpo: Record<string, unknown> | null): unknown {
  if (metodo === "GET" && caminho === "/v1/accounts") {
    return { accounts: [{ _id: CONTA, profileId: { _id: "PERFIL-E2E", name: "Perfil E2E" } }] };
  }
  if (metodo === "POST" && caminho === "/v1/broadcasts") {
    transmissao.criadas += 1;
    transmissao.id = transmissao.criadas === 1 ? TRANSMISSAO : `${TRANSMISSAO}-${transmissao.criadas}`;
    transmissao.telefones = [];
    transmissao.estado = "draft";
    return { success: true, broadcast: { id: transmissao.id, status: "draft" } };
  }
  const base = `/v1/broadcasts/${transmissao.id}`;
  if (metodo === "POST" && caminho === `${base}/recipients`) {
    const novos = ((corpo?.phones as string[]) ?? []).filter((t) => !transmissao.telefones.includes(t));
    transmissao.telefones.push(...novos);
    return { success: true, added: novos.length, skipped: 0 };
  }
  if (metodo === "DELETE" && caminho === base) {
    transmissao.telefones = [];
    return { success: true };
  }
  if (metodo === "POST" && caminho === `${base}/send`) {
    if (transmissao.recusarOEnvio) {
      transmissao.recusarOEnvio = false;
      return { success: false, code: "http_503", error: "fora do ar" };
    }
    transmissao.estado = "completed";
    return { success: true, status: "completed", sent: transmissao.telefones.length, failed: 0 };
  }
  if (metodo === "GET" && caminho === base) {
    return { success: true, broadcast: { id: transmissao.id, status: transmissao.estado } };
  }
  if (metodo === "GET" && caminho === `${base}/recipients`) {
    const agora = new Date().toISOString();
    return {
      success: true,
      recipients: transmissao.telefones.map((t, i) => ({
        id: `r${i}`,
        platformIdentifier: t.replace(/\D/g, ""),
        contactName: t,
        status: transmissao.estado === "draft" ? "pending" : "delivered",
        messageId: `wamid.BC${SUFIXO}-${transmissao.criadas}-${i}`,
        sentAt: agora,
        deliveredAt: agora,
        readAt: null,
      })),
      pagination: { total: transmissao.telefones.length, limit: 100, skip: 0, hasMore: false },
    };
  }
  return null;
}

function dubleDoProvedor(): http.Server {
  return http.createServer((req, res) => {
    let bruto = "";
    req.on("data", (c) => (bruto += c));
    req.on("end", () => {
      const url = new URL(req.url ?? "/", URL_DO_DUBLE);
      const corpo = bruto ? (JSON.parse(bruto) as Record<string, unknown>) : null;
      recebidos.push({ metodo: req.method ?? "", caminho: url.pathname, corpo });
      const broadcast = respostaDeBroadcast(req.method ?? "", url.pathname, corpo);
      if (broadcast !== null) {
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(JSON.stringify(broadcast));
        return;
      }
      res.writeHead(201, { "Content-Type": "application/json" });
      // wamid único por envio: `(organization_id, external_id)` é único em messages.
      res.end(
        JSON.stringify({
          success: true,
          data: { messageId: `wamid.E2E${SUFIXO}-${recebidos.length}`, conversationId: "6a3580f68fcd5b3a5b946bf8" },
        }),
      );
    });
  });
}

async function login(page: Page, email: string, senha: string): Promise<void> {
  await page.goto("/login");
  await page.locator("#email").fill(email);
  await page.locator("#password").fill(senha);
  await page.getByRole("button", { name: "Entrar", exact: true }).click();
  await page.waitForURL(/\/app/, { timeout: 60_000 });
}

test.describe("Campanhas — número oficial manda modelo aprovado", () => {
  test.describe.configure({ mode: "serial", timeout: 240_000 });

  const env = carregarEnvLocal();
  const admin = createClient(env.NEXT_PUBLIC_SUPABASE_URL!, env.SUPABASE_SERVICE_ROLE_KEY!, {
    auth: { autoRefreshToken: false, persistSession: false },
  });
  let creds: Creds;
  let sessaoId = "";
  let campanhaId = "";
  let campanhaRapidaId = "";
  let campanhaBroadcastId = "";
  let campanhaRetomadaId = "";

  test.beforeAll(async () => {
    creds = JSON.parse(fs.readFileSync(CREDS_PATH, "utf8")) as Creds;
    if (TEM_DUBLE) {
      servidor = dubleDoProvedor();
      await new Promise<void>((ok) => servidor!.listen(PORTA, "127.0.0.1", ok));
    }
    const { data: sessao, error: erroSessao } = await admin
      .from("channel_sessions")
      .insert({
        organization_id: creds.org_id,
        provider: "zernio",
        zernio_account_id: CONTA,
        webhook_path_token: `e2e-campanha-modelo-${SUFIXO}`,
        webhook_secret_encrypted: "\\x00",
        display_name: NUMERO_DO_CANAL,
        phone_number: `+551197${SUFIXO}`,
        status: "WORKING",
      })
      .select("id")
      .single();
    if (erroSessao) throw new Error(`sessão oficial: ${erroSessao.message}`);
    sessaoId = (sessao as { id: string }).id;
    // A janela do número cobre o dia inteiro: a spec não pode reprovar por rodar à noite.
    const { error: erroKnobs } = await admin.from("channel_knobs").insert({
      organization_id: creds.org_id,
      channel_session_id: sessaoId,
      window_start_hour: 0,
      window_end_hour: 24,
      allow_sunday: true,
    });
    if (erroKnobs) throw new Error(`janela do número: ${erroKnobs.message}`);

    const { error: erroModelos } = await admin.from("meta_templates").insert([
      {
        organization_id: creds.org_id,
        channel_session_id: sessaoId,
        waba_id: CONTA,
        name: MODELO,
        language: "pt_BR",
        status: "APPROVED",
        category: "MARKETING",
        components: COMPONENTES,
        contract_hash: "",
        parameter_format: "POSITIONAL",
        saved_values: { "header:1": IMAGEM_SALVA },
      },
      {
        organization_id: creds.org_id,
        channel_session_id: sessaoId,
        waba_id: CONTA,
        name: PAUSADO,
        language: "pt_BR",
        status: "PAUSED",
        category: "MARKETING",
        components: [{ type: "BODY", text: "Modelo pausado" }],
        contract_hash: "",
        parameter_format: "POSITIONAL",
        // Inserção em lote: coluna ausente numa linha vira null, e esta é NOT NULL.
        saved_values: {},
      },
    ]);
    if (erroModelos) throw new Error(`modelos: ${erroModelos.message}`);

    const { error: erroContatos } = await admin.from("contacts").insert([
      {
        organization_id: creds.org_id,
        display_name: `Maria José E2E ${SUFIXO}`,
        phone_number: `+55119${SUFIXO}1`,
        tags: [ETIQUETA],
      },
      {
        // Nome que é só telefone: "Olá +55…!" é pior do que não mandar.
        organization_id: creds.org_id,
        display_name: `+55 11 9${SUFIXO}2`,
        phone_number: `+55119${SUFIXO}2`,
        tags: [ETIQUETA],
      },
    ]);
    if (erroContatos) throw new Error(`contatos: ${erroContatos.message}`);

    const { error: erroRapida } = await admin.from("contacts").insert(
      NOMES_DA_RAPIDA.map((nome, i) => ({
        organization_id: creds.org_id,
        display_name: `${nome} E2E ${SUFIXO}`,
        phone_number: `+55118${SUFIXO}${i}`,
        tags: [ETIQUETA_RAPIDA],
      })),
    );
    if (erroRapida) throw new Error(`contatos da rápida: ${erroRapida.message}`);

    const { error: erroBroadcast } = await admin.from("contacts").insert(
      NOMES_DO_BROADCAST.map((nome, i) => ({
        organization_id: creds.org_id,
        display_name: `${nome} E2E ${SUFIXO}`,
        phone_number: `+55117${SUFIXO}${i}`,
        tags: [ETIQUETA_BROADCAST],
      })),
    );
    if (erroBroadcast) throw new Error(`contatos do broadcast: ${erroBroadcast.message}`);

    const { error: erroRetomada } = await admin.from("contacts").insert(
      NOMES_DA_RETOMADA.map((nome, i) => ({
        organization_id: creds.org_id,
        display_name: `${nome} E2E ${SUFIXO}`,
        phone_number: `+55116${SUFIXO}${i}`,
        tags: [ETIQUETA_RETOMADA],
      })),
    );
    if (erroRetomada) throw new Error(`contatos da retomada: ${erroRetomada.message}`);
  });

  test.afterAll(async () => {
    for (const id of [campanhaId, campanhaRapidaId, campanhaBroadcastId, campanhaRetomadaId].filter(Boolean)) {
      await admin.from("campaign_recipients").delete().eq("campaign_id", id);
      await admin.from("campaigns").delete().eq("id", id);
    }
    for (const etiqueta of [ETIQUETA, ETIQUETA_RAPIDA, ETIQUETA_BROADCAST, ETIQUETA_RETOMADA]) {
      const { data } = await admin.from("contacts").select("id").contains("tags", [etiqueta]);
      const ids = ((data as Array<{ id: string }> | null) ?? []).map((c) => c.id);
      if (ids.length) {
        await admin.from("messages").delete().in("contact_id", ids);
        await admin.from("conversations").delete().in("contact_id", ids);
        await admin.from("contacts").delete().in("id", ids);
      }
    }
    if (sessaoId) {
      await admin.from("meta_templates").delete().eq("channel_session_id", sessaoId);
      await admin.from("channel_sessions").delete().eq("id", sessaoId);
    }
    await new Promise<void>((ok) => (servidor ? servidor.close(() => ok()) : ok()));
  });

  test("⭐ escolher o modelo, dizer de onde sai cada variável, ver a prévia e salvar", async ({ page }) => {
    fs.mkdirSync(EVIDENCIA, { recursive: true });
    await login(page, creds.users.manager!.email, creds.password);
    await page.goto("/app/campaigns/new");

    await page.locator("#nome").fill(NOME_DA_CAMPANHA);
    await page.locator("#canal").selectOption(sessaoId);

    // Número oficial: o campo de texto livre dá lugar ao seletor de modelos.
    const bloco = page.getByTestId("modelo-da-campanha");
    await expect(bloco).toBeVisible({ timeout: 30_000 });
    await expect(page.getByLabel("Texto da mensagem")).toHaveCount(0);
    const seletor = page.locator("#modelo");
    // O pausado aparece, mas não dá para escolher.
    await expect(seletor.locator(`option[value="${PAUSADO}|pt_BR"]`)).toBeDisabled();
    await seletor.selectOption(`${MODELO}|pt_BR`);

    // A imagem do cabeçalho já vem com o link salvo na aba de modelos.
    const variaveis = page.getByTestId("variaveis-do-modelo");
    await expect(variaveis).toBeVisible();
    await expect(page.getByLabel("Texto fixo: a mídia do cabeçalho")).toHaveValue(IMAGEM_SALVA);
    await expect(page.getByTestId("variaveis-incompletas")).toContainText("{{1}}");

    await page.getByLabel("Origem: {{1}}", { exact: true }).selectOption("contact_first_name");
    await page.getByLabel("Origem: {{2}}", { exact: true }).selectOption("fixed");
    await page.getByLabel("Texto fixo: {{2}}", { exact: true }).fill("Cafeteira Elétrica");
    await expect(page.getByTestId("variaveis-incompletas")).toHaveCount(0);

    const previa = page.getByTestId("previa-modelo");
    await expect(previa).toContainText("Olá [Primeiro nome do contato]! A Cafeteira Elétrica sai por R$ 189,90");
    await expect(previa).toContainText("Sim, quero");
    await expect(previa.locator("img")).toHaveAttribute("src", IMAGEM_SALVA);
    await page.screenshot({ path: path.join(EVIDENCIA, "01-modelo-escolhido-com-previa.png"), fullPage: true });

    // Público pela etiqueta: quem tem nome que é só telefone fica de fora, com o motivo.
    await page.getByLabel("Com alguma destas etiquetas").fill(ETIQUETA);
    await page.getByRole("button", { name: "Ver quantas pessoas" }).click();
    await expect(page.getByText(/1\s*podem receber/)).toBeVisible({ timeout: 30_000 });
    await expect(page.getByText(/Falta um dado que a mensagem usa/)).toBeVisible();
    await page.screenshot({ path: path.join(EVIDENCIA, "02-publico-com-quem-fica-de-fora.png"), fullPage: true });

    await page.getByRole("button", { name: "Salvar rascunho" }).click();
    await page.waitForURL(/\/app\/campaigns\/[0-9a-f-]{36}$/, { timeout: 60_000 });
    campanhaId = page.url().split("/").pop()!;

    const { data: gravada } = await admin
      .from("campaigns")
      .select("message_mode, message_body, provider_template_name, provider_template_language, provider_template_variables")
      .eq("id", campanhaId)
      .single();
    expect(gravada).toMatchObject({
      message_mode: "template",
      message_body: null,
      provider_template_name: MODELO,
      provider_template_language: "pt_BR",
      provider_template_variables: {
        "header:1": { kind: "fixed", value: IMAGEM_SALVA },
        "1": { kind: "contact_first_name" },
        "2": { kind: "fixed", value: "Cafeteira Elétrica" },
      },
    });

    // O detalhe mostra o modelo e a prévia, não um texto vazio.
    const noDetalhe = page.getByTestId("modelo-no-detalhe");
    await expect(noDetalhe).toContainText(MODELO);
    await expect(noDetalhe.getByTestId("previa-modelo")).toContainText("Cafeteira Elétrica");
    await page.screenshot({ path: path.join(EVIDENCIA, "03-detalhe-do-rascunho.png"), fullPage: true });
  });

  test("preparar a lista congela os valores de cada contato", async ({ page }) => {
    await login(page, creds.users.manager!.email, creds.password);
    await page.goto(`/app/campaigns/${campanhaId}`);
    await page.getByRole("button", { name: "Preparar lista" }).click();
    await expect(page.getByRole("button", { name: "Enviar teste" })).toBeVisible({ timeout: 60_000 });

    const { data: destinatarios } = await admin
      .from("campaign_recipients")
      .select("status, exclusion_reason, variables, rendered_body")
      .eq("campaign_id", campanhaId);
    const lista = (destinatarios ?? []) as Array<{
      status: string;
      exclusion_reason: string | null;
      variables: { modelo?: Record<string, string> };
      rendered_body: string | null;
    }>;
    const elegivel = lista.find((d) => d.status === "pending");
    expect(elegivel?.variables.modelo).toEqual({ "header:1": IMAGEM_SALVA, "1": "Maria", "2": "Cafeteira Elétrica" });
    // O texto que fica na conversa — o que o agente lê quando o cliente responde.
    expect(elegivel?.rendered_body).toBe("Olá Maria! A Cafeteira Elétrica sai por R$ 189,90 com frete grátis. Quer que a gente separe uma?");
    expect(lista.find((d) => d.status === "skipped")?.exclusion_reason).toBe("variavel_ausente");
    await page.screenshot({ path: path.join(EVIDENCIA, "04-lista-preparada.png"), fullPage: true });
  });

  test("o envio de teste sai como modelo, com os valores na ordem e a imagem do cabeçalho", async ({ page }) => {
    test.skip(!TEM_DUBLE, `precisa do dublê do provedor: ZERNIO_API_BASE_URL=${URL_DO_DUBLE} ZERNIO_ACCOUNT_ID=${CONTA}`);
    await login(page, creds.users.manager!.email, creds.password);
    await page.goto(`/app/campaigns/${campanhaId}`);
    await page.getByRole("button", { name: "Enviar teste" }).click();
    await page.getByLabel("Contato do teste").fill(`Maria José E2E ${SUFIXO}`);
    await page.getByRole("button", { name: "Enviar para este" }).first().click();

    await expect.poll(() => recebidos.filter((r) => r.caminho === "/v1/inbox/conversations").length, { timeout: 30_000 }).toBe(1);
    const pedido = recebidos.find((r) => r.caminho === "/v1/inbox/conversations")!.corpo!;
    expect(pedido).toMatchObject({
      accountId: CONTA,
      templateName: MODELO,
      templateLanguage: "pt_BR",
      templateParams: ["Maria", "Cafeteira Elétrica"],
      headerMedia: { type: "image", link: IMAGEM_SALVA },
    });

    // Na conversa fica o texto preenchido, como modelo.
    const { data: msg } = await admin
      .from("messages")
      .select("type, body, template_name, status")
      .eq("organization_id", creds.org_id)
      .eq("template_name", MODELO)
      .order("created_at", { ascending: false })
      .limit(1)
      .single();
    expect(msg).toMatchObject({
      type: "template",
      template_name: MODELO,
      body: "Olá Maria! A Cafeteira Elétrica sai por R$ 189,90 com frete grátis. Quer que a gente separe uma?",
    });
    await page.screenshot({ path: path.join(EVIDENCIA, "05-teste-enviado.png"), fullPage: true });
  });

  test("o número oficial oferece o envio rápido, e a confirmação diz em quanto tempo sai", async ({ page }) => {
    await login(page, creds.users.manager!.email, creds.password);
    await page.goto(`/app/campaigns/${campanhaId}`);

    const velocidade = page.getByTestId("velocidade-da-campanha");
    await expect(velocidade).toBeVisible({ timeout: 30_000 });
    // Campanha que já existia é gradual: a rápida é escolha, nunca padrão.
    await expect(velocidade.getByRole("radio", { name: "Gradual" })).toBeChecked();
    await velocidade.getByRole("radio", { name: "Rápido" }).check();
    // Na rápida o intervalo não vale, e a tela não o oferece.
    await expect(page.locator("#r-intervalo")).toHaveCount(0);
    await page.getByRole("button", { name: "Salvar ritmo" }).click();
    await expect(page.getByText("Ritmo salvo.")).toBeVisible({ timeout: 30_000 });

    const { data: salva } = await admin.from("campaigns").select("velocidade").eq("id", campanhaId).single();
    expect((salva as { velocidade: string }).velocidade).toBe("rapida");

    await page.getByRole("button", { name: "Iniciar envio" }).click();
    await expect(page.getByText(/Envio rápido: sai tudo em cerca de 1 min\./)).toBeVisible();
    await page.screenshot({ path: path.join(EVIDENCIA, "06-envio-rapido-escolhido.png"), fullPage: true });
    await page.getByRole("button", { name: "Voltar" }).click();
  });

  test("⭐ envio rápido: uma rodada manda a lista inteira, cada um com o seu nome", async ({ page }) => {
    test.skip(!TEM_DUBLE, `precisa do dublê do provedor: ZERNIO_API_BASE_URL=${URL_DO_DUBLE} ZERNIO_ACCOUNT_ID=${CONTA}`);
    await login(page, creds.users.manager!.email, creds.password);
    await page.goto("/app/campaigns/new");

    await page.locator("#nome").fill(`Rápida E2E ${SUFIXO}`);
    await page.locator("#canal").selectOption(sessaoId);
    await expect(page.getByTestId("modelo-da-campanha")).toBeVisible({ timeout: 30_000 });
    await page.locator("#modelo").selectOption(`${MODELO}|pt_BR`);
    await page.getByLabel("Origem: {{1}}", { exact: true }).selectOption("contact_first_name");
    await page.getByLabel("Origem: {{2}}", { exact: true }).selectOption("fixed");
    await page.getByLabel("Texto fixo: {{2}}", { exact: true }).fill("Cafeteira Elétrica");
    await page.getByLabel("Com alguma destas etiquetas").fill(ETIQUETA_RAPIDA);
    await page.getByTestId("velocidade-da-campanha").getByRole("radio", { name: "Rápido" }).check();
    await page.getByRole("button", { name: "Salvar rascunho" }).click();
    await page.waitForURL(/\/app\/campaigns\/[0-9a-f-]{36}$/, { timeout: 60_000 });
    campanhaRapidaId = page.url().split("/").pop()!;

    await page.getByRole("button", { name: "Preparar lista" }).click();
    await expect(page.getByRole("button", { name: "Iniciar envio" })).toBeVisible({ timeout: 60_000 });
    await page.getByRole("button", { name: "Iniciar envio" }).click();
    await expect(page.getByText(/Envio rápido: sai tudo em cerca de 1 min\./)).toBeVisible();
    await page.getByRole("button", { name: "Confirmar" }).click();
    await expect
      .poll(async () => {
        const { data } = await admin.from("campaigns").select("status").eq("id", campanhaRapidaId).single();
        return (data as { status: string } | null)?.status;
      }, { timeout: 30_000 })
      .toBe("running");

    // UMA rodada do cron — no gradual sairia uma pessoa; na rápida, todas.
    const antes = recebidos.length;
    const tick = await page.request.post("/api/v1/cron/campaign-worker", {
      headers: { authorization: `Bearer ${process.env.INTERNAL_SECRET ?? "e2e-placeholder-nao-e-segredo"}` },
    });
    expect(tick.ok(), `rodada: ${tick.status()} ${await tick.text()}`).toBe(true);

    const enviados = recebidos.slice(antes).filter((r) => r.caminho === "/v1/inbox/conversations");
    expect(enviados).toHaveLength(NOMES_DA_RAPIDA.length);
    const primeiros = enviados.map((r) => (r.corpo!.templateParams as string[])[0]).sort();
    expect(primeiros).toEqual([...NOMES_DA_RAPIDA].sort());

    const { data: destinatarios } = await admin
      .from("campaign_recipients")
      .select("status")
      .eq("campaign_id", campanhaRapidaId);
    expect(((destinatarios ?? []) as Array<{ status: string }>).map((d) => d.status)).toEqual(
      NOMES_DA_RAPIDA.map(() => "sent"),
    );
    const { data: depois } = await admin.from("campaigns").select("status").eq("id", campanhaRapidaId).single();
    expect((depois as { status: string }).status).toBe("completed");

    // Pela tela: os contadores mostram a lista inteira enviada.
    await page.reload();
    await expect(page.getByText("Enviadas", { exact: true }).locator("xpath=following-sibling::p[1]")).toHaveText(
      String(NOMES_DA_RAPIDA.length),
      { timeout: 30_000 },
    );
    await page.screenshot({ path: path.join(EVIDENCIA, "07-rapida-lista-inteira-numa-rodada.png"), fullPage: true });
  });
  test("o número oficial oferece o broadcast; com dado de cada pessoa ele fica travado, com o motivo", async ({ page }) => {
    await login(page, creds.users.manager!.email, creds.password);
    await page.goto("/app/campaigns/new");
    await page.locator("#nome").fill(`Broadcast E2E ${SUFIXO}`);
    await page.locator("#canal").selectOption(sessaoId);
    await expect(page.getByTestId("modelo-da-campanha")).toBeVisible({ timeout: 30_000 });
    await page.locator("#modelo").selectOption(`${MODELO}|pt_BR`);
    await page.getByLabel("Origem: {{1}}", { exact: true }).selectOption("contact_first_name");
    await page.getByLabel("Origem: {{2}}", { exact: true }).selectOption("fixed");
    await page.getByLabel("Texto fixo: {{2}}", { exact: true }).fill("Cafeteira Elétrica");

    // O nome muda de pessoa para pessoa: o broadcast aparece, travado, e diz por quê.
    const velocidade = page.getByTestId("velocidade-da-campanha");
    const broadcast = velocidade.getByRole("radio", { name: "Broadcast" });
    await expect(broadcast).toBeVisible({ timeout: 30_000 });
    await expect(broadcast).toBeDisabled();
    await expect(page.getByTestId("broadcast-travado")).toContainText("{{1}}");
    await page.screenshot({ path: path.join(EVIDENCIA, "08-broadcast-travado-com-nome.png"), fullPage: true });

    // Texto fixo nas duas: o broadcast destrava, e a tela diz o que sai para todos.
    await page.getByLabel("Origem: {{1}}", { exact: true }).selectOption("fixed");
    await page.getByLabel("Texto fixo: {{1}}", { exact: true }).fill("amiga");
    await expect(broadcast).toBeEnabled();
    await broadcast.check();
    await expect(page.getByTestId("broadcast-travado")).toHaveCount(0);
    await expect(page.getByTestId("broadcast-texto-fixo")).toContainText("«amiga»");
    await expect(page.getByTestId("broadcast-texto-fixo")).toContainText("«Cafeteira Elétrica»");
    // A lista sai inteira: a tela não oferece o intervalo nem os máximos da campanha.
    await expect(page.locator("#intervalo")).toHaveCount(0);
    await expect(page.locator("#teto")).toHaveCount(0);
    await expect(page.locator("#teto-hora")).toHaveCount(0);
    await expect(page.locator("#janela-inicio")).toBeVisible();
    await expect(page.getByTestId("broadcast-teto")).toContainText("teto diário do número");
    await expect(page.getByTestId("broadcast-parar")).toContainText("cancelando a campanha inteira");
    await page.screenshot({ path: path.join(EVIDENCIA, "09-broadcast-escolhido-texto-fixo.png"), fullPage: true });
    await page.setViewportSize({ width: 375, height: 800 });
    const noCelular = await page.evaluate(() => ({ rolavel: document.documentElement.scrollWidth, tela: document.documentElement.clientWidth }));
    expect(noCelular.rolavel).toBeLessThanOrEqual(noCelular.tela);
    await page.screenshot({ path: path.join(EVIDENCIA, "09-broadcast-escolhido-375px.png"), fullPage: true });
  });

  test("⭐ broadcast: a lista vai ao provedor numa chamada e o relatório volta à campanha", async ({ page }) => {
    test.skip(!TEM_DUBLE, `precisa do dublê do provedor: ZERNIO_API_BASE_URL=${URL_DO_DUBLE} ZERNIO_ACCOUNT_ID=${CONTA}`);
    await login(page, creds.users.manager!.email, creds.password);
    await page.goto("/app/campaigns/new");

    await page.locator("#nome").fill(`Broadcast E2E ${SUFIXO}`);
    await page.locator("#canal").selectOption(sessaoId);
    await expect(page.getByTestId("modelo-da-campanha")).toBeVisible({ timeout: 30_000 });
    await page.locator("#modelo").selectOption(`${MODELO}|pt_BR`);
    await page.getByLabel("Origem: {{1}}", { exact: true }).selectOption("fixed");
    await page.getByLabel("Texto fixo: {{1}}", { exact: true }).fill("amiga");
    await page.getByLabel("Origem: {{2}}", { exact: true }).selectOption("fixed");
    await page.getByLabel("Texto fixo: {{2}}", { exact: true }).fill("Cafeteira Elétrica");
    await page.getByLabel("Com alguma destas etiquetas").fill(ETIQUETA_BROADCAST);
    await page.getByTestId("velocidade-da-campanha").getByRole("radio", { name: "Broadcast" }).check();
    await page.getByRole("button", { name: "Salvar rascunho" }).click();
    await page.waitForURL(/\/app\/campaigns\/[0-9a-f-]{36}$/, { timeout: 60_000 });
    campanhaBroadcastId = page.url().split("/").pop()!;
    const { data: salva } = await admin.from("campaigns").select("velocidade").eq("id", campanhaBroadcastId).single();
    expect((salva as { velocidade: string }).velocidade).toBe("transmissao");

    await page.getByRole("button", { name: "Preparar lista" }).click();
    await expect(page.getByRole("button", { name: "Iniciar envio" })).toBeVisible({ timeout: 60_000 });
    await page.getByRole("button", { name: "Iniciar envio" }).click();
    await expect(page.getByText(/Broadcast: a lista inteira vai para o provedor de uma vez/)).toBeVisible();
    await page.getByRole("button", { name: "Confirmar" }).click();
    await expect
      .poll(async () => {
        const { data } = await admin.from("campaigns").select("status").eq("id", campanhaBroadcastId).single();
        return (data as { status: string } | null)?.status;
      }, { timeout: 30_000 })
      .toBe("running");

    const rodada = () =>
      page.request.post("/api/v1/cron/campaign-worker", {
        headers: { authorization: `Bearer ${process.env.INTERNAL_SECRET ?? "e2e-placeholder-nao-e-segredo"}` },
      });

    // 1ª rodada: cria o broadcast, entrega os telefones e manda — nenhum envio de um em um.
    const antes = recebidos.length;
    const tick = await rodada();
    expect(tick.ok(), `rodada: ${tick.status()} ${await tick.text()}`).toBe(true);
    const desta = recebidos.slice(antes);
    expect(desta.filter((r) => r.caminho === "/v1/inbox/conversations")).toHaveLength(0);
    const criar = desta.find((r) => r.metodo === "POST" && r.caminho === "/v1/broadcasts")!;
    expect(criar.corpo).toMatchObject({
      profileId: "PERFIL-E2E",
      accountId: CONTA,
      platform: "whatsapp",
      template: {
        name: MODELO,
        language: "pt_BR",
        // O valor já escrito: o provedor não completa variável por destinatário.
        components: expect.arrayContaining([
          { type: "header", parameters: [{ type: "image", image: { link: IMAGEM_SALVA } }] },
          { type: "body", parameters: [{ type: "text", text: "amiga" }, { type: "text", text: "Cafeteira Elétrica" }] },
        ]),
      },
    });
    expect(transmissao.telefones.sort()).toEqual(NOMES_DO_BROADCAST.map((_, i) => `+55117${SUFIXO}${i}`).sort());
    expect(desta.some((r) => r.metodo === "POST" && r.caminho === `/v1/broadcasts/${TRANSMISSAO}/send`)).toBe(true);

    const { data: entregue } = await admin
      .from("campaigns")
      .select("provider_broadcast_id")
      .eq("id", campanhaBroadcastId)
      .single();
    expect((entregue as { provider_broadcast_id: string }).provider_broadcast_id).toBe(TRANSMISSAO);

    // 2ª rodada: lê o relatório e leva a cada destinatário; o provedor terminou, a campanha conclui.
    const tick2 = await rodada();
    expect(tick2.ok(), `rodada 2: ${tick2.status()} ${await tick2.text()}`).toBe(true);
    const { data: destinatarios } = await admin
      .from("campaign_recipients")
      .select("status, sent_at, delivered_at")
      .eq("campaign_id", campanhaBroadcastId);
    const lista = (destinatarios ?? []) as Array<{ status: string; sent_at: string | null; delivered_at: string | null }>;
    expect(lista.map((d) => d.status)).toEqual(NOMES_DO_BROADCAST.map(() => "delivered"));
    expect(lista.every((d) => d.sent_at && d.delivered_at)).toBe(true);
    const { data: depois } = await admin.from("campaigns").select("status").eq("id", campanhaBroadcastId).single();
    expect((depois as { status: string }).status).toBe("completed");

    // Pela tela: os contadores mostram a lista inteira entregue.
    await page.reload();
    await expect(page.getByText("Entregues", { exact: true }).locator("xpath=following-sibling::p[1]")).toHaveText(
      String(NOMES_DO_BROADCAST.length),
      { timeout: 30_000 },
    );
    await page.screenshot({ path: path.join(EVIDENCIA, "10-broadcast-entregue-pelo-relatorio.png"), fullPage: true });
  });

  test("⭐ broadcast: acima do teto pausa antes de criar, e quem sai da lista na pausa não recebe na retomada", async ({ page }) => {
    test.skip(!TEM_DUBLE, `precisa do dublê do provedor: ZERNIO_API_BASE_URL=${URL_DO_DUBLE} ZERNIO_ACCOUNT_ID=${CONTA}`);
    const telefones = NOMES_DA_RETOMADA.map((_, i) => `+55116${SUFIXO}${i}`);
    const rodada = async () => {
      const r = await page.request.post("/api/v1/cron/campaign-worker", {
        headers: { authorization: `Bearer ${process.env.INTERNAL_SECRET ?? "e2e-placeholder-nao-e-segredo"}` },
      });
      expect(r.ok(), `rodada: ${r.status()} ${await r.text()}`).toBe(true);
    };
    const daCampanha = async (): Promise<{ status: string; failure_code: string | null; provider_broadcast_id: string | null }> => {
      const { data } = await admin
        .from("campaigns")
        .select("status, failure_code, provider_broadcast_id")
        .eq("id", campanhaRetomadaId)
        .single();
      return data as { status: string; failure_code: string | null; provider_broadcast_id: string | null };
    };
    const retomarPelaTela = async () => {
      await page.reload();
      await page.getByRole("button", { name: "Retomar", exact: true }).click();
      await expect.poll(async () => (await daCampanha()).status, { timeout: 30_000 }).toBe("running");
    };
    const tetoDoNumero = async (teto: number) => {
      const { error } = await admin.from("channel_sessions").update({ daily_message_limit: teto }).eq("id", sessaoId);
      if (error) throw new Error(`teto do número: ${error.message}`);
    };

    await login(page, creds.users.manager!.email, creds.password);
    await page.goto("/app/campaigns/new");
    await page.locator("#nome").fill(`Broadcast retomada E2E ${SUFIXO}`);
    await page.locator("#canal").selectOption(sessaoId);
    await expect(page.getByTestId("modelo-da-campanha")).toBeVisible({ timeout: 30_000 });
    await page.locator("#modelo").selectOption(`${MODELO}|pt_BR`);
    await page.getByLabel("Origem: {{1}}", { exact: true }).selectOption("fixed");
    await page.getByLabel("Texto fixo: {{1}}", { exact: true }).fill("amiga");
    await page.getByLabel("Origem: {{2}}", { exact: true }).selectOption("fixed");
    await page.getByLabel("Texto fixo: {{2}}", { exact: true }).fill("Cafeteira Elétrica");
    await page.getByLabel("Com alguma destas etiquetas").fill(ETIQUETA_RETOMADA);
    await page.getByTestId("velocidade-da-campanha").getByRole("radio", { name: "Broadcast" }).check();
    await page.getByRole("button", { name: "Salvar rascunho" }).click();
    await page.waitForURL(/\/app\/campaigns\/[0-9a-f-]{36}$/, { timeout: 60_000 });
    campanhaRetomadaId = page.url().split("/").pop()!;
    await page.getByRole("button", { name: "Preparar lista" }).click();
    await expect(page.getByRole("button", { name: "Iniciar envio" })).toBeVisible({ timeout: 60_000 });
    await page.getByRole("button", { name: "Iniciar envio" }).click();
    await page.getByRole("button", { name: "Confirmar" }).click();
    await expect.poll(async () => (await daCampanha()).status, { timeout: 30_000 }).toBe("running");

    // A lista não cabe no teto do número: pausa antes de criar, e a tela diz por quê.
    await tetoDoNumero(2);
    const antesDoTeto = recebidos.length;
    await rodada();
    expect(recebidos.slice(antesDoTeto).filter((r) => r.metodo === "POST" && r.caminho === "/v1/broadcasts")).toHaveLength(0);
    expect(await daCampanha()).toMatchObject({ status: "paused", failure_code: "transmissao_acima_do_teto" });
    await page.reload();
    await expect(page.getByText(/Último problema: A lista inteira não cabe no teto diário deste número/)).toBeVisible({ timeout: 30_000 });
    const opcaoBroadcast = page.getByTestId("velocidade-da-campanha").getByRole("radio", { name: "Broadcast" });
    await expect(opcaoBroadcast).toBeChecked({ timeout: 30_000 });
    await page.screenshot({ path: path.join(EVIDENCIA, "11-broadcast-acima-do-teto.png"), fullPage: true });

    // Com o teto ajustado, os telefones sobem e o envio é recusado: pausa com o rascunho cheio.
    await tetoDoNumero(100_000);
    await retomarPelaTela();
    transmissao.recusarOEnvio = true;
    await rodada();
    expect(await daCampanha()).toMatchObject({ status: "paused", failure_code: "transmissao_http_503" });
    const rascunho = transmissao.id;
    expect([...transmissao.telefones].sort()).toEqual([...telefones].sort());

    // Na pausa, uma pessoa pede para sair: é a marca que a ingestão grava no contato.
    const { error: erroOptOut } = await admin
      .from("contacts")
      .update({ is_blocked: true })
      .eq("organization_id", creds.org_id)
      .eq("phone_number", telefones[0]!);
    if (erroOptOut) throw new Error(`opt-out: ${erroOptOut.message}`);

    await retomarPelaTela();
    const antesDaRetomada = recebidos.length;
    // 1ª rodada: apaga o rascunho, que é a única forma de tirar a pessoa dele.
    await rodada();
    const daRetomada = recebidos.slice(antesDaRetomada);
    expect(daRetomada.some((r) => r.metodo === "DELETE" && r.caminho === `/v1/broadcasts/${rascunho}`)).toBe(true);
    expect(daRetomada.some((r) => r.caminho.endsWith("/send"))).toBe(false);
    // 2ª rodada: rascunho novo, só com quem ficou, e o envio.
    await rodada();
    expect(transmissao.id).not.toBe(rascunho);
    expect([...transmissao.telefones].sort()).toEqual(telefones.slice(1).sort());
    expect(recebidos.filter((r) => r.caminho === `/v1/broadcasts/${transmissao.id}/send`)).toHaveLength(1);
    expect((await daCampanha()).provider_broadcast_id).toBe(transmissao.id);

    // Com a lista entregue, a tela trava a velocidade.
    await page.reload();
    await expect(page.getByTestId("broadcast-entregue")).toBeVisible({ timeout: 30_000 });
    await expect(opcaoBroadcast).toBeChecked({ timeout: 30_000 });
    await expect(opcaoBroadcast).toBeDisabled();

    // 3ª rodada: o relatório conclui; a pessoa que saiu segue fora.
    await rodada();
    expect((await daCampanha()).status).toBe("completed");
    const { data: fim } = await admin
      .from("campaign_recipients")
      .select("status, recipient_address")
      .eq("campaign_id", campanhaRetomadaId);
    const porTelefone = new Map(((fim ?? []) as Array<{ status: string; recipient_address: string }>).map((d) => [d.recipient_address, d.status]));
    expect(porTelefone.get(telefones[0]!)).toBe("opted_out");
    expect(porTelefone.get(telefones[1]!)).toBe("delivered");
    expect(porTelefone.get(telefones[2]!)).toBe("delivered");

    await page.reload();
    await expect(page.getByText("Entregues", { exact: true }).locator("xpath=following-sibling::p[1]")).toHaveText("2", { timeout: 30_000 });
    await page.screenshot({ path: path.join(EVIDENCIA, "12-broadcast-retomada-sem-quem-saiu.png"), fullPage: true });
    await page.setViewportSize({ width: 375, height: 800 });
    await page.reload();
    await expect(page.getByText("Entregues", { exact: true }).locator("xpath=following-sibling::p[1]")).toHaveText("2", { timeout: 30_000 });
    const larguras = await page.evaluate(() => ({ rolavel: document.documentElement.scrollWidth, tela: document.documentElement.clientWidth }));
    expect(larguras.rolavel).toBeLessThanOrEqual(larguras.tela);
    await page.screenshot({ path: path.join(EVIDENCIA, "13-broadcast-retomada-375px.png"), fullPage: true });
  });
});


test("campanha de texto prepara a reserva e o dado preenchido pelo mesmo fluxo da tela", async ({ page }) => {
  test.setTimeout(150_000);
  const { randomUUID } = await import("node:crypto");
  const { credenciaisSupabaseDeTeste } = await import("../../scripts/lib/env-de-teste");
  const { loginComoAdmin, lerCreds } = await import("./helpers/login-admin");
  const ambiente = credenciaisSupabaseDeTeste();
  expect(["127.0.0.1", "localhost"]).toContain(new URL(ambiente.url).hostname);
  const db = createClient(ambiente.url, ambiente.serviceRole, { auth: { persistSession: false } });
  const cc = JSON.parse(fs.readFileSync(CREDS_PATH, "utf8")) as Creds;
  const sufixo = randomUUID().slice(0, 8);
  const nome = `Reserva campanha ${sufixo}`;
  const etiqueta = `reserva-${sufixo}`;
  let canal = "";
  let campanha = "";
  const contatos: string[] = [];
  try {
    const sessao = await db.from("channel_sessions").insert({ organization_id: cc.org_id, display_name: nome, waha_session_name: `reserva-${sufixo}`, status: "WORKING", webhook_secret_encrypted: "\\x00" }).select("id").single();
    expect(sessao.error).toBeNull();
    canal = sessao.data!.id as string;
    for (const [i, pessoa] of ["Carla Souza", null].entries()) {
      const contato = await db.from("contacts").insert({ organization_id: cc.org_id, name: pessoa, display_name: null, phone_number: `+551197${Date.now().toString().slice(-6)}${i}`, tags: [etiqueta] }).select("id").single();
      expect(contato.error).toBeNull();
      contatos.push(contato.data!.id as string);
    }
    await loginComoAdmin(page, lerCreds());
    // Uma organização no seed: o seletor é oculto por contrato. Canal,
    // público e snapshot abaixo conferem a empresa real da sessão.
    await page.goto("/app/campaigns/new");
    await page.locator("#nome").fill(nome);
    await page.locator("#canal").selectOption(canal);
    await expect(page.getByText('Para não depender do dado, ponha um texto de reserva: {{primeiro_nome|"aluno"}}.')).toBeVisible();
    await page.getByRole("textbox", { name: "Texto da mensagem" }).fill('Oi {{primeiro_nome|"aluno"}}!');
    await page.getByLabel("Com alguma destas etiquetas").fill(etiqueta);
    await page.getByRole("button", { name: "Ver quantas pessoas" }).click();
    await expect(page.getByText(/2\s*podem receber/)).toBeVisible();
    await page.getByRole("button", { name: "Salvar rascunho" }).click();
    await page.waitForURL(/\/app\/campaigns\/[0-9a-f-]{36}$/);
    campanha = page.url().split("/").pop()!;
    await page.getByRole("button", { name: "Preparar lista" }).click();
    await expect(page.getByRole("button", { name: "Enviar teste" })).toBeVisible();
    const snapshot = await db.from("campaign_recipients").select("rendered_body, status").eq("organization_id", cc.org_id).eq("campaign_id", campanha);
    expect(snapshot.error).toBeNull();
    expect(snapshot.data?.map((r) => r.rendered_body).sort()).toEqual(["Oi Carla!", "Oi aluno!"]);
    expect(snapshot.data?.map((r) => r.status)).toEqual(["pending", "pending"]);
    fs.mkdirSync("evidence/antigos-core-3405", { recursive: true });
    await page.screenshot({ path: "evidence/antigos-core-3405/campanha-preparada.png", fullPage: true });
  } finally {
    const erros: boolean[] = [];
    for (const [tabela, coluna, valor] of [
      ["campaign_recipients", "campaign_id", campanha], ["campaigns", "name", nome],
      ...contatos.map((id) => ["contacts", "id", id]), ["channel_sessions", "id", canal],
    ]) {
      if (!valor) continue;
      try {
        const { error } = await db.from(tabela!).delete().eq("organization_id", cc.org_id).eq(coluna!, valor!);
        erros.push(error !== null);
      } catch { erros.push(true); }
    }
    expect(erros.every((erro) => !erro), "limpeza da fixture da campanha").toBe(true);
  }
});
