import { fireEvent, render, screen, within } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";

import { ConversationHeader } from "./ConversationHeader";
import type { ConversationWithContact } from "@/hooks/inbox/useConversationsRealtime";

/**
 * PARIDADE E GUIA DA BARRA DE AÇÕES DO CABEÇALHO (#3003).
 *
 * ─── O que este arquivo prova, e o que ele NÃO prova ─────────────────────────
 *
 * A issue pede duas coisas: (1) menu "⋯" para as ações menos usadas e (2) guia
 * curta em cada botão. As duas estão no cabeçalho. A (1) era proibida pela regra
 * "nenhuma ação de quem atende some da vista" (`#1625`, `#1627`, `#2700`); o doc
 * de decisão 251 (03/11, opção A) a trocou por "nenhuma ação some da conversa:
 * as do dia a dia ficam à vista e o resto fica no ⋯". A paridade abaixo é o que
 * garante que seguimos do lado certo dela: nenhuma ação sai da tela sem ter casa.
 *
 * ─── Por que a paridade é por CENÁRIO, e não uma lista só ────────────────────
 *
 * A barra é CONDICIONAL: "Liberar" só existe para o dono, "Reabrir" só em
 * conversa encerrada, "Arquivar" some na já arquivada. Uma lista única mediria o
 * pior caso e não veria a ação que sumiu de UM estado. Por isso cada cenário
 * declara o seu conjunto EXATO, e um caso à parte afirma que a UNIÃO dos
 * cenários cobre o inventário inteiro: é isso que pega a ação removida do
 * componente (ela deixa de aparecer em qualquer cenário, e a união encolhe).
 *
 * ─── Dublês ─────────────────────────────────────────────────────────────────
 *
 * `useAuth` e `useAutomaticoAtivo` são mutáveis: o estado do cenário decide o
 * que eles devolvem, senão não haveria como montar "com dono, automático de pé".
 * `SnoozeButton`, `DialButton` e `VideoCallButton` são componentes PRÓPRIOS e
 * ficam fora do escopo do #3003 (que é `ConversationHeader.tsx`); o dublê do
 * `SnoozeButton` preserva só o que a PARIDADE mede — que o cabeçalho continua
 * MONTANDO a ação. A guia deles é trabalho dos componentes deles.
 */

const estado = vi.hoisted(() => ({
  auth: {
    user: { id: "u1", support: null } as Record<string, unknown>,
  },
  automaticoDaOrg: true as boolean | undefined,
}));

vi.mock("@/hooks/auth/AuthProvider", () => ({
  useAuth: () => estado.auth,
  usePermission: () => true,
}));
vi.mock("@/hooks/inbox/useClaimConversation", () => ({
  useClaimConversation: () => ({ mutate: vi.fn(), isPending: false }),
}));
vi.mock("@/hooks/inbox/useReleaseConversation", () => ({
  useReleaseConversation: () => ({ mutate: vi.fn(), isPending: false }),
}));
vi.mock("@/hooks/inbox/useCloseConversation", () => ({
  useCloseConversation: () => ({ mutate: vi.fn(), isPending: false }),
  useReopenConversation: () => ({ mutate: vi.fn(), isPending: false }),
  useArchiveConversation: () => ({ mutate: vi.fn(), isPending: false }),
}));
vi.mock("@/hooks/inbox/useResumeAiAttendance", () => ({
  useResumeAiAttendance: () => ({ mutate: vi.fn(), isPending: false }),
}));
vi.mock("@/hooks/inbox/usePauseAiAttendance", () => ({
  usePauseAiAttendance: () => ({ mutate: vi.fn(), isPending: false }),
}));
// "IA com o dono" (#3967) usa `useMutation`: sem o mock, exigiria QueryClientProvider.
vi.mock("@/hooks/inbox/useIaComDono", () => ({
  useIaComDono: () => ({ mutate: vi.fn(), isPending: false }),
}));
vi.mock("@/hooks/contacts/usePersonalContact", () => ({
  useMarkPersonalContact: () => ({ mutate: vi.fn(), isPending: false }),
  useUnmarkPersonalContact: () => ({ mutate: vi.fn(), isPending: false }),
}));
vi.mock("@/hooks/ai/useAutomaticoAtivo", () => ({
  useAutomaticoAtivo: () => ({ data: estado.automaticoDaOrg }),
}));
// Desde a Spec 31 (B2) o cabeçalho consulta o número da resposta por uma query
// (`useQuery`), e este arquivo não monta `QueryClientProvider`. Mockar o hook é o
// tratamento que os outros dados do cabeçalho recebem aqui: sem resposta, o aviso
// de "a resposta sai por" simplesmente não aparece.
vi.mock("@/hooks/inbox/useEnderecoDaResposta", () => ({
  useEnderecoDaResposta: () => ({ data: undefined }),
}));
vi.mock("@/components/kanban/OwnerBadge", () => ({ OwnerBadge: () => null }));
vi.mock("@/components/inbox/FaixaDaCarteira", () => ({ FaixaDaCarteira: () => null }));
vi.mock("@/components/inbox/ReassignDialog", () => ({ ReassignDialog: () => null }));
vi.mock("@/components/inbox/JanelaSelo", () => ({ JanelaSelo: () => null }));
vi.mock("@/components/inbox/ChannelLogo", () => ({ ChannelLogo: () => null }));
vi.mock("@/components/voice/DialButton", () => ({ DialButton: () => null }));
vi.mock("@/components/inbox/VideoCallButton", () => ({ VideoCallButton: () => null }));
vi.mock("@/components/inbox/ParticipantesDaConversa", () => ({
  ParticipantesDaConversa: () => null,
  // O diálogo do convite deixou de morar dentro do menu ⋯ (#3003) e passou a
  // ser montado pelo cabeçalho: os dois nomes precisam existir no dublê.
  DialogoDeConvite: () => null,
}));
vi.mock("@/components/inbox/SnoozeButton", () => ({
  SnoozeButton: () => <button type="button">Lembrar</button>,
}));

/**
 * O INVENTÁRIO — toda ação que a barra do cabeçalho oferece hoje.
 *
 * Ele é a régua da paridade: a união dos cenários tem de ser EXATAMENTE isto.
 * Apagar uma ação do componente encolhe a união de um cenário E desta lista (o
 * caso "a união cobre o inventário" reprova nos dois lados).
 */
const INVENTARIO = [
  "Buscar nesta conversa",
  "Assumir",
  "Liberar",
  "Devolver ao automático",
  "Pausar o automático",
  "Transferir",
  "Lembrar",
  "Fechar",
  "Reabrir",
  "Arquivar",
  "Marcar como pessoal",
  "Desmarcar pessoal",
  "Ver contato",
  "Exportar conversa",
] as const;

/**
 * Botões que a barra RENDERIZA mas que pertencem a outros componentes — a guia
 * deles não está neste arquivo e não pode ser cobrada aqui. Estão nomeados um a
 * um, de propósito: a lista é curta e qualquer botão NOVO que este arquivo
 * desenhe cai fora dela e é cobrado.
 */
const GUIAS_DE_OUTROS_COMPONENTES = ["Lembrar"];

const CONTATO = {
  id: "contato-1",
  display_name: "Fulana",
  name: null,
  phone_number: "5511999",
  tags: [],
  is_blocked: false,
  is_personal: false,
  is_anonymized: false,
  force_human: false,
};

interface Cenario {
  nome: string;
  /** Sobrescreve a conversa base. */
  conversa?: Record<string, unknown>;
  contato?: Record<string, unknown>;
  /** Sobrescreve `estado.auth`. */
  auth?: typeof estado.auth;
  automaticoDaOrg?: boolean | undefined;
  /** O conjunto EXATO de ações que este cenário oferece. */
  esperadas: string[];
  comBusca?: boolean;
}

/**
 * O cenário que oferece MAIS ações de uma vez: é ele que ancora o caso "a guia
 * não renomeou nenhum botão" (abaixo) e precisa de nome próprio — `CENARIOS[1]`
 * é `Cenario | undefined` sob `noUncheckedIndexedAccess`.
 */
const CENARIO_MAIS_COMPLETO: Cenario = {
  nome: "minha, com o automático calado — o estado que devolve as duas voltas",
  conversa: {
    assigned_to_user_id: "u1",
    assignee_kind: "user",
    bot_silenced_until: new Date(Date.now() + 10 * 60_000).toISOString(),
  },
  esperadas: [
    // Sem "Assumir": desde o #3003 o cabeçalho não oferece assumir a conversa
    // que já é sua — era um no-op ao lado de "Liberar" (dois botões, um slot).
    "Liberar",
    "Devolver ao automático",
    "Transferir",
    "Lembrar",
    "Fechar",
    "Arquivar",
    "Marcar como pessoal",
    "Ver contato",
    "Exportar conversa",
  ],
};

const CENARIOS: Cenario[] = [
  {
    nome: "aberta e sem dono — a fila",
    comBusca: true,
    esperadas: [
      "Buscar nesta conversa",
      "Assumir",
      "Transferir",
      "Lembrar",
      "Fechar",
      "Arquivar",
      "Marcar como pessoal",
      "Ver contato",
      "Exportar conversa",
    ],
  },
  CENARIO_MAIS_COMPLETO,
  {
    nome: "com dono e o automático de pé — pausar é gesto PRÓPRIO aqui",
    conversa: { assigned_to_user_id: "u2", assignee_kind: "user" },
    esperadas: [
      "Assumir",
      "Pausar o automático",
      "Transferir",
      "Lembrar",
      "Fechar",
      "Arquivar",
      "Marcar como pessoal",
      "Ver contato",
      "Exportar conversa",
    ],
  },
  {
    nome: "encerrada — o par Fechar/Reabrir vira Reabrir",
    conversa: { status: "closed" },
    esperadas: ["Assumir", "Reabrir", "Arquivar", "Marcar como pessoal", "Ver contato", "Exportar conversa"],
  },
  {
    nome: "arquivada — arquivar de novo não é gesto que exista",
    conversa: { status: "archived" },
    esperadas: ["Assumir", "Reabrir", "Marcar como pessoal", "Ver contato", "Exportar conversa"],
  },
  {
    nome: "contato marcado como pessoal — o par inverte",
    contato: { is_personal: true },
    auth: { user: { id: "u1", support: null, is_platform_admin: true } },
    esperadas: [
      "Assumir",
      "Transferir",
      "Lembrar",
      "Fechar",
      "Arquivar",
      "Desmarcar pessoal",
      "Ver contato",
      "Exportar conversa",
    ],
  },
];

function montar(cenario: Cenario) {
  estado.auth = cenario.auth ?? {
    user: { id: "u1", support: null, is_platform_admin: true },
  };
  estado.automaticoDaOrg = cenario.automaticoDaOrg ?? true;

  const conversa = {
    id: "conv-1",
    organization_id: "org-1",
    contact_id: "contato-1",
    status: "open",
    assigned_to_user_id: null,
    assigned_to_user_name: null,
    assignee_kind: "ai",
    bot_silenced_until: null,
    snooze_until: null,
    service_revision: 3,
    is_group: false,
    tags: [],
    contacts: { ...CONTATO, ...cenario.contato },
    channel_sessions: null,
    ...cenario.conversa,
  } as unknown as ConversationWithContact;

  return render(
    <ConversationHeader conversation={conversa} onBuscar={cenario.comBusca ? () => {} : undefined} />,
  );
}

/**
 * A BARRA DE AÇÕES, ancorada no gatilho do ⋯ — nunca por índice, nunca por
 * rótulo (#3003).
 *
 * Ela era ancorada em `getByText(ancora).closest("button").parentElement`, com a
 * âncora escolhida por cenário. Isso deixou de funcionar quando as ações
 * secundárias passaram a morar no menu: "Pausar o automático" não está mais no
 * DOM com o menu fechado. A âncora nova é o gatilho do ⋯, que existe em TODO
 * estado — é justamente o que garante que nenhuma ação fique sem casa.
 */
function barraDeAcoes(): HTMLElement {
  const barra = screen.getByTestId("menu-de-acoes").parentElement;
  expect(barra, "a barra de ações não renderizou").toBeTruthy();
  return barra as HTMLElement;
}

/** Abre o ⋯ (Radix abre no `pointerdown`) e devolve o conteúdo portalizado. */
async function abrirOMenu(): Promise<HTMLElement> {
  fireEvent.pointerDown(screen.getByTestId("menu-de-acoes"), {
    button: 0,
    ctrlKey: false,
    pointerId: 1,
  });
  const conteudo = await screen.findByTestId("menu-de-acoes-conteudo");
  expect(conteudo, "o menu ⋯ não abriu").toBeTruthy();
  return conteudo;
}

/**
 * "Esta ação está alcançável no cabeçalho?" — na BARRA ou no MENU ⋯.
 *
 * A pergunta mudou de "está na barra?" para "está na tela?", e a paridade não
 * afrouxou: os conjuntos esperados por cenário continuam EXATOS, com a mesma
 * contagem de antes. O que saiu da barra tem de estar no menu — e o caso "o menu
 * ⋯ não é opcional" (`tests/unit/inbox-barra-enxuta-3003.test.tsx`) é quem cobra
 * que o menu exista.
 */
function naArea(rotulo: string): boolean {
  if (screen.queryAllByText(rotulo).length > 0) return true;
  // Ícone (a busca): o nome acessível está em `aria-label`, e ele é lido direto
  // do DOM de propósito. Com o menu ⋯ aberto o Radix é MODAL e marca
  // `aria-hidden` nos irmãos (`hideOthers`), então `getByRole` — que ignora a
  // árvore escondida — não enxergaria o botão da busca. A ação está lá, e o que
  // este arquivo mede é se ela está NA TELA.
  return Array.from(document.querySelectorAll<HTMLElement>("[aria-label]")).some(
    (el) => el.getAttribute("aria-label")?.trim() === rotulo,
  );
}

describe("#3003 — paridade: nenhuma ação da barra desaparece", () => {
  for (const cenario of CENARIOS) {
    it(`${cenario.nome}: continua oferecendo exatamente ${cenario.esperadas.length} ações`, async () => {
      montar(cenario);
      await abrirOMenu();

      const presentes = INVENTARIO.filter((rotulo) => naArea(rotulo));
      expect(
        [...presentes].sort(),
        `o conjunto de ações do cenário "${cenario.nome}" mudou — ação a menos (ou a mais) quebra a paridade do #3003`,
      ).toEqual([...cenario.esperadas].sort());
    });
  }

  it("a união dos cenários cobre TODA ação do inventário — é aqui que a ação removida do componente reprova", async () => {
    const vistas = new Set<string>();
    for (const cenario of CENARIOS) {
      const { unmount } = montar(cenario);
      await abrirOMenu();
      for (const rotulo of INVENTARIO) if (naArea(rotulo)) vistas.add(rotulo);
      unmount();
    }

    expect(
      [...INVENTARIO].filter((rotulo) => !vistas.has(rotulo)),
      "ação do inventário que NENHUM cenário oferece: ela saiu do componente, não do estado",
    ).toEqual([]);
    // Controle de não-vacuidade: a varredura varreu de verdade.
    expect(vistas.size, "a união não pode ser maior que o inventário").toBe(INVENTARIO.length);
  });
});

describe("#3003 — guia curta: todo botão da barra diz o que faz", () => {
  for (const cenario of CENARIOS) {
    it(`${cenario.nome}: nome acessível e guia, em cada botão`, async () => {
      montar(cenario);
      const barra = barraDeAcoes();
      // A barra E o menu: desde o #3003 os itens secundários são `menuitem` num
      // portal, não `button` na barra — e a guia é cobrada dos dois jeitos.
      const menu = await abrirOMenu();
      const alvos = [
        ...Array.from(barra.querySelectorAll<HTMLElement>("button, a[href]")),
        ...Array.from(menu.querySelectorAll<HTMLElement>('[role="menuitem"], a[href]')),
      ];
      expect(alvos.length, "a barra não renderizou botão nenhum").toBeGreaterThan(0);

      for (const el of alvos) {
        const nome = (el.getAttribute("aria-label") ?? el.textContent ?? "").trim();
        expect(nome, `botão sem nome acessível na barra: ${el.outerHTML}`).not.toBe("");

        if (GUIAS_DE_OUTROS_COMPONENTES.includes(nome)) continue;

        // A guia vai em `title` e NÃO em `aria-label`: o `aria-label` SUBSTITUI o
        // nome acessível, e a catraca do mantenedor acha estes botões por
        // `getByRole("button", { name: "Fechar" })`. Guia que renomeia o botão
        // quebraria a régua em vez de ajudar quem está começando.
        expect(
          (el.getAttribute("title") ?? "").trim(),
          `"${nome}" não tem guia curta (#3003): quem está começando não sabe o que ele faz`,
        ).not.toBe("");
      }
    });
  }

  it("a guia não renomeou nenhum botão — o nome acessível continua sendo o rótulo visível", () => {
    montar(CENARIO_MAIS_COMPLETO);
    const barra = barraDeAcoes();
    // "Assumir" saiu desta lista porque ele deixou de ser oferecido a quem já é
    // o dono (#3003): neste cenário a conversa é MINHA, e o botão era um no-op.
    // O rótulo dele continua vigiado no cenário "aberta e sem dono", acima.
    for (const rotulo of ["Liberar", "Devolver ao automático", "Transferir", "Fechar"]) {
      const botao = within(barra).getByRole("button", { name: rotulo });
      expect(botao.textContent?.trim()).toBe(rotulo);
    }
  });
});

/**
 * A GUIA DE "LIBERAR" DIZ PARA ONDE A CONVERSA VAI — e isso depende do estado.
 *
 * `fn_conversation_assign` (dono → null) só limpa `bot_silenced_until` quando não
 * houve escalação (`last_handoff_at is null`); trava do contato, opt-out e org sem
 * automático também deixam a conversa na fila. Uma guia fixa prometendo "o
 * automático assume de novo" mentiria em todos esses estados.
 */
describe("#3003 — a guia de Liberar não promete o automático quando ele não volta", () => {
  const VOLTA = "Solta esta conversa: ela fica sem dono e o atendimento automático volta a responder.";
  const FILA = "Solta esta conversa: ela fica sem dono e volta para a fila, esperando um atendente.";
  const minha = { assigned_to_user_id: "u1", assignee_kind: "user", bot_silenced_until: "infinity" };

  const casos: Array<{ nome: string; cenario: Partial<Cenario>; guia: string }> = [
    { nome: "assumida sem escalação, org com automático", cenario: { conversa: minha }, guia: VOLTA },
    {
      nome: "assumida DEPOIS de uma escalação — a RPC mantém o silêncio",
      cenario: { conversa: { ...minha, last_handoff_at: "2026-10-01T10:00:00Z" } },
      guia: FILA,
    },
    { nome: "org sem automático no ar", cenario: { conversa: minha, automaticoDaOrg: false }, guia: FILA },
    {
      nome: "contato travado (force_human) — Liberar não destrava",
      cenario: { conversa: minha, contato: { force_human: true } },
      guia: FILA,
    },
  ];

  for (const caso of casos) {
    it(caso.nome, () => {
      montar({ nome: caso.nome, esperadas: [], ...caso.cenario });
      expect(screen.getByRole("button", { name: "Liberar" }).getAttribute("title")).toBe(caso.guia);
    });
  }
});
