import { describe, expect, it, vi, beforeEach } from "vitest";

import { generateDraftReply } from "./draft-reply";
import { loadPublishedAgentConfig, type PublishedAgentConfig } from "./agent-config";
import { getLeadContext, type LeadContextResult } from "../edge/crm/get-lead-context";
import { runModelCall } from "../edge/llm/run-model-call";

vi.mock("./agent-config", () => ({ loadPublishedAgentConfig: vi.fn() }));
vi.mock("../edge/crm/get-lead-context", () => ({ getLeadContext: vi.fn() }));
vi.mock("../edge/llm/run-model-call", () => ({ runModelCall: vi.fn() }));

const mockLoadAgent = vi.mocked(loadPublishedAgentConfig);
const mockGetLeadContext = vi.mocked(getLeadContext);
const mockRunModelCall = vi.mocked(runModelCall);

const db = {} as never;
const llmCfg = {} as never;
const crmCfg = {} as never;

const input = {
  tenantId: "org-1",
  leadId: "contact-1",
  conversationId: "conv-1",
  channelSessionId: "session-1",
};

const AGENT: PublishedAgentConfig = {
  agentId: "agent-1",
  versionId: "version-1",
  agentName: "Bot Deskcomm",
  systemPrompt: "Você é a vendedora da loja X.",
  provider: "anthropic",
  model: "claude-sonnet-4-6",
  credentialId: "cred-1",
  maxSteps: 8,
  historyMessageWindow: 20,
  historyTokenWindow: 1000,
  handoffKeywords: [],
  handoffToolEnabled: false,
  proposalAiDraftEnabled: true,
  splitMessages: false,
  splitMaxChars: 400,
  inboundDebounceMs: null,
  replyMinIntervalMs: null,
  multimodalInput: false,
  casesEnabled: false,
  toolIds: [],
  knowledgeSourceIds: [],
  skillNames: null,
  activeKbVersionId: null,
  ragTopK: 5,
  ragSimilarityThreshold: 0.72,
  janelaDeAtendimento: null,
  versionCreatedBy: null,
  operatorEnabled: false,
  operatorModel: null,
  operatorToolIds: [], pipelineIds: [],
  agentCreatedBy: null,
  vozEnabled: false,
  vozQuando: "espelhar",
};

function contextResult(overrides: Partial<LeadContextResult & { ok: true }> = {}): LeadContextResult {
  return {
    ok: true,
    tokenCount: 42,
    lgpd: {
      isAnonymized: false,
      isProspecting: false,
      legalBasis: { basis: null, legalBasisRef: null, consentGranted: false, dataOrigin: "whatsapp" },
    },
    context: {
      lead_id: input.leadId,
      contact: { name: "Rafael", phone: "+551199", email: null, tags: [], is_blocked: false },
      conversation_id: input.conversationId,
      last_human_decision: null,
      messages: [
        { direction: "inbound", body: "Oi, quero saber o preço do produto X.", sent_at: "2026-07-22T10:00:00Z" },
      ],
    },
    ...overrides,
  };
}

beforeEach(() => {
  vi.clearAllMocks();
});

describe("generateDraftReply", () => {
  it("agente publicado + contexto com histórico → chama runModelCall SEM tools/maxSteps e retorna o rascunho", async () => {
    mockLoadAgent.mockResolvedValue(AGENT);
    mockGetLeadContext.mockResolvedValue(contextResult());
    mockRunModelCall.mockResolvedValue({
      result: { text: "  Olá! O produto X custa R$ 99,90.  " },
      callId: "call-1",
      provider: "anthropic",
      model: "claude-sonnet-4-6",
      usage: { inputTokens: 1, outputTokens: 1, cacheReadTokens: 0, cacheWriteTokens: 0 },
      costCents: 1,
      latencyMs: 1,
    } as never);

    const result = await generateDraftReply(db, llmCfg, crmCfg, input);

    expect(result).toEqual({ ok: true, draft: "Olá! O produto X custa R$ 99,90." });
    expect(mockRunModelCall).toHaveBeenCalledTimes(1);
    const call = mockRunModelCall.mock.calls[0]!;
    const runInput = call[2];
    expect(runInput.purpose).toBe("draft_suggestion");
    expect(runInput.tenantId).toBe(input.tenantId);
    expect(runInput.leadId).toBe(input.leadId);
    expect(runInput).not.toHaveProperty("tools");
    expect(runInput).not.toHaveProperty("maxSteps");
  });

  it("agente que não vê imagens (9108): o rascunho lê o contexto com verImagens=false; o padrão vê", async () => {
    mockGetLeadContext.mockResolvedValue({ ok: false, error: { code: "crm_error", message: "x" } } as never);
    mockLoadAgent.mockResolvedValueOnce({ ...AGENT, imageVisionEnabled: false });
    await generateDraftReply(db, llmCfg, crmCfg, input);
    mockLoadAgent.mockResolvedValueOnce(AGENT);
    await generateDraftReply(db, llmCfg, crmCfg, input);
    expect(mockGetLeadContext.mock.calls[0]![3]).toMatchObject({ verImagens: false });
    expect(mockGetLeadContext.mock.calls[1]![3]).toMatchObject({ verImagens: true });
  });

  it("sem agente publicado → no_agent, sem chamar runModelCall", async () => {
    mockLoadAgent.mockResolvedValue(null);

    const result = await generateDraftReply(db, llmCfg, crmCfg, input);

    expect(result).toEqual({ ok: false, reason: "no_agent" });
    expect(mockGetLeadContext).not.toHaveBeenCalled();
    expect(mockRunModelCall).not.toHaveBeenCalled();
  });

  it("contato bloqueado → blocked, sem chamar runModelCall", async () => {
    mockLoadAgent.mockResolvedValue(AGENT);
    mockGetLeadContext.mockResolvedValue(
      contextResult({
        context: {
          lead_id: input.leadId,
          contact: { name: "Rafael", phone: null, email: null, tags: [], is_blocked: true },
          conversation_id: input.conversationId,
          last_human_decision: null,
          messages: [],
        },
      }),
    );

    const result = await generateDraftReply(db, llmCfg, crmCfg, input);

    expect(result).toEqual({ ok: false, reason: "blocked" });
    expect(mockRunModelCall).not.toHaveBeenCalled();
  });

  it("contato anonimizado (lgpd.isAnonymized) → blocked, sem chamar runModelCall", async () => {
    mockLoadAgent.mockResolvedValue(AGENT);
    mockGetLeadContext.mockResolvedValue(
      contextResult({
        lgpd: {
          isAnonymized: true,
          isProspecting: false,
          legalBasis: { basis: null, legalBasisRef: null, consentGranted: false, dataOrigin: "whatsapp" },
        },
      }),
    );

    const result = await generateDraftReply(db, llmCfg, crmCfg, input);

    expect(result).toEqual({ ok: false, reason: "blocked" });
    expect(mockRunModelCall).not.toHaveBeenCalled();
  });

  it("erro de leitura do CRM (getLeadContext ok:false) → error, não blocked, sem chamar runModelCall", async () => {
    mockLoadAgent.mockResolvedValue(AGENT);
    mockGetLeadContext.mockResolvedValue({ ok: false, reason: "crm_unavailable" } as never);

    const result = await generateDraftReply(db, llmCfg, crmCfg, input);

    expect(result).toEqual({ ok: false, reason: "error" });
    expect(mockRunModelCall).not.toHaveBeenCalled();
  });

  it("histórico vazio (sem mensagens) → empty, sem chamar runModelCall", async () => {
    mockLoadAgent.mockResolvedValue(AGENT);
    mockGetLeadContext.mockResolvedValue(
      contextResult({
        context: {
          lead_id: input.leadId,
          contact: { name: "Rafael", phone: null, email: null, tags: [], is_blocked: false },
          conversation_id: input.conversationId,
          last_human_decision: null,
          messages: [],
        },
      }),
    );

    const result = await generateDraftReply(db, llmCfg, crmCfg, input);

    expect(result).toEqual({ ok: false, reason: "empty" });
    expect(mockRunModelCall).not.toHaveBeenCalled();
  });

  it("result.text vazio/whitespace → empty", async () => {
    mockLoadAgent.mockResolvedValue(AGENT);
    mockGetLeadContext.mockResolvedValue(contextResult());
    mockRunModelCall.mockResolvedValue({
      result: { text: "   " },
      callId: "call-1",
      provider: "anthropic",
      model: "claude-sonnet-4-6",
      usage: { inputTokens: 1, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 },
      costCents: 1,
      latencyMs: 1,
    } as never);

    const result = await generateDraftReply(db, llmCfg, crmCfg, input);

    expect(result).toEqual({ ok: false, reason: "empty" });
  });
});

describe("o rascunho conhece a decisão do vendedor", () => {
  /** O `system` que efetivamente foi ao modelo — é o que está sob nosso controle. */
  async function systemGerado(
    decisao: { action: string; decision: "approved" | "dismissed"; at: string } | null,
  ): Promise<string> {
    mockLoadAgent.mockResolvedValue(AGENT);
    const base = contextResult() as Extract<LeadContextResult, { ok: true }>;
    mockGetLeadContext.mockResolvedValue({
      ...base,
      context: { ...base.context, last_human_decision: decisao },
    });
    mockRunModelCall.mockResolvedValue({
      result: { text: "Claro! Segue o valor." },
      provider: "anthropic",
      model: "claude-sonnet-4-6",
      usage: { inputTokens: 1, outputTokens: 1, cacheReadTokens: 0, cacheWriteTokens: 0 },
      costCents: 1,
      latencyMs: 1,
    } as never);

    await generateDraftReply(db, llmCfg, crmCfg, input);
    const chamada = mockRunModelCall.mock.calls[0]?.[2] as { system: string };
    return chamada.system;
  }

  it("proposta DESCARTADA entra como instrução de NÃO repropor", async () => {
    // O caso que fecha a promessa da Wave 4: sem isto, o vendedor recusa uma
    // proposta, pede um rascunho, e o rascunho sugere o que ele acabou de negar
    // — escrito COMO ELE, sem disclosure. O botão "Ignorar" viraria mentira.
    const system = await systemGerado({
      action: "ligar para o Carlos amanhã",
      decision: "dismissed",
      at: "2026-07-25T10:00:00Z",
    });
    expect(system).toContain("DESCARTOU");
    expect(system).toContain("ligar para o Carlos amanhã");
    expect(system).toMatch(/NÃO sugira/i);
  });

  it("proposta APROVADA entra como instrução de APOIAR, não de substituir", async () => {
    const system = await systemGerado({
      action: "enviar a proposta revisada",
      decision: "approved",
      at: "2026-07-25T10:00:00Z",
    });
    expect(system).toContain("APROVOU");
    expect(system).toContain("enviar a proposta revisada");
  });

  it("sem decisão nenhuma o prompt NÃO ganha bloco vazio", async () => {
    // Um "[DECISÃO DO VENDEDOR]" sem conteúdo gastaria contexto e ensinaria o
    // modelo a preencher lacuna — pior que não dizer nada.
    const system = await systemGerado(null);
    expect(system).not.toContain("DECISÃO DO VENDEDOR");
    expect(system).toContain("[MODO RASCUNHO]");
  });
});
