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

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

/**
 * GATES DO BOTÃO PESSOAL (spec 21, etapa 15 — decisão 1).
 *
 * Atendente não vê o botão; gerente e dono veem. Sem contato, sem botão.
 * Marcar pede confirmação (esconde da operação); desmarcar é direto.
 *
 * ─── SABOTAGEM (prova no CI) ───────────────────────────────────────────────
 * - Trocar o gate para `>= ROLE_RANK.agent`: o caso "atendente não vê" cai.
 * - Tirar a confirmação do marcar: o caso "marcar confirma antes" cai.
 * Linha para reverter: `components/inbox/ConversationHeader.tsx`,
 * `app/app/contacts/[id]/_client.tsx` (mesmo gate, lado a lado com o de admin).
 */

const papel = vi.hoisted(() => ({ atual: "agent" }));
const marcarMutate = vi.hoisted(() => vi.fn());
const desmarcarMutate = vi.hoisted(() => vi.fn());

vi.mock("@/hooks/auth/AuthProvider", () => ({
  useAuth: () => ({
    user: { id: "u1", support: null, is_platform_admin: false },
    activeOrg: { orgId: "org-1", role: papel.atual },
  }),
}));
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/ai/useAutomaticoAtivo", () => ({
  useAutomaticoAtivo: () => ({ data: false }),
}));
vi.mock("@/hooks/contacts/usePersonalContact", () => ({
  useMarkPersonalContact: () => ({ mutate: marcarMutate, isPending: false }),
  useUnmarkPersonalContact: () => ({ mutate: desmarcarMutate, isPending: false }),
}));
// A FAIXA DA CARTEIRA (#2591) passou a ser desenhada pelo cabeçalho — e ela lê a
// carteira com react-query. Aqui estes dois hooks entram mockados como todos os
// outros: este arquivo mede o gate do botão pessoal, não a carteira (essa tem
// arquivo próprio), e sem o Provider o `useQuery` dos dois derrubava a suíte.
vi.mock("@/hooks/contacts/useCarteiraDoContato", () => ({
  useCarteiraDoContato: () => ({ data: { dono_user_id: null }, isLoading: false }),
  useDefinirCarteira: () => ({ mutate: vi.fn(), isPending: false }),
}));
vi.mock("@/hooks/inbox/useAssignableMembers", () => ({
  useAssignableMembers: () => ({ data: [], isLoading: false }),
}));
vi.mock("@/components/kanban/OwnerBadge", () => ({ OwnerBadge: () => null }));
vi.mock("@/components/inbox/ReassignDialog", () => ({ ReassignDialog: () => null }));
vi.mock("@/components/inbox/SnoozeButton", () => ({ SnoozeButton: () => null }));
vi.mock("@/components/inbox/JanelaSelo", () => ({ JanelaSelo: () => null }));
// O endereço da resposta busca com useQuery; sem telefone extra, nada aparece.
vi.mock("@/hooks/inbox/useEnderecoDaResposta", () => ({
  useEnderecoDaResposta: () => ({ data: undefined }),
}));
// O atendimento conjunto (#2542) desenha no cabeçalho e lê react-query (exigiria
// QueryClientProvider). Este arquivo mede o gate do BOTÃO PESSOAL; a lista de
// participantes tem arquivo próprio. Mesmo caminho do ConversationHeader.test.tsx.
vi.mock("@/components/inbox/ParticipantesDaConversa", () => ({
  ParticipantesDaConversa: () => null,
  // O cabeçalho agora monta o diálogo de convite FORA do menu (o menu precisa
  // fechar antes, senão o Radix modal esconde o botão de presença da árvore de
  // acessibilidade). Este arquivo isola o botão pessoal: o diálogo é dublê.
  DialogoDeConvite: () => null,
}));
// A reserva tem sua própria prova autenticada; este gate isola o botão pessoal.
vi.mock("@/components/agenda/ReservarServicoNaConversa", () => ({ ItemReservarServico: () => null, ReservarServicoNaConversa: () => null }));
// O atendimento conjunto (#2542) desenha no cabeçalho e lê react-query (exigiria
// QueryClientProvider). Este arquivo mede o gate do BOTÃO PESSOAL; a lista de
// participantes tem arquivo próprio. Mesmo caminho do ConversationHeader.test.tsx.
vi.mock("@/components/inbox/ParticipantesDaConversa", () => ({
  ParticipantesDaConversa: () => null,
  // O cabeçalho agora monta o diálogo de convite FORA do menu (o menu precisa
  // fechar antes, senão o Radix modal esconde o botão de presença da árvore de
  // acessibilidade). Este arquivo isola o botão pessoal: o diálogo é dublê.
  DialogoDeConvite: () => null,
}));
vi.mock("@/components/inbox/ChannelLogo", () => ({ ChannelLogo: () => null }));
vi.mock("@/hooks/voice/useVoiceSessionStatus", () => ({
  useVoiceSessionStatus: () => ({ data: { configured: true, paired: true } }),
}));
vi.mock("@/components/voice/VoiceCallContext", () => ({
  useVoiceCall: () => ({ call: null, startCall: vi.fn() }),
}));

function conversa(pessoal: boolean | null): ConversationWithContact {
  return {
    id: "conv-1",
    organization_id: "org-1",
    contact_id: "contato-1",
    channel_session_id: "sess-1",
    channel: "whatsapp",
    status: "open",
    status_changed_at: new Date().toISOString(),
    service_revision: 3,
    assigned_to_user_id: null,
    assigned_to_user_name: null,
    assignee_kind: null,
    assigned_at: null,
    last_inbound_at: null,
    last_outbound_at: null,
    last_message_at: null,
    last_message_preview: null,
    unread_count_for_assignee: 0,
    is_group: false,
    group_chat_id: null,
    tags: [],
    metadata: {},
    snooze_until: null,
    contacts:
      pessoal === null
        ? null
        : {
            id: "contato-1",
            display_name: "Mãe",
            name: "Mãe",
            phone_number: "+5511999999999",
            tags: [],
            is_blocked: false,
            is_personal: pessoal,
            is_anonymized: false,
          },
    channel_sessions: null,
  } as unknown as ConversationWithContact;
}

beforeEach(() => {
  papel.atual = "agent";
  marcarMutate.mockReset();
  desmarcarMutate.mockReset();
});

/**
 * O botão pessoal mora no menu ⋯ desde o #3003: a barra ficou com as ações de
 * todo atendimento e o par marcar/desmarcar foi para lá, com ícone, nome e
 * guia. O GATE não mudou de lugar nenhum — continua sendo o mesmo
 * `podeMarcarPessoal` do componente —, então o que este arquivo passa a fazer é
 * olhar DENTRO do menu. Fechado, o menu não renderiza item nenhum: procurar o
 * testid com ele fechado passaria por ausência, e ausência é exatamente o que
 * os casos "não vê" querem provar. Abrir em todos é o que mantém a distinção.
 */
async function abrirOMenu(): Promise<HTMLElement> {
  fireEvent.pointerDown(screen.getByTestId("menu-de-acoes"), {
    button: 0,
    ctrlKey: false,
    pointerId: 1,
  });
  return await screen.findByTestId("menu-de-acoes-conteudo");
}

describe("gate do botão pessoal no cabeçalho", () => {
  it("atendente não vê nem marcar nem desmarcar", async () => {
    papel.atual = "agent";
    const { unmount } = render(<ConversationHeader conversation={conversa(false)} />);
    const menu = await abrirOMenu();
    expect(within(menu).queryByTestId("marcar-pessoal")).toBeNull();
    expect(within(menu).queryByTestId("desmarcar-pessoal")).toBeNull();
    unmount();
  });

  it("gerente vê marcar no contato normal", async () => {
    papel.atual = "manager";
    const { unmount } = render(<ConversationHeader conversation={conversa(false)} />);
    const menu = await abrirOMenu();
    expect(within(menu).getByTestId("marcar-pessoal")).toBeTruthy();
    expect(within(menu).queryByTestId("desmarcar-pessoal")).toBeNull();
    unmount();
  });

  it("gerente vê desmarcar (e não marcar) no contato pessoal", async () => {
    papel.atual = "manager";
    const { unmount } = render(<ConversationHeader conversation={conversa(true)} />);
    const menu = await abrirOMenu();
    expect(within(menu).getByTestId("desmarcar-pessoal")).toBeTruthy();
    expect(within(menu).queryByTestId("marcar-pessoal")).toBeNull();
    unmount();
  });

  it("sem contato, sem botão para ninguém", async () => {
    papel.atual = "manager";
    const { unmount } = render(<ConversationHeader conversation={conversa(null)} />);
    const menu = await abrirOMenu();
    expect(within(menu).queryByTestId("marcar-pessoal")).toBeNull();
    expect(within(menu).queryByTestId("desmarcar-pessoal")).toBeNull();
    unmount();
  });

  it("marcar pede confirmação antes de mutar", async () => {
    papel.atual = "manager";
    const user = userEvent.setup();
    const { unmount } = render(<ConversationHeader conversation={conversa(false)} />);
    const menu = await abrirOMenu();
    await user.click(within(menu).getByTestId("marcar-pessoal"));
    expect(marcarMutate).not.toHaveBeenCalled();
    expect(await screen.findByText("Marcar este contato como pessoal?")).toBeTruthy();
    unmount();
  });

  it("desmarcar é direto no botão", async () => {
    papel.atual = "manager";
    const user = userEvent.setup();
    const { unmount } = render(<ConversationHeader conversation={conversa(true)} />);
    const menu = await abrirOMenu();
    await user.click(within(menu).getByTestId("desmarcar-pessoal"));
    expect(desmarcarMutate).toHaveBeenCalledTimes(1);
    unmount();
  });
});
