/**
 * O defeito: duplicar um mcp_agent pela lista devolvia um agente em branco —
 * a cópia rasa levava só a linha de `ai_agents`, e tudo que define o agente
 * (prompt, ferramentas, credencial, canal, handoff, budgets, follow-up) mora em
 * `ai_agent_versions`.
 */
import { describe, it, expect, vi } from "vitest";

import { duplicateAgentWithVersion } from "./duplicate";
import { agentMcpCreateSchema } from "./validation";

// O módulo `login_codex` DESLIGADO em todo o arquivo, salvo onde um caso o liga:
// os casos com `anthropic` provam que a régua do #2458 não toca os outros.
const modulo = vi.hoisted(() => ({ loginCodex: false }));
vi.mock("@/lib/instalacao/modulos", () => ({
  moduloLigado: vi.fn(async () => modulo.loginCodex),
}));

const ORG = "org-1";
const ACTOR = "user-1";

const AGENTE_MCP = {
  id: "agent-1",
  organization_id: ORG,
  name: "Suporte",
  description: "desc",
  model: "claude-sonnet-4-6",
  system_prompt: "prompt do agente",
  kind: "mcp_agent",
  priority: 10,
  config: {},
  guardrails: null,
  active_kb_version_id: null,
};

const VERSAO_PUBLICADA = {
  id: "version-1",
  organization_id: ORG,
  agent_id: "agent-1",
  version_number: 7,
  system_prompt: "prompt da versao",
  provider: "anthropic",
  model: "claude-sonnet-4-6",
  credential_id: "cred-1",
  tool_ids: ["crm_get_lead", "crm_move_lead_stage", "crm_request_human_handoff"],
  trigger_config: { events: ["message"] },
  channel_session_id: "chan-1",
  max_steps: 12,
  token_budget: 60000,
  cost_budget_cents: 80,
  history_message_window: 30,
  history_token_window: 10000,
  handoff_keywords: ["falar com humano", "atendente"],
  handoff_tool_enabled: true,
  cases_enabled: true,
  split_messages: true,
  split_max_chars: 240,
  followup: { enabled: true, flow_pointer_ids: ["pointer-1"], callback_enabled: false },
  status: "published",
  published_at: "2026-07-31T00:00:00Z",
  superseded_at: null,
  created_at: "2026-07-01T00:00:00Z",
  created_by: "someone",
};

/**
 * Fake mínimo do supabase-js: guarda os inserts para inspeção e IMITA o índice
 * `ai_agents_name_unique`. Sem isso o teste de duplicar duas vezes passaria no
 * fake e bateria no banco de verdade: a colisão é do par (organização, nome), e
 * é ele que este fake recusa com o 23505 que o banco devolve.
 */
function makeDb(opts: {
  agent: unknown;
  draft?: unknown;
  published?: unknown;
  /** Estado inicial do índice: nomes já gravados, por organização. */
  nomes?: Record<string, string[]>;
}) {
  const inserts: Array<{ table: string; row: Record<string, unknown> }> = [];
  const nomes = opts.nomes ?? {};

  function builder(table: string) {
    const state: Record<string, unknown> = {};
    let soNomes = false;
    const api: Record<string, unknown> = {
      select: (cols?: string) => {
        soNomes = cols === "name";
        return api;
      },
      eq: (col: string, val: unknown) => {
        state[col] = val;
        return api;
      },
      order: () => api,
      limit: () => api,
      update: () => api,
      insert: (row: Record<string, unknown>) => {
        inserts.push({ table, row });
        if (table === "ai_agents") {
          const daOrg = (nomes[String(row.organization_id)] ??= []);
          const nome = String(row.name);
          // `unique (organization_id, name)`, simulado.
          if (daOrg.includes(nome)) {
            return {
              select: () => ({
                single: async () => ({
                  data: null,
                  error: {
                    code: "23505",
                    message:
                      'duplicate key value violates unique constraint "ai_agents_name_unique"',
                  },
                }),
              }),
            };
          }
          daOrg.push(nome);
        }
        const id = table === "ai_agents" ? "novo-agente" : "nova-versao";
        return {
          select: () => ({ single: async () => ({ data: { ...row, id }, error: null }) }),
        };
      },
      maybeSingle: async () => {
        if (table === "ai_agents") return { data: opts.agent, error: null };
        if (state.status === "draft") return { data: opts.draft ?? null, error: null };
        if (state.status === "published") return { data: opts.published ?? null, error: null };
        return { data: null, error: null };
      },
      // A consulta de nomes é a única que NÃO termina em `single`/`maybeSingle`.
      then: (resolve: (v: unknown) => unknown) =>
        resolve({
          // Sem o filtro de organização, a leitura devolve os nomes de TODAS —
          // como o service role faria. É o que torna o caso "outra organização"
          // capaz de reprovar a consulta que esquece o `.eq`.
          data: soNomes
            ? (state.organization_id === undefined
                ? Object.values(nomes).flat()
                : (nomes[String(state.organization_id)] ?? [])
              ).map((name) => ({ name }))
            : [],
          error: null,
        }),
    };
    return api;
  }

  return { db: { from: (t: string) => builder(t) } as never, inserts };
}

describe("duplicateAgentWithVersion", () => {
  it("mcp_agent: copia a versão inteira, não só a casca", async () => {
    const { db, inserts } = makeDb({ agent: AGENTE_MCP, published: VERSAO_PUBLICADA });

    const res = await duplicateAgentWithVersion(db, {
      orgId: ORG,
      agentId: "agent-1",
      actorUserId: ACTOR,
      requireVersion: false,
    });

    expect(res.ok).toBe(true);
    const versao = inserts.find((i) => i.table === "ai_agent_versions");
    expect(versao, "a versão precisa ser inserida — sem ela a cópia é uma casca").toBeDefined();

    // O que o usuário perdia na cópia rasa:
    expect(versao!.row.tool_ids).toEqual(VERSAO_PUBLICADA.tool_ids);
    expect(versao!.row.handoff_keywords).toEqual(VERSAO_PUBLICADA.handoff_keywords);
    expect(versao!.row.cases_enabled).toBe(true);
    // Flags por-agente que a tela edita: o teste de drift vigia o SELECT, mas
    // quem escreve a linha nova é `versionPayloadFrom`. Coluna presente no SELECT
    // e ausente do INSERT faz a cópia nascer com o default — o toggle "some".
    expect(versao!.row.split_messages).toBe(true);
    expect(versao!.row.split_max_chars).toBe(240);
    expect(versao!.row.followup).toEqual(VERSAO_PUBLICADA.followup);
    expect((versao!.row.followup as { callback_enabled: boolean }).callback_enabled).toBe(false);
    expect(versao!.row.credential_id).toBe("cred-1");
    expect(versao!.row.channel_session_id).toBe("chan-1");
    expect(versao!.row.system_prompt).toBe("prompt da versao");
  });

  it("a cópia nasce como draft v1 e fora do ar", async () => {
    const { db, inserts } = makeDb({ agent: AGENTE_MCP, published: VERSAO_PUBLICADA });
    await duplicateAgentWithVersion(db, {
      orgId: ORG,
      agentId: "agent-1",
      actorUserId: ACTOR,
      requireVersion: false,
    });

    const versao = inserts.find((i) => i.table === "ai_agent_versions")!;
    expect(versao.row.status).toBe("draft");
    expect(versao.row.version_number).toBe(1);
    // published_at/superseded_at da origem não podem vazar para a cópia
    expect(versao.row.published_at).toBeUndefined();
    expect(versao.row.superseded_at).toBeUndefined();

    const agente = inserts.find((i) => i.table === "ai_agents")!;
    expect(agente.row.published_version_id).toBeUndefined();
    expect(agente.row.is_default).toBe(false);
    expect(agente.row.name).toBe("Suporte (cópia)");
  });

  it("prefere a draft mais recente à published", async () => {
    const draft = {
      ...VERSAO_PUBLICADA,
      id: "version-2",
      status: "draft",
      system_prompt: "rascunho novo",
    };
    const { db, inserts } = makeDb({ agent: AGENTE_MCP, draft, published: VERSAO_PUBLICADA });

    await duplicateAgentWithVersion(db, {
      orgId: ORG,
      agentId: "agent-1",
      actorUserId: ACTOR,
      requireVersion: false,
    });

    const versao = inserts.find((i) => i.table === "ai_agent_versions")!;
    expect(versao.row.system_prompt).toBe("rascunho novo");
  });

  it("rag_bot legado sem versão nasce mcp_agent + v1 draft, não casca", async () => {
    const ragBot = { ...AGENTE_MCP, kind: "rag_bot" };
    const { db, inserts } = makeDb({ agent: ragBot });

    const res = await duplicateAgentWithVersion(db, {
      orgId: ORG,
      agentId: "agent-1",
      actorUserId: ACTOR,
      requireVersion: false,
    });

    expect(res.ok).toBe(true);

    // Antes (#1357): `kind: "rag_bot"` e NENHUMA linha em `ai_agent_versions` —
    // um clone que os dois runtimes resolvem por `published_version_id` não
    // enxergam. Duplicar clonava um mudo.
    const agente = inserts.find((i) => i.table === "ai_agents")!;
    expect(agente.row.kind).toBe("mcp_agent");

    const versao = inserts.find((i) => i.table === "ai_agent_versions");
    expect(
      versao,
      "sem a v1 a cópia é a casca que este módulo existe para não produzir",
    ).toBeDefined();
    expect(versao!.row.status).toBe("draft");
    expect(versao!.row.version_number).toBe(1);
    // A ponte do formato legado: `model` vira provider/modelo e os dois nulos
    // que o corpo legado nunca teve.
    expect(versao!.row.provider).toBe("anthropic");
    expect(versao!.row.model).toBe("claude-sonnet-4-6");
    expect(versao!.row.credential_id).toBeNull();
    expect(versao!.row.channel_session_id).toBeNull();
    expect(versao!.row.system_prompt).toBe("prompt do agente");
    // Não é cópia de versão nenhuma — a origem não tinha.
    if (!res.ok) throw new Error(`duplicacao falhou: ${res.message ?? res.error}`);
    expect(res.sourceVersionId).toBeNull();
  });

  it("rag_bot legado com prompt que a v1 recusa: recusa sem gravar, não lança", async () => {
    // Prompt abaixo do mínimo da versão (10) só existe escrito direto no banco;
    // `mcpAgentDraftRecords` usa `parse`, e sem a guarda a ZodError escapava.
    const ragBot = { ...AGENTE_MCP, kind: "rag_bot", system_prompt: "curto" };
    const { db, inserts } = makeDb({ agent: ragBot });

    const res = await duplicateAgentWithVersion(db, {
      orgId: ORG,
      agentId: "agent-1",
      actorUserId: ACTOR,
      requireVersion: false,
    });

    expect(res).toMatchObject({ ok: false, error: "agent_insert_failed" });
    expect(inserts).toHaveLength(0);
  });

  it("rota da API: mcp_agent sem versão é conflito, não casca", async () => {
    const { db } = makeDb({ agent: AGENTE_MCP });

    const res = await duplicateAgentWithVersion(db, {
      orgId: ORG,
      agentId: "agent-1",
      actorUserId: ACTOR,
      requireVersion: true,
    });

    expect(res).toEqual({ ok: false, error: "no_version_to_duplicate" });
  });
});

describe("duplicar com a assinatura do ChatGPT na origem (#2458)", () => {
  const ORIGEM_ASSINATURA = {
    ...VERSAO_PUBLICADA,
    provider: "openai-assinatura",
    model: "gpt-5.5",
  };
  const pedido = { orgId: ORG, agentId: "agent-1", actorUserId: ACTOR, requireVersion: true };

  it("módulo desligado: recusa com a frase do módulo e não grava nem o agente", async () => {
    modulo.loginCodex = false;
    const { db, inserts } = makeDb({ agent: AGENTE_MCP, published: ORIGEM_ASSINATURA });
    const res = await duplicateAgentWithVersion(db, pedido);
    expect(res).toEqual({
      ok: false,
      error: "provedor_desligado",
      message: expect.stringMatching(/assinatura do ChatGPT está desligada/),
    });
    expect(inserts).toEqual([]);
  });

  it("módulo ligado: copia a assinatura como copia qualquer provedor", async () => {
    modulo.loginCodex = true;
    const { db, inserts } = makeDb({ agent: AGENTE_MCP, published: ORIGEM_ASSINATURA });
    const res = await duplicateAgentWithVersion(db, pedido);
    expect(res.ok).toBe(true);
    expect(inserts.find((i) => i.table === "ai_agent_versions")!.row.provider).toBe(
      "openai-assinatura",
    );
    modulo.loginCodex = false;
  });
});

/**
 * Duplicar o MESMO agente duas vezes (#3978). O nome da cópia era literalmente
 * `${name} (cópia)`: a segunda tentativa gravava o mesmo nome da primeira,
 * batia em `ai_agents_name_unique` e a rota devolvia 500. Aqui o índice é
 * simulado (não há banco de pé no teste) — a segunda duplicação devolve
 * `agent_insert_failed` mesmo código 23505 do banco → 500 na rota.
 */
describe("duplicar o mesmo agente mais de uma vez (#3978)", () => {
  const pedido = { orgId: ORG, agentId: "agent-1", actorUserId: ACTOR, requireVersion: true };

  function nomeInserido(inserts: Array<{ table: string; row: Record<string, unknown> }>): string {
    const agente = inserts.filter((i) => i.table === "ai_agents").at(-1);
    if (!agente) throw new Error("nenhum insert em ai_agents");
    return String(agente.row.name);
  }

  it("(cópia), (cópia 2), (cópia 3) — e o nome do original nunca volta", async () => {
    const { db, inserts } = makeDb({
      agent: AGENTE_MCP,
      published: VERSAO_PUBLICADA,
      // Estado do índice: o original já está gravado.
      nomes: { [ORG]: ["Suporte"] },
    });

    const nomes: string[] = [];
    for (let i = 1; i <= 3; i++) {
      const res = await duplicateAgentWithVersion(db, pedido);
      expect(res.ok, `duplicação ${i} recusada: ${res.ok ? "" : (res.message ?? res.error)}`).toBe(
        true,
      );
      nomes.push(nomeInserido(inserts));
    }

    expect(nomes).toEqual(["Suporte (cópia)", "Suporte (cópia 2)", "Suporte (cópia 3)"]);
    expect(nomes).not.toContain("Suporte");
  });

  it("não pula número: (cópia) livre com (cópia 2) ocupada dá (cópia)", async () => {
    const { db, inserts } = makeDb({
      agent: AGENTE_MCP,
      published: VERSAO_PUBLICADA,
      nomes: { [ORG]: ["Suporte", "Suporte (cópia 2)"] },
    });

    const res = await duplicateAgentWithVersion(db, pedido);
    expect(res.ok).toBe(true);
    expect(nomeInserido(inserts)).toBe("Suporte (cópia)");
  });

  it("nome que já é cópia continua a linhagem: (cópia) vira (cópia 2), não (cópia) (cópia)", async () => {
    const copia = { ...AGENTE_MCP, name: "Suporte (cópia)" };
    const { db, inserts } = makeDb({
      agent: copia,
      published: VERSAO_PUBLICADA,
      nomes: { [ORG]: ["Suporte", "Suporte (cópia)"] },
    });

    const res = await duplicateAgentWithVersion(db, pedido);
    expect(res.ok).toBe(true);
    expect(nomeInserido(inserts)).toBe("Suporte (cópia 2)");
  });

  it("teto de 120 do schema: o sufixo come o fim do nome e o resultado continua válido", async () => {
    const longo = "z".repeat(120);
    const { db, inserts } = makeDb({
      agent: { ...AGENTE_MCP, name: longo },
      published: VERSAO_PUBLICADA,
      nomes: { [ORG]: [longo] },
    });

    const primeira = await duplicateAgentWithVersion(db, pedido);
    expect(primeira.ok).toBe(true);
    const nome1 = nomeInserido(inserts);
    expect(nome1.length).toBe(120);
    expect(nome1.endsWith(" (cópia)")).toBe(true);
    // A MESMA régua com que a tela cria um agente: `agentMcpCreateSchema.name`.
    expect(agentMcpCreateSchema.shape.name.safeParse(nome1).success).toBe(true);

    const segunda = await duplicateAgentWithVersion(db, pedido);
    expect(segunda.ok).toBe(true);
    const nome2 = nomeInserido(inserts);
    expect(nome2).not.toBe(nome1);
    expect(nome2.endsWith(" (cópia 2)")).toBe(true);
    expect(nome2.length).toBeLessThanOrEqual(120);
    expect(agentMcpCreateSchema.shape.name.safeParse(nome2).success).toBe(true);
  });

  it("duas duplicações SIMULTÂNEAS: a que perde a corrida tenta o próximo nome, não 500", async () => {
    const { db, inserts } = makeDb({
      agent: AGENTE_MCP,
      published: VERSAO_PUBLICADA,
      nomes: { [ORG]: ["Suporte"] },
    });

    // As duas leem a lista antes de qualquer uma gravar: escolhem o mesmo nome.
    const [a, b] = await Promise.all([
      duplicateAgentWithVersion(db, pedido),
      duplicateAgentWithVersion(db, pedido),
    ]);

    expect(a.ok && b.ok, `${a.ok ? "" : a.message} ${b.ok ? "" : b.message}`).toBe(true);
    const gravados = inserts.filter((i) => i.table === "ai_agents").map((i) => String(i.row.name));
    // A colisão aconteceu (3 tentativas para 2 cópias) e foi resolvida.
    expect(gravados).toEqual(["Suporte (cópia)", "Suporte (cópia)", "Suporte (cópia 2)"]);
  });

  it("outra organização com o mesmo nome não ocupa o nome desta", async () => {
    const OUTRA = "org-2";
    const { db, inserts } = makeDb({
      agent: AGENTE_MCP,
      published: VERSAO_PUBLICADA,
      nomes: {
        // A outra organização já tem as três primeiras cópias; esta, nenhuma.
        [OUTRA]: ["Suporte", "Suporte (cópia)", "Suporte (cópia 2)"],
        [ORG]: ["Suporte"],
      },
    });

    const res = await duplicateAgentWithVersion(db, pedido);
    expect(res.ok).toBe(true);
    // Se a lista de nomes vazasse entre organizações, sairia "(cópia 3)".
    expect(nomeInserido(inserts)).toBe("Suporte (cópia)");
  });
});
