import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { Pool } from "pg";
import { createAdminClient } from "@/lib/supabase/admin";
import { runRoutingWorker } from "@/lib/routing/worker";
import { routingPgSupabase } from "../support/routing-pg-supabase";
vi.mock("@/lib/supabase/admin", () => ({ createAdminClient: vi.fn() }));
vi.mock("@/lib/logger", () => ({ logger: { warn: vi.fn(), error: vi.fn() } }));
import { seedGov, GOV_ORG as org, GOV_AGENT_A as ana, GOV_AGENT_B as bruno, GOV_MANAGER as manager, GOV_ADMIN as admin, GOV_VIEWER as viewer, GOV_SESSION as channel, GOV_CONV_UNASSIGNED as conv, GOV_CONV_CLAIM as second } from "./gov-helpers";
const pool = new Pool({ connectionString: `postgresql://postgres:postgres@127.0.0.1:${process.env.TEST_DB_PORT}/postgres`, max: 5 });
const query = (text: string, args: unknown[] = []) => pool.query(text, args);
async function asUser(user: string, text: string, args: unknown[] = [], aal = "aal1") {
  const c = await pool.connect();
  try {
    await c.query("begin"); await c.query("set local role authenticated");
    await c.query("select set_config('request.jwt.claims',$1,true)", [JSON.stringify({ sub: user, aal })]);
    const result = await c.query(text, args); await c.query("commit"); return result;
  } catch (error) { await c.query("rollback"); throw error; } finally { c.release(); }
}
const policy = (users: string[], reset = false) => asUser(manager, "select fn_set_channel_routing($1,$2,$3,$4) result", [org, channel, users, reset]);
const claim = (conversation = conv, user = ana, expectedChannel = channel) => query("select fn_channel_routing_claim($1,$2,$3,$4,'{}') result", [org, conversation, expectedChannel, user]).then(r => r.rows[0].result);

beforeAll(() => seedGov());
afterAll(() => pool.end());
beforeEach(async () => {
  await query("update user_organizations set revoked_at=null,role='agent' where organization_id=$1 and user_id=any($2)", [org, [ana, bruno]]);
  await query("delete from channel_routing_policies where organization_id=$1", [org]);
  await query("update conversations set assigned_to_user_id=null,assignee_kind=null,status='open' where organization_id=$1", [org]);
  await query("insert into attendant_availability(organization_id,user_id,is_available,capacity,schedule) values($1,$2,true,1,'{}') on conflict(organization_id,user_id) do update set is_available=true,capacity=1,schedule='{}'", [org, ana]);
});

describe("roteamento instalado e transacional", () => {
  it("ausência de policy usa legado; CAS de replay conserva dono", async () => {
    expect(await claim()).toBe("assigned"); expect(await claim()).toBe("already_assigned");
    const r = await query("select assignee_kind,assigned_to_user_id,assigned_to_user_name,status from conversations where organization_id=$1 and id=$2", [org, conv]);
    expect(r.rows[0]).toMatchObject({ assignee_kind: "user", assigned_to_user_id: ana, status: "claimed" });
  });
  it("política vazia bloqueia e reset remove somente o marcador", async () => {
    await policy([]); expect(await claim()).toBe("candidate_not_allowed");
    await policy([], true); expect(await claim()).toBe("assigned");
  });
  it("revogação entre seleção e claim remove vínculo imediatamente", async () => {
    await policy([ana]); await query("update user_organizations set revoked_at=now() where organization_id=$1 and user_id=$2", [org, ana]);
    expect(await claim()).toBe("candidate_revoked");
    expect((await query("select count(*)::int n from channel_routing_responsibles where organization_id=$1", [org])).rows[0].n).toBe(0);
  });
  it("revogação de membro desatribui conversas abertas e devolve para a fila (#1562)", async () => {
    await query(
      "update conversations set assigned_to_user_id=$1, assigned_to_user_name='Ana', assignee_kind='user', status='claimed', bot_silenced_until='infinity' where organization_id=$2 and id=$3",
      [ana, org, conv],
    );
    await query("update user_organizations set revoked_at=now() where organization_id=$1 and user_id=$2", [org, ana]);

    const r = await query(
      "select assigned_to_user_id, assigned_to_user_name, assignee_kind, status, bot_silenced_until from conversations where organization_id=$1 and id=$2",
      [org, conv],
    );
    expect(r.rows[0]).toMatchObject({
      assigned_to_user_id: null,
      assigned_to_user_name: null,
      assignee_kind: null,
      status: "open",
      bot_silenced_until: null,
    });

    const ev = await query(
      "select from_user_id, to_user_id, reason from conversation_assignment_events where organization_id=$1 and conversation_id=$2 order by created_at desc limit 1",
      [org, conv],
    );
    expect(ev.rows[0]).toMatchObject({
      from_user_id: ana,
      to_user_id: null,
      reason: "member_revoked",
    });
  });
  it("duas conversas concorrentes disputam uma única vaga global", async () => {
    expect((await Promise.all([claim(conv), claim(second)])).sort()).toEqual(["assigned", "capacity_changed"]);
  });
  it("alteração de jornada entre seleção e claim invalida o snapshot", async () => {
    await query("update attendant_availability set schedule='{\"timezone\":\"UTC\"}' where organization_id=$1 and user_id=$2", [org, ana]);
    expect(await claim()).toBe("capacity_changed");
  });
  it("canal divergente é terminal sem atribuir", async () => {
    expect(await claim(conv, ana, "00000000-0000-4000-8000-000000000000")).toBe("conversation_changed");
  });
  it("viewer não grava; usuário estranho não provoca substituição parcial", async () => {
    await expect(asUser(viewer,"select fn_set_channel_routing($1,$2,$3,false)",[org,channel,[ana]])).rejects.toThrow("routing_forbidden");
    await policy([ana]);
    await expect(policy(["00000000-0000-4000-8000-000000000000"])).rejects.toThrow("routing_invalid_members");
    expect(await claim()).toBe("assigned");
  });
  it("aviso único resolve na atribuição e evento ativo é deduplicado", async () => {
    await query("select fn_routing_unassigned_notice($1,$2,'no_eligible'),fn_request_channel_routing($1,$2)", [org, conv]);
    await query("select fn_routing_unassigned_notice($1,$2,'no_eligible'),fn_request_channel_routing($1,$2)", [org, conv]);
    const n = await query("select count(*)::int n from agent_inbox_items where organization_id=$1 and ref_id=$2 and kind='routing_unassigned'", [org, conv]);
    expect(n.rows[0].n).toBe(1); expect(await claim()).toBe("assigned");
    expect((await query("select status from agent_inbox_items where organization_id=$1 and ref_id=$2 and kind='routing_unassigned'",[org,conv])).rows[0].status).toBe("resolved");
  });
  it("RLS não revela políticas a não membro; RPC privada não é alcançável por authenticated", async () => {
    await policy([ana]);
    const outsider = "00000000-0000-4000-8000-000000000001";
    expect((await asUser(outsider,"select * from channel_routing_policies where organization_id=$1",[org])).rowCount).toBe(0);
    expect((await asUser(outsider,"select * from channel_routing_responsibles where organization_id=$1",[org])).rowCount).toBe(0);
    await expect(asUser(ana,"select fn_channel_routing_claim($1,$2,$3,$4,'{}')",[org,conv,channel,ana])).rejects.toThrow("permission denied");
  });
});

describe("regras por NÚMERO (canal) — issue #2331", () => {
  // A regra do número viaja na MESMA RPC dos responsáveis, com dois parâmetros
  // a mais. A chamada de 4 argumentos (helper `policy`, usada neste arquivo
  // inteiro) continua válida — é a prova de compatibilidade com quem já instalou.
  const policyComRegra = (users: string[], mode: string | null, destino: string | null) =>
    asUser(manager, "select fn_set_channel_routing($1,$2,$3,false,$4,$5) result", [org, channel, users, mode, destino]);
  const regraGravada = async () =>
    (await query("select distribution_mode, default_assignee_user_id from channel_routing_policies where organization_id=$1 and channel_session_id=$2", [org, channel])).rows[0];
  const claimSemJornada = (conversation = conv, user = ana) =>
    query("select fn_channel_routing_claim($1,$2,$3,$4,null) result", [org, conversation, channel, user]).then(r => r.rows[0].result);
  const disponibilizar = (user: string) =>
    query("insert into attendant_availability(organization_id,user_id,is_available,capacity,schedule) values($1,$2,true,1,'{}') on conflict(organization_id,user_id) do update set is_available=true,capacity=1,schedule='{}'", [org, user]);

  it("modo e destino padrão persistem; a chamada de 4 argumentos continua sem regra própria", async () => {
    await policy([ana]);
    expect(await regraGravada()).toEqual({ distribution_mode: null, default_assignee_user_id: null });

    await policyComRegra([ana], "round_robin", bruno);
    expect(await regraGravada()).toEqual({ distribution_mode: "round_robin", default_assignee_user_id: bruno });

    // Regravar pelos responsáveis (o que a tela antiga faz) LIMPA a regra do
    // número em vez de deixar lixo: o número volta a herdar a organização.
    await policy([ana]);
    expect(await regraGravada()).toEqual({ distribution_mode: null, default_assignee_user_id: null });
  });

  it("modo fora da lista da tela e destino que não é membro são recusados sem gravar nada", async () => {
    await expect(policyComRegra([ana], "telefone", null)).rejects.toThrow("routing_invalid_mode");
    await expect(policyComRegra([ana], "round_robin", "00000000-0000-4000-8000-000000000000")).rejects.toThrow("routing_invalid_members");
    expect((await query("select count(*)::int n from channel_routing_policies where organization_id=$1", [org])).rows[0].n).toBe(0);
  });

  it("destino padrão do número recebe mesmo fora dos responsáveis (entrega direta)", async () => {
    await disponibilizar(bruno);
    // Responsáveis = [ana]; destino padrão = bruno. Sem a exceção no claim, a
    // entrega ao destino padrão cairia em candidate_not_allowed e a conversa
    // ficaria na fila para sempre.
    await policyComRegra([ana], "round_robin", bruno);
    expect(await claimSemJornada(conv, bruno)).toBe("assigned");
    const r = await query("select assigned_to_user_id from conversations where organization_id=$1 and id=$2", [org, conv]);
    expect(r.rows[0].assigned_to_user_id).toBe(bruno);
  });

  it("a exceção do destino padrão não abre a porta para quem não é nem responsável nem destino", async () => {
    await policyComRegra([ana], "round_robin", bruno);
    expect(await claimSemJornada(conv, manager)).toBe("candidate_not_allowed");
  });

  it("CHECK recusa modo inventado e a FK composta recusa destino de fora da organização", async () => {
    await policyComRegra([ana], "round_robin", bruno);
    await expect(query("update channel_routing_policies set distribution_mode='telefone' where organization_id=$1 and channel_session_id=$2", [org, channel])).rejects.toMatchObject({ code: "23514" });
    await expect(query("update channel_routing_policies set default_assignee_user_id='00000000-0000-4000-8000-0000000000ff' where organization_id=$1 and channel_session_id=$2", [org, channel])).rejects.toMatchObject({ code: "23503" });
  });

  it("worker real: o modo do NÚMERO vence o da organização (org manual, número em rodízio)", async () => {
    await query("update organizations set settings=jsonb_set(coalesce(settings,'{}'::jsonb),'{routing}','{\"mode\":\"manual\"}') where id=$1", [org]);
    await policyComRegra([ana], "round_robin", null);
    await query("delete from event_log where organization_id=$1 and event_type='conversation.routing_requested'", [org]);
    await query("select fn_request_channel_routing($1,$2)", [org, conv]);
    const adapter = routingPgSupabase(pool); vi.mocked(createAdminClient).mockReturnValue(adapter.client as never);
    const result = await runRoutingWorker({ now: new Date() });
    expect(result.outcomes.assigned).toBe(1);
    expect(adapter.errors).toEqual([]);
    expect((await query("select assigned_to_user_id from conversations where organization_id=$1 and id=$2", [org, conv])).rows[0].assigned_to_user_id).toBe(ana);
  });

  it("worker real: número SEM regra cai no comportamento da organização (manual ⇒ não distribui)", async () => {
    await query("update organizations set settings=jsonb_set(coalesce(settings,'{}'::jsonb),'{routing}','{\"mode\":\"manual\"}') where id=$1", [org]);
    await policy([ana]); // número com responsáveis, mas sem modo próprio
    await query("delete from event_log where organization_id=$1 and event_type='conversation.routing_requested'", [org]);
    await query("select fn_request_channel_routing($1,$2)", [org, conv]);
    const adapter = routingPgSupabase(pool); vi.mocked(createAdminClient).mockReturnValue(adapter.client as never);
    const result = await runRoutingWorker({ now: new Date() });
    expect(result.outcomes.skipped_manual).toBe(1);
    expect((await query("select assigned_to_user_id from conversations where organization_id=$1 and id=$2", [org, conv])).rows[0].assigned_to_user_id).toBeNull();
  });

  it("worker real: destino padrão do número entrega direto, sem passar pela distribuição", async () => {
    await disponibilizar(bruno);
    await query("update organizations set settings=jsonb_set(coalesce(settings,'{}'::jsonb),'{routing}','{\"mode\":\"load\"}') where id=$1", [org]);
    // Modo "manual" no número (entrega direta ao destino padrão) — se o worker
    // ainda lesse o modo da organização (load), distribuiria por carga.
    await policyComRegra([], "manual", bruno);
    await query("delete from event_log where organization_id=$1 and event_type='conversation.routing_requested'", [org]);
    await query("select fn_request_channel_routing($1,$2)", [org, conv]);
    const adapter = routingPgSupabase(pool); vi.mocked(createAdminClient).mockReturnValue(adapter.client as never);
    const result = await runRoutingWorker({ now: new Date() });
    expect(result.outcomes.assigned).toBe(1);
    expect(adapter.errors).toEqual([]);
    expect((await query("select to_user_id from conversation_assignment_events where organization_id=$1 and conversation_id=$2 order by created_at desc limit 1", [org, conv])).rows[0].to_user_id).toBe(bruno);
  });

  it("worker real: a conversa COM setor pertence à fila — o destino padrão do número que não é da fila não a recebe", async () => {
    // (revisão do mantenedor, 31/10) Este caso existe porque o `select` do
    // worker não trazia `sector_id` e um cast escondia a falta: a fila era lida
    // como `null`, o destino padrão do número entregava direto e o horário do
    // setor nunca era consultado. Aqui o destino padrão é o bruno, que NÃO é da
    // fila: se a fila for ignorada, ele recebe a conversa e o teste reprova.
    await disponibilizar(ana);
    await disponibilizar(bruno);
    await query("insert into platform_config (chave, valor) values ('MODULO_SETORES','ligado') on conflict (chave) do update set valor='ligado'");
    const fila = (await query(
      "insert into attendant_sectors (organization_id, name) values ($1, 'Fila do worker') returning id",
      [org],
    )).rows[0].id as string;
    await query("insert into attendant_sector_members (organization_id, user_id, sector_id) values ($1,$2,$3)", [org, ana, fila]);
    await query("update organizations set settings=jsonb_set(coalesce(settings,'{}'::jsonb),'{routing}','{\"mode\":\"round_robin\"}') where id=$1", [org]);
    await policyComRegra([ana, bruno], "round_robin", bruno);
    // `metadata` também volta ao zero: os casos anteriores deste arquivo deixam
    // `devolucao_ao_rodizio` no metadata da MESMA conversa, e o worker exclui
    // quem está nessa lista — a fila pareceria vazia por herança, não por regra.
    await query("update conversations set sector_id=$1, assigned_to_user_id=null, assignee_kind=null, status='open', metadata='{}'::jsonb where organization_id=$2 and id=$3", [fila, org, conv]);
    await query("delete from event_log where organization_id=$1 and event_type='conversation.routing_requested'", [org]);
    await query("select fn_request_channel_routing($1,$2)", [org, conv]);
    const adapter = routingPgSupabase(pool); vi.mocked(createAdminClient).mockReturnValue(adapter.client as never);
    const resultado = await runRoutingWorker({ now: new Date() });
    expect(resultado.outcomes.assigned).toBe(1);
    expect(adapter.errors).toEqual([]);
    expect((await query("select assigned_to_user_id from conversations where organization_id=$1 and id=$2", [org, conv])).rows[0].assigned_to_user_id).toBe(ana);
    // Devolve a instalação ao estado de fábrica: o módulo nasce desligado, e
    // outro caso deste arquivo não pode herdar o setor ligado.
    await query("delete from platform_config where chave='MODULO_SETORES'");
    await query("update conversations set sector_id=null where organization_id=$1 and id=$2", [org, conv]);
  });

  it("worker real: módulo setores DESLIGADO — a fila gravada na conversa não vale e o destino padrão recebe, como na main", async () => {
    // Quem liga Setores, usa e desliga fica com `sector_id` nas conversas. O
    // rodízio tem de voltar a ser o de sempre: nem o worker nem o claim podem
    // seguir barrando quem não é da fila.
    await disponibilizar(ana);
    await disponibilizar(bruno);
    await query("delete from platform_config where chave='MODULO_SETORES'");
    const fila = (await query(
      "insert into attendant_sectors (organization_id, name) values ($1, 'Fila desligada') returning id",
      [org],
    )).rows[0].id as string;
    await query("insert into attendant_sector_members (organization_id, user_id, sector_id) values ($1,$2,$3)", [org, ana, fila]);
    await query("update organizations set settings=jsonb_set(coalesce(settings,'{}'::jsonb),'{routing}','{\"mode\":\"round_robin\"}') where id=$1", [org]);
    await policyComRegra([ana, bruno], "round_robin", bruno);
    await query("update conversations set sector_id=$1, assigned_to_user_id=null, assignee_kind=null, status='open', metadata='{}'::jsonb where organization_id=$2 and id=$3", [fila, org, conv]);
    await query("delete from event_log where organization_id=$1 and event_type='conversation.routing_requested'", [org]);
    await query("select fn_request_channel_routing($1,$2)", [org, conv]);
    const adapter = routingPgSupabase(pool); vi.mocked(createAdminClient).mockReturnValue(adapter.client as never);
    const resultado = await runRoutingWorker({ now: new Date() });
    expect(resultado.outcomes.assigned).toBe(1);
    expect(adapter.errors).toEqual([]);
    expect((await query("select assigned_to_user_id from conversations where organization_id=$1 and id=$2", [org, conv])).rows[0].assigned_to_user_id).toBe(bruno);
    await query("update conversations set sector_id=null where organization_id=$1 and id=$2", [org, conv]);
  });
});

describe("reserva de conexão org-owned", () => {
  const key = "10000000-0000-4000-8000-000000000001";
  const reserve = (hash="a".repeat(64)) => asUser(admin,"select fn_reserve_channel_connection($1,$2,$3,'Teste',false) result",[org,key,hash]).then(r=>r.rows[0].result);
  beforeEach(()=>query("delete from channel_connection_requests where organization_id=$1",[org]));
  it("concorrência da mesma chave não cria duas identidades; payload distinto conflita", async () => {
    const first = await reserve(); expect(first.channel.organization_id).toBe(org);
    await expect(reserve()).rejects.toThrow("connection_in_progress");
    await expect(reserve("b".repeat(64))).rejects.toThrow("idempotency_conflict");
    await query("select fn_finish_channel_connection($1,$2,$3,'SCAN_QR_CODE')",[org,first.receipt_id,first.lease_token]);
    const replay = await reserve(); expect(replay.replay).toBe(true); expect(replay.channel.id).toBe(first.channel.id);
  });
  it("lease incorreta não publica sucesso; falha mantém identidade FAILED para reparo", async () => {
    const first = await reserve();
    await expect(query("select fn_finish_channel_connection($1,$2,$3,'WORKING')",[org,first.receipt_id,key])).rejects.toThrow("connection_lease_lost");
    await query("select fn_finish_channel_connection($1,$2,$3,'FAILED','connection_repair_required')",[org,first.receipt_id,first.lease_token]);
    const next = await reserve(); expect(next.channel.id).toBe(first.channel.id); expect(next.lease_token).not.toBe(first.lease_token);
  });
  it("retry antigo não toma o lease de outro recibo do mesmo onboarding", async () => {
    const firstKey = "40000000-0000-4000-8000-000000000001", nextKey = "40000000-0000-4000-8000-000000000002";
    const reserveOnboarding = (k: string) => asUser(admin,"select fn_reserve_channel_connection($1,$2,$3,null,true) result",[org,k,"d".repeat(64)]).then(r=>r.rows[0].result);
    const first = await reserveOnboarding(firstKey);
    await query("select fn_finish_channel_connection($1,$2,$3,'FAILED','repair')",[org,first.receipt_id,first.lease_token]);
    const next = await reserveOnboarding(nextKey); expect(next.channel.id).toBe(first.channel.id);
    await expect(reserveOnboarding(firstKey)).rejects.toThrow("connection_in_progress");
  });
  it("recibo privado não concede SELECT/UPDATE a membro", async () => {
    await reserve();
    await expect(asUser(admin,"select * from channel_connection_requests where organization_id=$1",[org])).rejects.toThrow("permission denied");
  });
});

describe("fronteiras de política e reserva", () => {
  it("dois tenants reais: leitura positiva local, negativa cruzada e FK composta", async () => {
    const otherOrg = "30000000-0000-4000-8000-000000000001", otherUser = "30000000-0000-4000-8000-000000000002";
    await query("insert into organizations(id,slug,legal_name,display_name) values($1,'routing-other','Outra','Outra') on conflict do nothing",[otherOrg]);
    await query("insert into auth.users(id,email) values($1,'routing-other@invariant.test') on conflict do nothing",[otherUser]);
    await query("insert into user_organizations(organization_id,user_id,role,accepted_at) values($1,$2,'admin',now()) on conflict do nothing",[otherOrg,otherUser]);
    await policy([ana]);
    expect((await asUser(manager,"select * from channel_routing_policies where organization_id=$1",[org])).rowCount).toBe(1);
    expect((await asUser(otherUser,"select * from channel_routing_policies where organization_id=$1",[org])).rowCount).toBe(0);
    expect((await asUser(otherUser,"select * from channel_routing_responsibles where organization_id=$1",[org])).rowCount).toBe(0);
    await expect(query("insert into channel_routing_policies(organization_id,channel_session_id) values($1,$2)",[otherOrg,channel])).rejects.toMatchObject({code:"23503"});
    await expect(asUser(otherUser,"select fn_set_channel_routing($1,$2,$3,false)",[org,channel,[otherUser]])).rejects.toThrow("routing_forbidden");
  });
  it("platform admin com fator cadastrado prova MFA nas duas RPCs humanas", async () => {
    const factor="30000000-0000-4000-8000-000000000003";
    await query("insert into platform_admins(user_id,granted_by,scope,mfa_required,reason) values($1,$1,'full',false,'Test')",[admin]);
    await query("insert into auth.mfa_factors(id,user_id,status,factor_type) values($1,$2,'verified','totp')",[factor,admin]);
    try {
      await expect(asUser(admin,"select fn_set_channel_routing($1,$2,$3,false)",[org,channel,[]])).rejects.toThrow("routing_mfa_required");
      await expect(asUser(admin,"select fn_reserve_channel_connection($1,$2,$3)",[org,factor,"c".repeat(64)])).rejects.toThrow("connection_mfa_required");
      expect((await asUser(admin,"select fn_set_channel_routing($1,$2,$3,false)",[org,channel,[]],"aal2")).rowCount).toBe(1);
      expect((await asUser(admin,"select fn_reserve_channel_connection($1,$2,$3)",[org,factor,"c".repeat(64)],"aal2")).rowCount).toBe(1);
    } finally { await query("delete from auth.mfa_factors where id=$1",[factor]);await query("delete from platform_admins where user_id=$1",[admin]); }
  });
});


describe("worker real contra PostgreSQL — ponte SQL, sem HTTP PostgREST", () => {
  it("timestamp do trigger permite persistir requeue/attempts/aviso; payload inválido fica terminal", async () => {
    await policy([]);
    await query("update organizations set settings=jsonb_set(settings,'{routing}','{\"mode\":\"round_robin\",\"max_retries\":1,\"backoff_seconds\":1}') where id=$1",[org]);
    await query("delete from event_log where organization_id=$1 and event_type='conversation.routing_requested'",[org]);
    await query("select fn_request_channel_routing($1,$2)",[org,conv]);
    await query("update event_log set attempts=1,next_attempt_at=null where organization_id=$1 and entity_id=$2 and event_type='conversation.routing_requested'",[org,conv]);
    const adapter=routingPgSupabase(pool);vi.mocked(createAdminClient).mockReturnValue(adapter.client as never);
    const now=new Date(); const result=await runRoutingWorker({now});
    expect(result.outcomes.requeued_no_eligible).toBe(1);expect(adapter.errors).toEqual([]);
    const pending=(await query("select status,attempts,next_attempt_at from event_log where organization_id=$1 and entity_id=$2 and event_type='conversation.routing_requested'",[org,conv])).rows[0];
    expect(pending.status).toBe("pending");expect(pending.attempts).toBe(1);expect(new Date(pending.next_attempt_at).getTime()).toBe(now.getTime()+900000);
    await query("update event_log set payload=jsonb_build_object('conversation_id','not-a-uuid','organization_id',$1::text),next_attempt_at=null where organization_id=$1 and entity_id=$2 and event_type='conversation.routing_requested'",[org,conv]);
    expect((await runRoutingWorker()).outcomes.skipped_invalid_payload).toBe(1);
    expect((await query("select status from event_log where organization_id=$1 and entity_id=$2 and event_type='conversation.routing_requested'",[org,conv])).rows[0].status).toBe("done");
  });
  it("falha após criação preserva identidade e permite retry com novo lease", async () => {
    const key="50000000-0000-4000-8000-000000000001";
    const reserve=()=>asUser(admin,"select fn_reserve_channel_connection($1,$2,$3) r",[org,key,"e".repeat(64)]).then(r=>r.rows[0].r);
    const first=await reserve();
    await query("select fn_finish_channel_connection($1,$2,$3,'remote_created')",[org,first.receipt_id,first.lease_token]);
    const failed=(await query("select fn_finish_channel_connection($1,$2,$3,'FAILED','connection_repair_required') r",[org,first.receipt_id,first.lease_token])).rows[0].r;
    expect(failed.status).toBe("FAILED");expect(failed.archived_at).toBeNull();
    const next=await reserve();expect(next.lease_token).not.toBe(first.lease_token);
    expect(next.channel.id).toBe(first.channel.id);expect(next.channel.waha_session_name).toBe(first.channel.waha_session_name);
    await expect(query("select fn_finish_channel_connection($1,$2,$3,'WORKING')",[org,first.receipt_id,first.lease_token])).rejects.toMatchObject({code:"55P03"});
  });
});
