/**
 * Task 3.1 — REST API dos fluxos de follow-up (CRUD + publish/disable/rollback).
 *
 * Prova contra os Route Handlers REAIS (auth e Supabase mockados, no padrão de
 * tests/unit/leads-bulk-assign.test.ts): TDD rota a rota — draft CRUD → publish
 * inválido 422 (por node) → publish válido → rollback → disable.
 */
import { beforeEach, describe, expect, it, vi } from "vitest";
import { randomUUID } from "node:crypto";
import { NextRequest } from "next/server";

import { requireRole } from "@/lib/auth/require-role";
import { audit } from "@/lib/audit";
import { createClient } from "@/lib/supabase/server";
import { createAdminClient } from "@/lib/supabase/admin";
import { fail } from "@/lib/api/wrappers";
import { ROLE_RANK, type AuthUser, type Role } from "@/lib/auth/types";
import type { FlowGraph, FlowNode, FlowEdge } from "@/lib/followup/graph-schema";

vi.mock("@/lib/auth/require-role", () => ({ requireRole: vi.fn() }));
vi.mock("@/lib/supabase/server", () => ({ createClient: vi.fn() }));
vi.mock("@/lib/supabase/admin", () => ({ createAdminClient: vi.fn() }));
vi.mock("@/lib/audit", () => ({ audit: vi.fn(async () => undefined) }));
// Roteiro de atendimento só publica com o módulo ligado; follow-up não consulta.
vi.mock("@/lib/instalacao/modulos", async (original) => ({
  ...(await original<typeof import("@/lib/instalacao/modulos")>()),
  moduloLigado: vi.fn(async () => true),
}));

const USER_ID = "11111111-1111-4111-8111-111111111111";
const ORG_ID = "22222222-2222-4222-8222-222222222222";
const OTHER_ORG_ID = "22222222-2222-4222-8222-999999999999";

// ---------------------------------------------------------------------------
// Fixture: minimal graphs (mirrors lib/followup/validate-publish.test.ts helpers)
// ---------------------------------------------------------------------------

const pos = { x: 0, y: 0 };
function trigger(id: string): FlowNode {
  return { id, type: "trigger", label: id, position: pos, config: {} };
}
function end(id: string, outcome: "converted" | "exhausted" | "custom" = "exhausted"): FlowNode {
  return { id, type: "end", label: id, position: pos, config: { outcome } };
}
function edge(id: string, source: string, target: string): FlowEdge {
  return { id, source, target, priority: 0, condition: { type: "always" } };
}

/** trigger -> end, always edge: passes flowGraphSchema AND validateFlowForPublish. */
const VALID_GRAPH: FlowGraph = {
  nodes: [trigger("t1"), end("e1")],
  edges: [edge("edge1", "t1", "e1")],
};

/** trigger + orphan end (unreachable from nothing traversing to it) -> no_end_path. */
const INVALID_GRAPH: FlowGraph = {
  nodes: [trigger("t1"), end("e1"), end("orphan")],
  edges: [edge("edge1", "t1", "e1")],
};

// ---------------------------------------------------------------------------
// In-memory fake Supabase client (chainable: select/insert/update/eq/order/
// maybeSingle/single, thenable for implicit awaits after order()).
// ---------------------------------------------------------------------------

type Row = Record<string, unknown>;

function makeDb(pointers: Row[], versions: Row[], stages: Row[] = [], conexoes: Row[] = [], modelos: Row[] = []) {
  const tables: Record<string, Row[]> = {
    followup_flow_pointers: pointers,
    followup_flow_versions: versions,
    followup_enrollments: [],
    // O publish do gatilho de etapa LÊ a etapa antes de deixar publicar (etapa
    // apagada/arquivada = fluxo `active` que nunca matricula ninguém). Sem esta
    // tabela no mock, o caso positivo do `stage_change` não teria como existir.
    crm_stages: stages,
    // O publish lê os providers das conexões: o plano B da IA só é exigido de
    // quem tem canal com janela de 24 h (revisão do #1729).
    channel_sessions: conexoes,
    // O publish confere que o modelo citado existe NESTA org (seguimento do
    // #3056). A linha leva `tabela` para cair na tabela que o motor lê.
    message_templates: modelos.filter((m) => m.tabela === "message_templates"),
    meta_templates: modelos.filter((m) => m.tabela === "meta_templates"),
  };

  function builder(table: string) {
    const filters: Array<[string, unknown]> = [];
    // `neq` (roteiros fora da lista de follow-ups, PR 2 dos fluxos de atendimento).
    const negados: Array<[string, unknown]> = [];
    let orderCol: string | null = null;
    let orderAsc = true;
    let mode: "select" | "insert" | "update" | "delete" = "select";
    let payload: Row | undefined;

    function matches(row: Row): boolean {
      const valor = (k: string) => (k === "surface" ? (row.surface ?? "followup") : row[k]);
      return (
        filters.every(([k, v]) => {
          if (v instanceof Set) return v.has(row[k]);
          if (v === null) return (row[k] ?? null) === null;
          return valor(k) === v;
        }) && negados.every(([k, v]) => valor(k) !== v)
      );
    }

    function execute(): { data: Row[] | null; error: { code?: string; message: string } | null } {
      const tableRows = tables[table]!;
      if (mode === "select") {
        let list = tableRows.filter(matches);
        if (orderCol) {
          const col = orderCol;
          list = [...list].sort((a, b) => {
            const av = String(a[col]);
            const bv = String(b[col]);
            if (av === bv) return 0;
            const cmp = av > bv ? 1 : -1;
            return orderAsc ? cmp : -cmp;
          });
        }
        return { data: list, error: null };
      }
      if (mode === "insert") {
        const row: Row = {
          id: randomUUID(),
          status: "draft",
          draft_graph: null,
          handoff_policy: "pause",
          trigger_config: { kind: "manual" },
          surface: "followup",
          active_version_id: null,
          created_at: new Date().toISOString(),
          updated_at: new Date().toISOString(),
          ...payload,
        };
        if (table === "followup_flow_pointers") {
          const dup = tableRows.some(
            (r) => r.organization_id === row.organization_id && r.name === row.name,
          );
          if (dup) return { data: null, error: { code: "23505", message: "duplicate name" } };
        }
        tableRows.push(row);
        return { data: [row], error: null };
      }
      // update
      const matched = tableRows.filter(matches);
      if (
        table === "followup_flow_pointers" &&
        payload &&
        "name" in payload &&
        matched.length > 0
      ) {
        const self = matched[0]!;
        const dup = tableRows.some(
          (r) =>
            r.organization_id === self.organization_id &&
            r.name === payload!.name &&
            r.id !== self.id,
        );
        if (dup) return { data: null, error: { code: "23505", message: "duplicate name" } };
      }
      if (mode === "delete") {
        const kept = tableRows.filter((r) => !matches(r));
        const removed = tableRows.filter(matches);
        tableRows.length = 0;
        tableRows.push(...kept);
        return { data: removed, error: null };
      }
      for (const row of matched) Object.assign(row, payload);
      return { data: matched, error: null };
    }

    const b = {
      select(_cols?: string) {
        return b;
      },
      insert(obj: Row) {
        mode = "insert";
        payload = obj;
        return b;
      },
      update(obj: Row) {
        mode = "update";
        payload = obj;
        return b;
      },
      delete() {
        mode = "delete";
        return b;
      },
      eq(col: string, val: unknown) {
        filters.push([col, val]);
        return b;
      },
      neq(col: string, val: unknown) {
        negados.push([col, val]);
        return b;
      },
      is(col: string, val: null) {
        filters.push([col, val]);
        return b;
      },
      in(col: string, vals: unknown[]) {
        filters.push([col, new Set(vals)]);
        return b;
      },
      order(col: string, opts?: { ascending?: boolean }) {
        orderCol = col;
        orderAsc = opts?.ascending ?? true;
        return b;
      },
      async maybeSingle() {
        const r = execute();
        if (r.error) return { data: null, error: r.error };
        return { data: r.data && r.data.length > 0 ? r.data[0] : null, error: null };
      },
      async single() {
        const r = execute();
        if (r.error) return { data: null, error: r.error };
        if (!r.data || r.data.length !== 1) {
          return { data: null, error: { message: "expected exactly one row" } };
        }
        return { data: r.data[0], error: null };
      },
      then(onF: (v: ReturnType<typeof execute>) => unknown, onR?: (e: unknown) => unknown) {
        return Promise.resolve(execute()).then(onF, onR);
      },
    };
    return b;
  }

  /** Mirrors fn_publish_followup_flow_version (migration 0055): insert version
   *  (with pointer_id) + activate pointer, atomically, or 'pointer_not_found'. */
  async function rpc(name: string, params: Record<string, unknown>) {
    if (name !== "fn_publish_followup_flow_version") {
      return { data: null, error: { message: `unknown rpc: ${name}` } };
    }
    const pointer = pointers.find((p) => p.id === params.p_pointer);
    if (!pointer || pointer.organization_id !== params.p_org) {
      return { data: null, error: { message: "pointer_not_found" } };
    }
    const versionId = randomUUID();
    versions.push({
      id: versionId,
      organization_id: params.p_org,
      pointer_id: params.p_pointer,
      graph: params.p_graph,
      created_by: params.p_created_by,
      created_at: new Date().toISOString(),
    });
    pointer.active_version_id = versionId;
    pointer.status = "active";
    pointer.updated_at = new Date().toISOString();
    return { data: versionId, error: null };
  }

  return { from: (table: string) => builder(table), rpc };
}

function session(effectiveRole: Role, db: ReturnType<typeof makeDb>, idioma: AuthUser["idioma"] = "pt-BR") {
  const user: AuthUser = {
    id: USER_ID,
    email: "m@example.com",
    full_name: null,
    avatar_url: null,
    is_platform_admin: false,
    idioma,
    organizations: [{ organization_id: ORG_ID, organization_name: "Org", role: effectiveRole }],
  };
  vi.mocked(requireRole).mockImplementation(async (min: Role) => {
    if (ROLE_RANK[effectiveRole] >= ROLE_RANK[min]) {
      return { ok: true, user, org: { orgId: ORG_ID, name: "Org", role: effectiveRole } };
    }
    return { ok: false, response: fail("forbidden_role", `Requer role >= ${min}.`, 403, {}) };
  });
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  vi.mocked(createClient).mockResolvedValue(db as any);
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  vi.mocked(createAdminClient).mockReturnValue(db as any);
}

function req(method: string, body?: Record<string, unknown>) {
  return new NextRequest("http://localhost/api/v1/ai/followup-flows", {
    method,
    ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
  });
}

function ctx(id: string) {
  return { params: Promise.resolve({ id }) };
}

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

// ---------------------------------------------------------------------------
// Draft CRUD
// ---------------------------------------------------------------------------

describe("POST /api/v1/ai/followup-flows — create draft", () => {
  it("agent (< manager) → 403 forbidden_role, sem insert", async () => {
    const db = makeDb([], []);
    session("agent", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/route");
    const res = await POST(req("POST", { name: "Recuperação carrinho" }));
    expect(res.status).toBe(403);
    expect(db.from("followup_flow_pointers")).toBeDefined();
  });

  it("manager → 201, draft com status='draft' e draft_graph null", async () => {
    const db = makeDb([], []);
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/route");
    const res = await POST(req("POST", { name: "Recuperação carrinho" }));
    expect(res.status).toBe(201);
    const body = (await res.json()) as { data: Row };
    expect(body.data.status).toBe("draft");
    expect(body.data.draft_graph).toBeNull();
    expect(vi.mocked(audit)).toHaveBeenCalledWith(
      expect.objectContaining({ action: "followup_flow.created" }),
    );
  });

  it("nome duplicado na mesma org → 409 conflict", async () => {
    const db = makeDb(
      [{ id: randomUUID(), organization_id: ORG_ID, name: "Recuperação carrinho" }],
      [],
    );
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/route");
    const res = await POST(req("POST", { name: "Recuperação carrinho" }));
    expect(res.status).toBe(409);
    const body = (await res.json()) as { error: { code: string } };
    expect(body.error.code).toBe("conflict");
  });

  it("body inválido (name vazio) → 422 validation_failed", async () => {
    const db = makeDb([], []);
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/route");
    const res = await POST(req("POST", { name: "" }));
    expect(res.status).toBe(422);
  });
});

describe("GET /api/v1/ai/followup-flows — list", () => {
  it("viewer (any member) → 200, só pointers da própria org", async () => {
    const db = makeDb(
      [
        { id: "33333333-3333-4333-8333-333333333333", organization_id: ORG_ID, name: "A", status: "draft", active_version_id: null, handoff_policy: "pause", updated_at: "2026-01-01" },
        { id: "44444444-4444-4444-8444-444444444444", organization_id: OTHER_ORG_ID, name: "B", status: "draft", active_version_id: null, handoff_policy: "pause", updated_at: "2026-01-01" },
      ],
      [],
    );
    session("viewer", db);
    const { GET } = await import("@/app/api/v1/ai/followup-flows/route");
    const res = await GET(req("GET"));
    expect(res.status).toBe(200);
    const body = (await res.json()) as { data: Row[] };
    expect(body.data).toHaveLength(1);
    expect(body.data[0]!.id).toBe("33333333-3333-4333-8333-333333333333");
  });
});

describe("GET /api/v1/ai/followup-flows/:id", () => {
  it("pointer de outra org → 404 not_found", async () => {
    const db = makeDb(
      [{ id: "33333333-3333-4333-8333-333333333333", organization_id: OTHER_ORG_ID, name: "A" }],
      [],
    );
    session("viewer", db);
    const { GET } = await import("@/app/api/v1/ai/followup-flows/[id]/route");
    const res = await GET(req("GET"), ctx("33333333-3333-4333-8333-333333333333"));
    expect(res.status).toBe(404);
  });

  it("pointer da própria org → 200 com draft_graph/trigger_config/handoff_policy", async () => {
    const db = makeDb(
      [
        {
          id: "33333333-3333-4333-8333-333333333333",
          organization_id: ORG_ID,
          name: "A",
          status: "draft",
          draft_graph: null,
          handoff_policy: "pause",
          trigger_config: { kind: "manual" },
          active_version_id: null,
        },
      ],
      [],
    );
    session("viewer", db);
    const { GET } = await import("@/app/api/v1/ai/followup-flows/[id]/route");
    const res = await GET(req("GET"), ctx("33333333-3333-4333-8333-333333333333"));
    expect(res.status).toBe(200);
    const body = (await res.json()) as { data: Row };
    expect(body.data.trigger_config).toEqual({ kind: "manual" });
  });
});

describe("PATCH /api/v1/ai/followup-flows/:id", () => {
  function pointerRow(overrides: Row = {}): Row {
    return {
      id: "33333333-3333-4333-8333-333333333333",
      organization_id: ORG_ID,
      name: "A",
      status: "draft",
      draft_graph: null,
      handoff_policy: "pause",
      trigger_config: { kind: "manual" },
      active_version_id: null,
      ...overrides,
    };
  }

  it("agent (< manager) → 403", async () => {
    const db = makeDb([pointerRow()], []);
    session("agent", db);
    const { PATCH } = await import("@/app/api/v1/ai/followup-flows/[id]/route");
    const res = await PATCH(req("PATCH", { name: "B" }), ctx("33333333-3333-4333-8333-333333333333"));
    expect(res.status).toBe(403);
  });

  it("draft_graph com shape inválido (nó desconhecido) → 422 validation_failed", async () => {
    const db = makeDb([pointerRow()], []);
    session("manager", db);
    const { PATCH } = await import("@/app/api/v1/ai/followup-flows/[id]/route");
    const res = await PATCH(
      req("PATCH", { draft_graph: { nodes: [{ id: "x", type: "bogus" }], edges: [] } }),
      ctx("33333333-3333-4333-8333-333333333333"),
    );
    expect(res.status).toBe(422);
  });

  it("draft_graph com shape válido + trigger_config + handoff_policy → 200, persiste tudo", async () => {
    const db = makeDb([pointerRow()], []);
    session("manager", db);
    const { PATCH } = await import("@/app/api/v1/ai/followup-flows/[id]/route");
    const res = await PATCH(
      req("PATCH", {
        draft_graph: VALID_GRAPH,
        handoff_policy: "cancel",
        trigger_config: { kind: "silence", params: { threshold_minutes: 30 } },
      }),
      ctx("33333333-3333-4333-8333-333333333333"),
    );
    expect(res.status).toBe(200);
    const body = (await res.json()) as { data: Row };
    expect(body.data.draft_graph).toEqual(VALID_GRAPH);
    expect(body.data.handoff_policy).toBe("cancel");
    expect(vi.mocked(audit)).toHaveBeenCalledWith(
      expect.objectContaining({ action: "followup_flow.updated" }),
    );
  });

  it("pointer inexistente na org → 404", async () => {
    const db = makeDb([], []);
    session("manager", db);
    const { PATCH } = await import("@/app/api/v1/ai/followup-flows/[id]/route");
    const res = await PATCH(req("PATCH", { name: "B" }), ctx("55555555-5555-4555-8555-555555555555"));
    expect(res.status).toBe(404);
  });

  it("body {} (nada a mudar) → 200, recarrega o pointer da própria org (reload filtra organization_id)", async () => {
    const db = makeDb([pointerRow({ name: "nome-original" })], []);
    session("manager", db);
    const { PATCH } = await import("@/app/api/v1/ai/followup-flows/[id]/route");
    const res = await PATCH(req("PATCH", {}), ctx("33333333-3333-4333-8333-333333333333"));
    expect(res.status).toBe(200);
    const body = (await res.json()) as { data: Row };
    expect(body.data.name).toBe("nome-original");
    expect(vi.mocked(audit)).not.toHaveBeenCalled();
  });

  it("renomeia → 200 com o nome novo", async () => {
    const db = makeDb([pointerRow({ name: "Antigo" })], []);
    session("manager", db);
    const { PATCH } = await import("@/app/api/v1/ai/followup-flows/[id]/route");
    const res = await PATCH(
      req("PATCH", { name: "Novo nome" }),
      ctx("33333333-3333-4333-8333-333333333333"),
    );
    expect(res.status).toBe(200);
    const body = (await res.json()) as { data: Row };
    expect(body.data.name).toBe("Novo nome");
  });

  it("nome já usado na mesma org → 409 conflict", async () => {
    const db = makeDb(
      [
        pointerRow({ name: "A" }),
        pointerRow({
          id: "44444444-4444-4444-8444-444444444444",
          name: "B",
        }),
      ],
      [],
    );
    session("manager", db);
    const { PATCH } = await import("@/app/api/v1/ai/followup-flows/[id]/route");
    const res = await PATCH(
      req("PATCH", { name: "B" }),
      ctx("33333333-3333-4333-8333-333333333333"),
    );
    expect(res.status).toBe(409);
    const body = (await res.json()) as { error: { code: string } };
    expect(body.error.code).toBe("conflict");
  });
});

// ---------------------------------------------------------------------------
// Publish
// ---------------------------------------------------------------------------

describe("POST /api/v1/ai/followup-flows/:id/publish", () => {
  it("draft_graph null → 422 validation_failed com details.errors", async () => {
    const db = makeDb(
      [{ id: "33333333-3333-4333-8333-333333333333", organization_id: ORG_ID, status: "draft", draft_graph: null }],
      [],
    );
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/publish/route");
    const res = await POST(req("POST"), ctx("33333333-3333-4333-8333-333333333333"));
    expect(res.status).toBe(422);
    const body = (await res.json()) as { error: { code: string; details: { errors: unknown[] } } };
    expect(body.error.code).toBe("validation_failed");
    expect(body.error.details.errors.length).toBeGreaterThan(0);
  });

  it("draft_graph inválido (nó órfão) → 422 com erro por node", async () => {
    const db = makeDb(
      [{ id: "33333333-3333-4333-8333-333333333333", organization_id: ORG_ID, status: "draft", draft_graph: INVALID_GRAPH }],
      [],
    );
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/publish/route");
    const res = await POST(req("POST"), ctx("33333333-3333-4333-8333-333333333333"));
    expect(res.status).toBe(422);
    const body = (await res.json()) as {
      error: { details: { errors: Array<{ code: string; node_id: string | null }> } };
    };
    expect(body.error.details.errors).toEqual([
      { node_id: "orphan", code: "unreachable_node", message: expect.any(String) },
    ]);
  });

  describe("plano B da IA depois de 24 h de espera (revisão do #1729)", () => {
    const PID = "33333333-3333-4333-8333-333333333333";
    const ESPERA_LONGA: FlowGraph = {
      nodes: [
        trigger("t1"),
        { id: "w1", type: "wait", label: "w1", position: pos, config: { mode: "fixed", duration_ms: 90_000_000 } },
        { id: "a1", type: "action", label: "a1", position: pos, config: { mode: "ai_message", prompt_hint: "oi" } },
        end("e1"),
      ],
      edges: [edge("x1", "t1", "w1"), edge("x2", "w1", "a1"), edge("x3", "a1", "e1")],
    };
    const ponteiro = () => [{ id: PID, organization_id: ORG_ID, status: "draft", draft_graph: ESPERA_LONGA }];

    it("organização só com canal sem janela publica sem plano B", async () => {
      const db = makeDb(ponteiro(), [], [], [
        { organization_id: ORG_ID, provider: "waha", archived_at: null },
        // Arquivada e de OUTRA organização: nenhuma das duas pode pesar.
        { organization_id: ORG_ID, provider: "meta_cloud", archived_at: "2026-09-01T00:00:00Z" },
        { organization_id: OTHER_ORG_ID, provider: "meta_cloud", archived_at: null },
      ]);
      session("manager", db);
      const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/publish/route");
      const res = await POST(req("POST"), ctx(PID));
      expect(res.status).toBe(200);
    });

    it("organização com canal de janela de 24 h continua exigindo o plano B", async () => {
      const db = makeDb(ponteiro(), [], [], [
        { organization_id: ORG_ID, provider: "waha", archived_at: null },
        { organization_id: ORG_ID, provider: "meta_cloud", archived_at: null },
      ]);
      session("manager", db);
      const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/publish/route");
      const res = await POST(req("POST"), ctx(PID));
      expect(res.status).toBe(422);
      const body = (await res.json()) as { error: { details: { errors: Array<{ code: string }> } } };
      expect(body.error.details.errors.map((e) => e.code)).toEqual(["long_wait_needs_template"]);
    });
  });

  describe("modelo de mensagem que não existe nesta conta (seguimento do #3056)", () => {
    const PID = "33333333-3333-4333-8333-333333333333";
    const MODELO = "55555555-5555-4555-8555-555555555555";
    const comPassos = (...acoes: FlowNode[]): FlowGraph => {
      const ids = ["t1", ...acoes.map((a) => a.id), "e1"];
      return {
        nodes: [trigger("t1"), ...acoes, end("e1")],
        edges: ids.slice(1).map((alvo, i) => edge(`x${i}`, ids[i]!, alvo)),
      };
    };
    const passoDeModelo: FlowNode = {
      id: "m1", type: "action", label: "m1", position: pos, config: { mode: "template", template_id: MODELO },
    };
    const iaComPlanoB = (planoB = MODELO): FlowNode => ({
      id: "ia1", type: "action", label: "ia1", position: pos,
      config: { mode: "ai_message", prompt_hint: "oi", fallback_template_id: planoB },
    });
    const ponteiro = (g: FlowGraph) => [{ id: PID, organization_id: ORG_ID, status: "draft", draft_graph: g }];

    it("modelo de OUTRA org → 422 modelo_inexistente, com a frase no idioma de quem publica", async () => {
      const db = makeDb(ponteiro(comPassos(passoDeModelo)), [], [], [], [
        { tabela: "message_templates", id: MODELO, organization_id: OTHER_ORG_ID },
      ]);
      session("manager", db, "es");
      const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/publish/route");
      const res = await POST(req("POST"), ctx(PID));
      expect(res.status).toBe(422);
      const body = (await res.json()) as { error: { details: { errors: Array<{ code: string; message: string }> } } };
      expect(body.error.details.errors).toEqual([
        {
          node_id: "m1",
          code: "modelo_inexistente",
          message: "Elige una plantilla de mensaje: la que estaba aquí no existe en esta cuenta.",
        },
      ]);
    });

    it("plano B apontando para texto pronto → 422: o motor só lê o plano B em meta_templates", async () => {
      const db = makeDb(ponteiro(comPassos(iaComPlanoB())), [], [], [], [
        { tabela: "message_templates", id: MODELO, organization_id: ORG_ID },
      ]);
      session("manager", db);
      const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/publish/route");
      const res = await POST(req("POST"), ctx(PID));
      expect(res.status).toBe(422);
      const body = (await res.json()) as { error: { details: { errors: Array<{ code: string }> } } };
      expect(body.error.details.errors.map((e) => e.code)).toEqual(["modelo_inexistente"]);
    });

    it("modelo da própria org publica — texto pronto no passo, modelo do canal no plano B", async () => {
      const DO_CANAL = "66666666-6666-4666-8666-666666666666";
      const db = makeDb(
        ponteiro(comPassos(passoDeModelo, iaComPlanoB(DO_CANAL))),
        [], [], [],
        [
          { tabela: "message_templates", id: MODELO, organization_id: ORG_ID },
          { tabela: "meta_templates", id: DO_CANAL, organization_id: ORG_ID },
        ],
      );
      session("manager", db);
      const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/publish/route");
      const res = await POST(req("POST"), ctx(PID));
      expect(res.status).toBe(200);
    });
  });

  it("draft_graph válido → cria version, pointer vira active com active_version_id", async () => {
    const db = makeDb(
      [{ id: "33333333-3333-4333-8333-333333333333", organization_id: ORG_ID, status: "draft", draft_graph: VALID_GRAPH }],
      [],
    );
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/publish/route");
    const res = await POST(req("POST"), ctx("33333333-3333-4333-8333-333333333333"));
    expect(res.status).toBe(200);
    const body = (await res.json()) as { data: { status: string; active_version_id: string } };
    expect(body.data.status).toBe("active");
    expect(body.data.active_version_id).toBeTruthy();

    const { data: versionRows } = (await db
      .from("followup_flow_versions")
      .select()
      .eq("id", body.data.active_version_id)) as { data: Row[] };
    expect(versionRows.length).toBe(1);
    expect(versionRows[0]!.graph).toEqual(VALID_GRAPH);

    expect(vi.mocked(audit)).toHaveBeenCalledWith(
      expect.objectContaining({ action: "followup_flow.published" }),
    );
  });

  it("pointer de outra org → 404", async () => {
    const db = makeDb(
      [{ id: "33333333-3333-4333-8333-333333333333", organization_id: OTHER_ORG_ID, status: "draft", draft_graph: VALID_GRAPH }],
      [],
    );
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/publish/route");
    const res = await POST(req("POST"), ctx("33333333-3333-4333-8333-333333333333"));
    expect(res.status).toBe(404);
  });

  /**
   * O CASO POSITIVO DO KIND QUE GANHOU MOTOR.
   *
   * Aqui havia uma cópia do caso de `conversation_end` que já existe logo
   * abaixo — eu a criei ao repontar um caso obsoleto (ele afirmava que
   * `stage_change` era recusado, e a wave do gatilho de etapa entregou o
   * consumidor de `lead.stage_changed`). Duplicata medida por
   * `@MaestroConexoes`, e a proposta dele é melhor que o repontamento: a
   * propriedade "kind sem motor não publica" já está guardada no caso de baixo,
   * e o que FALTAVA era a guarda do caminho novo.
   *
   * Sem ela, publicar com `stage_change` só era coberto por
   * `tests/e2e/gatilho-de-etapa.spec.ts` — e o `e2e` não segura merge neste
   * repo, então uma regressão passaria pelos checks obrigatórios.
   */
  it("trigger_config.kind='stage_change' com etapa válida → publica (kind com motor)", async () => {
    const STAGE_ID = "44444444-4444-4444-8444-444444444444";
    const db = makeDb(
      [
        {
          id: "33333333-3333-4333-8333-333333333333",
          organization_id: ORG_ID,
          status: "draft",
          draft_graph: VALID_GRAPH,
          trigger_config: { kind: "stage_change", params: { stage_id: STAGE_ID } },
        },
      ],
      [],
      [{ id: STAGE_ID, organization_id: ORG_ID, name: "Proposta enviada", is_archived: false }],
    );
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/publish/route");
    const res = await POST(req("POST"), ctx("33333333-3333-4333-8333-333333333333"));
    expect(res.status).toBe(200);

    const { data: pointerRows } = (await db
      .from("followup_flow_pointers")
      .select()
      .eq("id", "33333333-3333-4333-8333-333333333333")) as { data: Row[] };
    expect(pointerRows[0]!.status).toBe("active");
  });

  /**
   * A regra de etapa do nó de condição guarda o `stage_id`, e só o banco sabe se
   * ele existe. Antes do seletor, a tela aceitava o NOME digitado ("PAGO"), que o
   * motor nunca casa — e o fluxo publicava com a regra morta. A rota lê as etapas
   * citadas FILTRANDO a organização: id de etapa de outra org é etapa que não existe.
   */
  describe("regra de etapa no nó de condição", () => {
    const STAGE_ID = "55555555-5555-4555-8555-555555555555";
    const POINTER_ID = "33333333-3333-4333-8333-333333333333";
    const comRegraDeEtapa = (valor: string): FlowGraph => ({
      nodes: [
        trigger("t1"),
        {
          id: "c1",
          type: "condition",
          label: "c1",
          position: pos,
          config: { combinator: "and", checks: [{ field: "lead_stage", op: "eq", value: valor }] },
        },
        end("e1"),
      ],
      edges: [
        edge("edge1", "t1", "c1"),
        { id: "edge2", source: "c1", target: "e1", priority: 0, condition: { type: "cond_result", value: true } },
        { id: "edge3", source: "c1", target: "e1", priority: 0, condition: { type: "cond_result", value: false } },
      ],
    });
    const publicar = async (valor: string, stages: Row[]) => {
      const db = makeDb(
        [{ id: POINTER_ID, organization_id: ORG_ID, status: "draft", draft_graph: comRegraDeEtapa(valor) }],
        [],
        stages,
      );
      session("manager", db);
      const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/publish/route");
      return POST(req("POST"), ctx(POINTER_ID));
    };
    const codigos = async (res: Response) =>
      ((await res.json()) as { error: { details: { errors: Array<{ code: string }> } } }).error.details.errors.map(
        (e) => e.code,
      );

    it("etapa ativa da organização → publica", async () => {
      const res = await publicar(STAGE_ID, [
        { id: STAGE_ID, organization_id: ORG_ID, name: "Pago", is_archived: false, crm_pipelines: { name: "Vendas" } },
      ]);
      expect(res.status).toBe(200);
    });

    it("nome digitado à mão (fluxo antigo) → 422 check_stage_not_found", async () => {
      const res = await publicar("PAGO", []);
      expect(res.status).toBe(422);
      expect(await codigos(res)).toEqual(["check_stage_not_found"]);
    });

    it("etapa de OUTRA organização → 422, igual a etapa que não existe", async () => {
      const res = await publicar(STAGE_ID, [
        { id: STAGE_ID, organization_id: OTHER_ORG_ID, name: "Pago", is_archived: false, crm_pipelines: { name: "Vendas" } },
      ]);
      expect(res.status).toBe(422);
      expect(await codigos(res)).toEqual(["check_stage_not_found"]);
    });

    it("etapa arquivada → 422 check_stage_archived", async () => {
      const res = await publicar(STAGE_ID, [
        { id: STAGE_ID, organization_id: ORG_ID, name: "Pago", is_archived: true, crm_pipelines: { name: "Vendas" } },
      ]);
      expect(res.status).toBe(422);
      expect(await codigos(res)).toEqual(["check_stage_archived"]);
    });
  });

  it("kind conhecido mas SEM motor ('conversation_end') → 422 trigger_kind_not_implemented", async () => {
    const db = makeDb(
      [
        {
          id: "33333333-3333-4333-8333-333333333333",
          organization_id: ORG_ID,
          status: "draft",
          draft_graph: VALID_GRAPH,
          trigger_config: { kind: "conversation_end", params: {} },
        },
      ],
      [],
    );
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/publish/route");
    const res = await POST(req("POST"), ctx("33333333-3333-4333-8333-333333333333"));
    expect(res.status).toBe(422);
    const body = (await res.json()) as { error: { code: string } };
    expect(body.error.code).toBe("trigger_kind_not_implemented");
  });

  /**
   * ⚠️ ESTE CASO É A DIFERENÇA ENTRE DENYLIST E ALLOWLIST, e é o que a versão
   * anterior deixava passar. Ela recusava UM literal (`conversation_end`); um
   * kind que ela nunca tinha visto publicava com 200 e ficava `active` sem
   * enrollar ninguém — fluxo morto com cara de vivo.
   *
   * O caminho não é teórico: `trigger_config` é `jsonb` sem CHECK e o publish lê
   * a linha CRUA (não passa pelo Zod do PATCH), então SQL à mão, clone
   * open-source ou versão futura do produto chegam aqui com qualquer coisa.
   */
  it("kind DESCONHECIDO pelo produto → 422 (a denylist antiga publicaria)", async () => {
    const db = makeDb(
      [
        {
          id: "33333333-3333-4333-8333-333333333333",
          organization_id: ORG_ID,
          status: "draft",
          draft_graph: VALID_GRAPH,
          trigger_config: { kind: "kind_que_nunca_existiu", params: {} },
        },
      ],
      [],
    );
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/publish/route");
    const res = await POST(req("POST"), ctx("33333333-3333-4333-8333-333333333333"));
    expect(res.status, "kind sem motor não pode publicar, mesmo sendo desconhecido").toBe(422);
    const body = (await res.json()) as { error: { code: string; message: string } };
    expect(body.error.code).toBe("trigger_kind_not_implemented");
    // A mensagem nomeia o kind recusado: erro que não diz O QUÊ manda o operador
    // adivinhar, e aqui quem lê pode ser um self-hoster com dado legado.
    expect(body.error.message).toContain("kind_que_nunca_existiu");
  });

  it("trigger_config.kind='silence' → publica normalmente (kind com motor)", async () => {
    const db = makeDb(
      [
        {
          id: "33333333-3333-4333-8333-333333333333",
          organization_id: ORG_ID,
          status: "draft",
          draft_graph: VALID_GRAPH,
          trigger_config: { kind: "silence", params: { threshold_minutes: 30 } },
        },
      ],
      [],
    );
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/publish/route");
    const res = await POST(req("POST"), ctx("33333333-3333-4333-8333-333333333333"));
    expect(res.status).toBe(200);
  });

  it("appointment_no_show com consumidor ativo publica versão e mantém gatilhos desconhecidos recusados",async()=>{
    const id="33333333-3333-4333-8333-333333333333";
    const db=makeDb([{id,organization_id:ORG_ID,status:"draft",draft_graph:VALID_GRAPH,trigger_config:{kind:"appointment_no_show"}}],[]);
    session("manager",db);
    const {POST}=await import("@/app/api/v1/ai/followup-flows/[id]/publish/route");
    expect((await POST(req("POST"),ctx(id))).status).toBe(200);
    const {data}=(await db.from("followup_flow_pointers").select().eq("id",id)) as {data:Row[]};
    expect(data[0]).toMatchObject({status:"active"});expect(data[0]?.active_version_id).toBeTruthy();
  });

  it("trigger_config.kind='manual' → publica normalmente", async () => {
    const db = makeDb(
      [
        {
          id: "33333333-3333-4333-8333-333333333333",
          organization_id: ORG_ID,
          status: "draft",
          draft_graph: VALID_GRAPH,
          trigger_config: { kind: "manual" },
        },
      ],
      [],
    );
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/publish/route");
    const res = await POST(req("POST"), ctx("33333333-3333-4333-8333-333333333333"));
    expect(res.status).toBe(200);
  });
});

describe("POST /api/v1/ai/followup-flows/:id/publish — roteiro que encadeia (revisão do #1573)", () => {
  const A = "44444444-4444-4444-8444-44444444444a";
  const B = "44444444-4444-4444-8444-44444444444b";
  const VB = "44444444-4444-4444-8444-4444444444b1";
  const roteiroQueVaiPara = (fluxo: string | null): FlowGraph => ({
    nodes: [
      trigger("t"),
      {
        id: "c",
        type: "collect",
        label: "Nome",
        position: pos,
        config: { key: "nome", label: "Nome", type: "text", required: true, permite_correcao: true },
      },
      {
        id: "f",
        type: "end",
        label: "f",
        position: pos,
        config: { outcome: "converted", ...(fluxo ? { ao_finalizar: { tipo: "proximo_fluxo" as const, fluxo } } : {}) },
      },
    ],
    edges: [edge("e1", "t", "c"), edge("e2", "c", "f")],
  });
  const cenario = (bVaiPara: string | null) =>
    makeDb(
      [
        { id: A, organization_id: ORG_ID, name: "Cadastro", status: "draft", surface: "atendimento", draft_graph: roteiroQueVaiPara(B), trigger_config: { kind: "manual" } },
        { id: B, organization_id: ORG_ID, name: "Financiamento", status: "active", surface: "atendimento", active_version_id: VB, trigger_config: { kind: "manual" } },
      ],
      [{ id: VB, organization_id: ORG_ID, pointer_id: B, graph: roteiroQueVaiPara(bVaiPara) }],
    );

  it("⭐ A → B com B → A publicado: 422 roteiro_em_ciclo, nada publicado", async () => {
    const db = cenario(A);
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/publish/route");
    const res = await POST(req("POST"), ctx(A));
    expect(res.status).toBe(422);
    const body = (await res.json()) as { error: { details: { errors: Array<{ code: string; message: string }> } } };
    expect(body.error.details.errors.map((e) => e.code)).toEqual(["roteiro_em_ciclo"]);
    expect(body.error.details.errors[0]!.message).toContain("Financiamento");
    const { data } = (await db.from("followup_flow_pointers").select().eq("id", A)) as { data: Row[] };
    expect(data[0]).toMatchObject({ status: "draft" });
  });

  it("A → B com B terminando: publica", async () => {
    const db = cenario(null);
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/publish/route");
    expect((await POST(req("POST"), ctx(A))).status).toBe(200);
  });
});

// ---------------------------------------------------------------------------
// Rollback
// ---------------------------------------------------------------------------

describe("POST /api/v1/ai/followup-flows/:id/rollback", () => {
  const P1 = "33333333-3333-4333-8333-333333333333";
  const OTHER_POINTER = "99999999-9999-4999-8999-999999999999";

  it("version_id de outra org → 404 not_found", async () => {
    const db = makeDb(
      [{ id: P1, organization_id: ORG_ID, status: "active", active_version_id: "66666666-6666-4666-8666-666666666666" }],
      [{ id: "88888888-8888-4888-8888-888888888888", organization_id: OTHER_ORG_ID, pointer_id: P1, graph: VALID_GRAPH }],
    );
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/rollback/route");
    const res = await POST(req("POST", { version_id: "88888888-8888-4888-8888-888888888888" }), ctx(P1));
    expect(res.status).toBe(404);
  });

  it("version da mesma org mas de OUTRO pointer (linhagem errada) → 404 not_found", async () => {
    const db = makeDb(
      [{ id: P1, organization_id: ORG_ID, status: "active", active_version_id: "66666666-6666-4666-8666-666666666666" }],
      [{ id: "77777777-7777-4777-8777-777777777777", organization_id: ORG_ID, pointer_id: OTHER_POINTER, graph: VALID_GRAPH }],
    );
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/rollback/route");
    const res = await POST(req("POST", { version_id: "77777777-7777-4777-8777-777777777777" }), ctx(P1));
    expect(res.status).toBe(404);
  });

  it("version órfã (pointer_id null) → 404 not_found, nunca é alvo de rollback", async () => {
    const db = makeDb(
      [{ id: P1, organization_id: ORG_ID, status: "active", active_version_id: "66666666-6666-4666-8666-666666666666" }],
      [{ id: "77777777-7777-4777-8777-777777777777", organization_id: ORG_ID, pointer_id: null, graph: VALID_GRAPH }],
    );
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/rollback/route");
    const res = await POST(req("POST", { version_id: "77777777-7777-4777-8777-777777777777" }), ctx(P1));
    expect(res.status).toBe(404);
  });

  it("version_id válido e da linhagem do pointer → 200, pointer aponta pra ela", async () => {
    const db = makeDb(
      [{ id: P1, organization_id: ORG_ID, status: "active", active_version_id: "66666666-6666-4666-8666-666666666666" }],
      [
        { id: "66666666-6666-4666-8666-666666666666", organization_id: ORG_ID, pointer_id: P1, graph: VALID_GRAPH },
        { id: "77777777-7777-4777-8777-777777777777", organization_id: ORG_ID, pointer_id: P1, graph: VALID_GRAPH },
      ],
    );
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/rollback/route");
    const res = await POST(req("POST", { version_id: "77777777-7777-4777-8777-777777777777" }), ctx(P1));
    expect(res.status).toBe(200);
    const body = (await res.json()) as { data: { active_version_id: string; status: string } };
    expect(body.data.active_version_id).toBe("77777777-7777-4777-8777-777777777777");
    expect(body.data.status).toBe("active");
    expect(vi.mocked(audit)).toHaveBeenCalledWith(
      expect.objectContaining({ action: "followup_flow.rolled_back" }),
    );
  });

  it("pointer 'disabled' → rollback só troca active_version_id, NÃO reativa (status continua disabled)", async () => {
    const db = makeDb(
      [{ id: P1, organization_id: ORG_ID, status: "disabled", active_version_id: "66666666-6666-4666-8666-666666666666" }],
      [
        { id: "66666666-6666-4666-8666-666666666666", organization_id: ORG_ID, pointer_id: P1, graph: VALID_GRAPH },
        { id: "77777777-7777-4777-8777-777777777777", organization_id: ORG_ID, pointer_id: P1, graph: VALID_GRAPH },
      ],
    );
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/rollback/route");
    const res = await POST(req("POST", { version_id: "77777777-7777-4777-8777-777777777777" }), ctx(P1));
    expect(res.status).toBe(200);
    const body = (await res.json()) as { data: { active_version_id: string; status: string } };
    expect(body.data.active_version_id).toBe("77777777-7777-4777-8777-777777777777");
    expect(body.data.status).toBe("disabled");
  });
});

// ---------------------------------------------------------------------------
// Disable
// ---------------------------------------------------------------------------

describe("POST /api/v1/ai/followup-flows/:id/disable", () => {
  it("pointer ativo → 200, status='disabled', audit emitido", async () => {
    const db = makeDb([{ id: "33333333-3333-4333-8333-333333333333", organization_id: ORG_ID, status: "active" }], []);
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/disable/route");
    const res = await POST(req("POST"), ctx("33333333-3333-4333-8333-333333333333"));
    expect(res.status).toBe(200);
    const body = (await res.json()) as { data: { status: string } };
    expect(body.data.status).toBe("disabled");
    expect(vi.mocked(audit)).toHaveBeenCalledWith(
      expect.objectContaining({ action: "followup_flow.disabled" }),
    );
  });

  it("pointer já disabled → 200 no-op, sem novo audit", async () => {
    const db = makeDb([{ id: "33333333-3333-4333-8333-333333333333", organization_id: ORG_ID, status: "disabled" }], []);
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/disable/route");
    const res = await POST(req("POST"), ctx("33333333-3333-4333-8333-333333333333"));
    expect(res.status).toBe(200);
    expect(vi.mocked(audit)).not.toHaveBeenCalled();
  });

  it("pointer inexistente na org → 404", async () => {
    const db = makeDb([], []);
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/disable/route");
    const res = await POST(req("POST"), ctx("55555555-5555-4555-8555-555555555555"));
    expect(res.status).toBe(404);
  });
});

describe("DELETE /api/v1/ai/followup-flows/:id", () => {
  const P1 = "33333333-3333-4333-8333-333333333333";

  it("manager → 200, pointer some, audit emitido", async () => {
    const db = makeDb([{ id: P1, organization_id: ORG_ID, status: "disabled" }], []);
    session("manager", db);
    const { DELETE } = await import("@/app/api/v1/ai/followup-flows/[id]/route");
    const res = await DELETE(req("DELETE"), ctx(P1));
    expect(res.status).toBe(200);
    const body = (await res.json()) as { data: { id: string } };
    expect(body.data.id).toBe(P1);
    expect(vi.mocked(audit)).toHaveBeenCalledWith(
      expect.objectContaining({ action: "followup_flow.deleted" }),
    );
  });

  it("pointer inexistente na org → 404", async () => {
    const db = makeDb([], []);
    session("manager", db);
    const { DELETE } = await import("@/app/api/v1/ai/followup-flows/[id]/route");
    const res = await DELETE(req("DELETE"), ctx("55555555-5555-4555-8555-555555555555"));
    expect(res.status).toBe(404);
  });

  it("agent (< manager) → 403", async () => {
    const db = makeDb([{ id: P1, organization_id: ORG_ID, status: "draft" }], []);
    session("agent", db);
    const { DELETE } = await import("@/app/api/v1/ai/followup-flows/[id]/route");
    const res = await DELETE(req("DELETE"), ctx(P1));
    expect(res.status).toBe(403);
  });
});

// ---------------------------------------------------------------------------
// Duplicate
// ---------------------------------------------------------------------------

describe("POST /api/v1/ai/followup-flows/:id/duplicate", () => {
  const P1 = "33333333-3333-4333-8333-333333333333";
  const P2 = "44444444-4444-4444-8444-444444444444";
  const VID = "66666666-6666-4666-8666-666666666666";

  function origem(overrides: Row = {}): Row {
    return {
      id: P1,
      organization_id: ORG_ID,
      name: "Carrinho",
      status: "active",
      draft_graph: VALID_GRAPH,
      trigger_config: { kind: "silence", params: { threshold_minutes: 30 } },
      handoff_policy: "cancel",
      surface: "followup",
      active_version_id: VID,
      ...overrides,
    };
  }

  it("agent (< manager) → 403, sem insert", async () => {
    const db = makeDb([origem()], []);
    session("agent", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/duplicate/route");
    const res = await POST(req("POST"), ctx(P1));
    expect(res.status).toBe(403);
    const { data } = await db.from("followup_flow_pointers").select();
    expect(data).toHaveLength(1);
  });

  it("pointer de outra org → 404", async () => {
    const db = makeDb([origem({ organization_id: OTHER_ORG_ID })], []);
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/duplicate/route");
    const res = await POST(req("POST"), ctx(P1));
    expect(res.status).toBe(404);
  });

  it("clona rascunho, gatilho e handoff — nasce draft, sem versão publicada", async () => {
    const db = makeDb([origem()], []);
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/duplicate/route");
    const res = await POST(req("POST"), ctx(P1));
    expect(res.status).toBe(201);
    const body = (await res.json()) as { data: Row };
    expect(body.data.status).toBe("draft");
    expect(body.data.active_version_id).toBeNull();
    expect(body.data.name).toBe("Carrinho (cópia)");
    expect(body.data.draft_graph).toEqual(VALID_GRAPH);
    expect(body.data.trigger_config).toEqual({
      kind: "silence",
      params: { threshold_minutes: 30 },
    });
    expect(body.data.handoff_policy).toBe("cancel");
    expect(body.data.id).not.toBe(P1);
    expect(vi.mocked(audit)).toHaveBeenCalledWith(
      expect.objectContaining({
        action: "followup_flow.duplicated",
        resourceId: body.data.id,
        metadata: expect.objectContaining({ source_pointer_id: P1 }),
      }),
    );
  });

  it("rascunho vazio com versão no ar copia o grafo publicado", async () => {
    const db = makeDb(
      [origem({ draft_graph: null })],
      [{ id: VID, organization_id: ORG_ID, pointer_id: P1, graph: VALID_GRAPH }],
    );
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/duplicate/route");
    const res = await POST(req("POST"), ctx(P1));
    expect(res.status).toBe(201);
    const body = (await res.json()) as { data: Row };
    expect(body.data.draft_graph).toEqual(VALID_GRAPH);
    expect(body.data.status).toBe("draft");
  });

  it("segunda cópia numera o nome — unique (organization_id, name)", async () => {
    const db = makeDb(
      [origem(), origem({ id: P2, name: "Carrinho (cópia)", status: "draft", active_version_id: null })],
      [],
    );
    session("manager", db);
    const { POST } = await import("@/app/api/v1/ai/followup-flows/[id]/duplicate/route");
    const res = await POST(req("POST"), ctx(P1));
    expect(res.status).toBe(201);
    const body = (await res.json()) as { data: Row };
    expect(body.data.name).toBe("Carrinho (cópia 2)");
  });
});

// Este teste isola o handler; autoridade de suporte é exercitada na suíte própria.
vi.mock("@/lib/impersonate/support", async (importOriginal) => ({
  ...await importOriginal<typeof import("@/lib/impersonate/support")>(),
  requireSupportWrite: vi.fn(async () => null),
  authenticatedSessionId: vi.fn(async () => "f2200000-0000-4000-8000-000000000099"),
}));
