/**
 * A rota que EXCLUI um canal — a única deste módulo que destrói coisa.
 *
 * O dublê aplica os filtros `eq`/`is` de verdade e conta as linhas de verdade,
 * como o de `app/api/v1/pipelines/**`. Um stub de contagem fixa deixaria "canal
 * de outra organização → 404" e "o roteador de IA impede o hard delete" passarem
 * mesmo com o `eq("organization_id", …)` apagado da rota: mediria o dublê.
 *
 * Ele também registra CADA escrita com seus filtros e a ORDEM das chamadas ao
 * transporte, porque três garantias aqui são sobre a emissão em si — "nenhuma
 * escrita quando é 404", "revoga ANTES de mexer no banco" e "arquivar não apaga".
 */
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { NextRequest } from "next/server";

import { audit } from "@/lib/audit";
import { fail } from "@/lib/api/wrappers";
import { requireRole } from "@/lib/auth/require-role";
import { loadAuthUser, orgAtivaSemPortao } from "@/lib/auth/server";
import type { AuthUser } from "@/lib/auth/types";
import { createClient } from "@/lib/supabase/server";
import { createAdminClient } from "@/lib/supabase/admin";
import { getWahaClient } from "@/lib/waha/client";
import { apagarAssinaturaSocial } from "@/lib/channels/social/store";
import { logger } from "@/lib/logger";
import { decryptWebhookSecret } from "@/lib/webhooks/secrets";

// `orgAtivaDaApi` REAL (sobre o `orgAtivaSemPortao` mockado): é ela que decide o 403 da org suspensa.
vi.mock("@/lib/auth/require-role", async (original) => ({
  ...(await original<Record<string, unknown>>()),
  requireRole: vi.fn(),
}));
vi.mock("@/lib/auth/server", () => ({
  loadAuthUser: vi.fn(),
  orgAtivaSemPortao: vi.fn(),
  mfaEmDivida: vi.fn(async () => false),
}));
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) }));
vi.mock("@/lib/waha/client", () => ({
  getWahaClient: vi.fn(),
  wahaFriendlyError: (m: string) => m,
}));
// A assinatura do Zernio morre no provedor, não no dublê de banco: aqui só
// interessa que a rota PEÇA para apagar antes de mexer na linha — apagar de
// verdade é do módulo, coberto em `lib/channels/social/apagar-assinatura.test.ts`.
vi.mock("@/lib/channels/social/store", () => ({ apagarAssinaturaSocial: vi.fn() }));
// A credencial da linha chega CIFRADA: aqui só interessa que a rota peça para
// decifrar a coluna intacta — decifrar de verdade é do módulo, não desta rota.
vi.mock("@/lib/webhooks/secrets", async (importOriginal) => {
  const real = (await importOriginal()) as Record<string, unknown>;
  return { ...real, decryptWebhookSecret: vi.fn() };
});

const ORG = "22222222-2222-4222-8222-222222222222";
const OUTRA_ORG = "33333333-3333-4333-8333-333333333333";
const USER = "11111111-1111-4111-8111-111111111111";
const CANAL = "44444444-4444-4444-8444-444444444444";

type Linha = Record<string, unknown>;

interface Escrita {
  tipo: "update" | "delete";
  table: string;
  patch: Linha | null;
  filtros: Array<[string, unknown]>;
}

interface Registro {
  escritas: Escrita[];
  /** Ordem observável de TUDO que tem efeito colateral (transporte + banco). */
  eventos: string[];
  /**
   * O que sobrou em cada tabela — para afirmar o EFEITO, e não só a chamada.
   * Sem isso, "o aviso foi resolvido" viraria "alguém chamou update".
   */
  linhas: (table: string) => Linha[];
}

interface DbOpts {
  /** Linhas de `channel_sessions`. Default: um canal pareado por QR, da ORG. */
  sessions?: Linha[];
  /** Demais tabelas, por nome — é o que a contagem de impacto lê. */
  rows?: Record<string, Linha[]>;
  readError?: (table: string) => { code?: string; message: string } | null;
  /** Erro do banco na n-ésima escrita (1-based), como o PostgREST devolveria. */
  writeError?: (n: number, table: string) => { code?: string; message: string } | null;
}

function canal(over: Linha = {}): Linha {
  return {
    id: CANAL,
    organization_id: ORG,
    provider: "waha",
    waha_session_name: "org_2222_abc",
    display_name: "Comercial",
    phone_number: "+5531999998888",
    status: "WORKING",
    archived_at: null,
    ...over,
  };
}

function makeDb(opts: DbOpts = {}): Registro {
  const registro: Registro = {
    escritas: [],
    eventos: [],
    linhas: (t) => tabelas[t] ?? [],
  };
  const tabelas: Record<string, Linha[]> = {
    channel_sessions: opts.sessions ?? [canal()],
    ...(opts.rows ?? {}),
  };
  let nEscritas = 0;

  class Q implements PromiseLike<unknown> {
    private filtros: Array<[string, unknown]> = [];
    private inclusoes: Array<[string, unknown[]]> = [];
    private limite: number | undefined;
    private ordenacao: { col: string; ascending: boolean } | undefined;
    private head = false;
    private contar = false;
    private single = false;

    constructor(
      private readonly table: string,
      private readonly op: "select" | "update" | "delete",
      private readonly patch: Linha | null = null,
    ) {}

    select(_cols?: string, o?: { count?: string; head?: boolean }): this {
      this.head = o?.head === true;
      this.contar = o?.count !== undefined;
      return this;
    }
    eq(col: string, val: unknown): this {
      this.filtros.push([col, val]);
      return this;
    }
    is(col: string, val: unknown): this {
      this.filtros.push([col, val]);
      return this;
    }
    in(col: string, values: unknown[]): this {
      this.inclusoes.push([col, values]);
      return this;
    }
    order(col: string, options?: { ascending?: boolean }): this {
      this.ordenacao = { col, ascending: options?.ascending !== false };
      return this;
    }
    limit(n: number): this {
      this.limite = n;
      return this;
    }
    maybeSingle(): this {
      this.single = true;
      return this;
    }

    private casam(): Linha[] {
      const linhas = tabelas[this.table] ?? [];
      return linhas.filter((l) => this.filtros.every(([c, v]) => (l[c] ?? null) === v)
        && this.inclusoes.every(([c, values]) => values.includes(l[c])));
    }

    private executar(): { data: unknown; error: unknown; count?: number } {
      if (this.op === "select") {
        const error = opts.readError?.(this.table);
        if (error) return { data: null, error };
        let achadas = this.casam();
        const order = this.ordenacao;
        if (order) achadas.sort((a, b) => String(a[order.col]).localeCompare(String(b[order.col])) * (order.ascending ? 1 : -1));
        if (this.limite !== undefined) achadas = achadas.slice(0, this.limite);
        if (this.contar) return { data: null, error: null, count: achadas.length };
        if (this.head) return { data: null, error: null };
        return { data: this.single ? (achadas[0] ?? null) : achadas, error: null };
      }

      nEscritas += 1;
      const erro = opts.writeError?.(nEscritas, this.table) ?? null;
      registro.escritas.push({
        tipo: this.op,
        table: this.table,
        patch: this.patch,
        filtros: this.filtros,
      });
      registro.eventos.push(`${this.op}:${this.table}`);
      if (erro) return { data: null, error: erro };

      const alvo = this.casam();
      if (this.op === "delete") {
        tabelas[this.table] = (tabelas[this.table] ?? []).filter((l) => !alvo.includes(l));
      } else {
        for (const l of alvo) Object.assign(l, this.patch);
      }
      return { data: null, error: null };
    }

    then<R1 = unknown, R2 = never>(
      onOk?: ((v: { data: unknown; error: unknown; count?: number }) => R1 | PromiseLike<R1>) | null,
      onErr?: ((r: unknown) => R2 | PromiseLike<R2>) | null,
    ): PromiseLike<R1 | R2> {
      return Promise.resolve(this.executar()).then(onOk, onErr);
    }
  }

  const client = {
    from: (table: string) => ({
      select: (cols?: string, o?: { count?: string; head?: boolean }) =>
        new Q(table, "select").select(cols, o),
      update: (patch: Linha) => new Q(table, "update", patch),
      delete: () => new Q(table, "delete"),
    }),
  };

  vi.mocked(createClient).mockResolvedValue(client as never);
  vi.mocked(createAdminClient).mockReturnValue(client as never);
  return registro;
}

function authOk(): void {
  const user: AuthUser = {
    id: USER,
    email: "a@example.com",
    full_name: null,
    avatar_url: null,
    is_platform_admin: false,
    idioma: "pt-BR" as const,
    organizations: [{ organization_id: ORG, organization_name: "Org", role: "admin" }],
  };
  vi.mocked(requireRole).mockResolvedValue({
    ok: true,
    user,
    org: { orgId: ORG, name: "Org", role: "admin" },
  });
  vi.mocked(loadAuthUser).mockResolvedValue(user);
  vi.mocked(orgAtivaSemPortao).mockResolvedValue({ orgId: ORG, name: "Org", role: "admin", org_status: "active" });
}

/** Transporte do canal pareado por QR, registrando a ordem junto com o banco. */
function wahaOk(registro: Registro) {
  const cliente = {
    logoutSession: vi.fn(async () => {
      registro.eventos.push("waha:logout");
    }),
    deleteSession: vi.fn(async () => {
      registro.eventos.push("waha:delete");
    }),
    getVerifiedSession: vi.fn(async () => ({ name: "org_2222_abc", status: "WORKING", me: { id: "5531999998888@c.us" } })),
  };
  vi.mocked(getWahaClient).mockReturnValue(cliente as never);
  return cliente;
}

const ctx = (id = CANAL) => ({ params: Promise.resolve({ id }) });
const reqDelete = (id = CANAL) =>
  new NextRequest(`http://localhost/api/v1/channel-sessions/${id}`, { method: "DELETE" });
const reqGet = (qs = "", id = CANAL) =>
  new NextRequest(`http://localhost/api/v1/channel-sessions/${id}${qs}`);

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

describe("DELETE /api/v1/channel-sessions/[id]", () => {
  it("exige admin", async () => {
    authOk();
    const db = makeDb();
    wahaOk(db);
    const { DELETE } = await import("./route");
    await DELETE(reqDelete(), ctx());
    expect(vi.mocked(requireRole).mock.calls[0]?.[0]).toBe("admin");
  });

  it("sem auth → repassa a resposta, sem escrever nem revogar", async () => {
    vi.mocked(requireRole).mockResolvedValue({
      ok: false,
      response: fail("unauthenticated", "Auth required.", 401, {}),
    });
    const db = makeDb();
    const waha = wahaOk(db);
    const { DELETE } = await import("./route");
    expect((await DELETE(reqDelete(), ctx())).status).toBe(401);
    expect(db.escritas).toEqual([]);
    expect(waha.logoutSession).not.toHaveBeenCalled();
  });

  it("canal VIRGEM → apaga a linha e emite channel.deleted", async () => {
    authOk();
    const db = makeDb();
    wahaOk(db);
    const { DELETE } = await import("./route");
    const res = await DELETE(reqDelete(), ctx());

    expect(res.status).toBe(200);
    expect((await res.json()).data.archived).toBe(false);
    expect(db.escritas).toHaveLength(1);
    expect(db.escritas[0]).toMatchObject({ tipo: "delete", table: "channel_sessions" });
    expect(db.escritas[0]?.filtros).toContainEqual(["organization_id", ORG]);
    expect(audit).toHaveBeenCalledWith(expect.objectContaining({ action: "channel.deleted" }));
  });

  it("revoga no transporte ANTES de tocar no banco", async () => {
    authOk();
    const db = makeDb();
    wahaOk(db);
    const { DELETE } = await import("./route");
    await DELETE(reqDelete(), ctx());
    // Ordem inversa deixaria sessão órfã ativa no transporte, recebendo webhook
    // de um canal que a UI já não mostra.
    expect(db.eventos).toEqual(["waha:logout", "waha:delete", "delete:channel_sessions"]);
  });

  it("canal COM CONVERSAS → arquiva, NÃO apaga, e emite channel.archived", async () => {
    authOk();
    const db = makeDb({
      rows: { conversations: [{ id: "c1", organization_id: ORG, channel_session_id: CANAL }] },
    });
    wahaOk(db);
    const { DELETE } = await import("./route");
    const res = await DELETE(reqDelete(), ctx());

    expect(res.status).toBe(200);
    expect((await res.json()).data.archived).toBe(true);
    expect(db.escritas.map((e) => e.tipo)).toEqual(["update"]);
    expect(db.escritas[0]?.patch).toMatchObject({ status: "STOPPED" });
    expect(db.escritas[0]?.patch?.archived_at).toEqual(expect.any(String));
    expect(audit).toHaveBeenCalledWith(expect.objectContaining({ action: "channel.archived" }));
  });

  /**
   * ⭐ `ai_routers.channel_session_id` é ON DELETE CASCADE, não RESTRICT: pela
   * régua das "três FKs que bloqueiam", este canal é virgem — e o DELETE levava o
   * roteador e os `ai_router_members` dele sem uma palavra, com o toast dizendo só
   * "Canal excluído".
   */
  it("canal com ROTEADOR DE IA → arquiva; o roteador continua de pé", async () => {
    authOk();
    const db = makeDb({
      rows: {
        ai_routers: [{ id: "r1", organization_id: ORG, channel_session_id: CANAL }],
      },
    });
    wahaOk(db);
    const { DELETE } = await import("./route");
    const res = await DELETE(reqDelete(), ctx());
    const body = await res.json();

    expect(res.status).toBe(200);
    expect(body.data.archived).toBe(true);
    expect(body.data.impact.configuration.ai_routers).toBe(1);
    expect(db.escritas.map((e) => e.tipo)).toEqual(["update"]);
  });

  it("ajuste anti-ban do operador (channel_knobs) também impede o hard delete", async () => {
    authOk();
    const db = makeDb({
      rows: { channel_knobs: [{ organization_id: ORG, channel_session_id: CANAL }] },
    });
    wahaOk(db);
    const { DELETE } = await import("./route");
    expect((await DELETE(reqDelete(), ctx())).status).toBe(200);
    expect(db.escritas.map((e) => e.tipo)).toEqual(["update"]);
  });

  it("dependência de OUTRA organização não conta — a contagem é por tenant", async () => {
    authOk();
    const db = makeDb({
      rows: { ai_routers: [{ id: "r1", organization_id: OUTRA_ORG, channel_session_id: CANAL }] },
    });
    wahaOk(db);
    const { DELETE } = await import("./route");
    const res = await DELETE(reqDelete(), ctx());
    expect((await res.json()).data.archived).toBe(false);
    expect(db.escritas.map((e) => e.tipo)).toEqual(["delete"]);
  });

  /**
   * ⭐ `meta_phone_number_id` é a coluna do ramo oficial da união; `waha_session_name`
   * é NULL nele por CHECK. Uma guarda `if (waha && session.waha_session_name)` some
   * inteira aqui: a linha era arquivada, a plataforma continuava entregando no
   * webhook, e o inbox recebia mensagem de um canal "excluído".
   */
  it("canal OFICIAL → revoga credencial e rotaciona o webhook, sem falar com o transporte", async () => {
    authOk();
    const db = makeDb({
      sessions: [
        canal({
          provider: "meta_cloud",
          waha_session_name: null,
          meta_phone_number_id: "123456",
          webhook_path_token: "tokenantigo",
        }),
      ],
      rows: { conversations: [{ id: "c1", organization_id: ORG, channel_session_id: CANAL }] },
    });
    const waha = wahaOk(db);
    const { DELETE } = await import("./route");
    const res = await DELETE(reqDelete(), ctx());

    expect(res.status).toBe(200);
    expect(waha.logoutSession).not.toHaveBeenCalled();
    const patch = db.escritas[0]?.patch as Linha;
    expect(patch.meta_token_encrypted).toBeNull();
    expect(patch.webhook_path_token).toEqual(expect.any(String));
    expect(patch.webhook_path_token).not.toBe("tokenantigo");
  });

  it("canal oficial NÃO exige o transporte no ar", async () => {
    authOk();
    makeDb({
      sessions: [
        canal({ provider: "meta_cloud", waha_session_name: null, meta_phone_number_id: "123456" }),
      ],
    });
    vi.mocked(getWahaClient).mockReturnValue(null);
    const { DELETE } = await import("./route");
    expect((await DELETE(reqDelete(), ctx())).status).toBe(200);
  });

  /** Falha FECHADA: 200 sem revogar prometia uma desconexão que não aconteceu. */
  it("canal por QR sem o transporte no ar → 503 e NENHUMA escrita", async () => {
    authOk();
    const db = makeDb();
    vi.mocked(getWahaClient).mockReturnValue(null);
    const { DELETE } = await import("./route");
    const res = await DELETE(reqDelete(), ctx());

    expect(res.status).toBe(503);
    expect(db.escritas).toEqual([]);
    expect(audit).not.toHaveBeenCalled();
  });

  it("transporte recusa a revogação → 502, preserva canal e registra FAILED", async () => {
    authOk();
    const db = makeDb();
    const waha = wahaOk(db);
    waha.logoutSession.mockRejectedValue(new Error("waha_logout_500"));
    const { DELETE } = await import("./route");
    expect((await DELETE(reqDelete(), ctx())).status).toBe(502);
    expect(waha.logoutSession).toHaveBeenCalledWith("org_2222_abc");
    expect(waha.deleteSession).not.toHaveBeenCalled();
    expect(db.escritas).toHaveLength(1);
    expect(db.escritas[0]).toMatchObject({ tipo: "update", table: "channel_sessions",
      patch: { status: "FAILED", status_reason: "connection_repair_required" } });
    expect(db.escritas[0]?.patch).not.toHaveProperty("archived_at");
    expect(db.escritas[0]?.filtros).toContainEqual(["organization_id", ORG]);
    expect(audit).not.toHaveBeenCalled();
  });

  it("lease vigente da mesma org e canal bloqueia exclusão sem efeito", async () => {
    authOk();
    const db = makeDb({ rows: { channel_connection_requests: [
      { id: "expired", organization_id: ORG, channel_session_id: CANAL, state: "processing", lease_until: new Date(Date.now() - 60000).toISOString() },
      { id: "busy", organization_id: ORG, channel_session_id: CANAL, state: "processing", lease_until: new Date(Date.now() + 60000).toISOString() },
    ] } });
    const waha = wahaOk(db); const { DELETE } = await import("./route");
    const res = await DELETE(reqDelete(), ctx());
    expect(res.status).toBe(409);expect((await res.json()).error.code).toBe("connection_in_progress");
    expect(db.escritas).toEqual([]);expect(waha.logoutSession).not.toHaveBeenCalled();expect(waha.deleteSession).not.toHaveBeenCalled();
  });

  it.each(["other_tenant", "other_channel", "failed", "expired"])("recibo %s não bloqueia a exclusão autorizada", async (scenario) => {
    authOk();
    const receipt = { id: "r1", organization_id: scenario === "other_tenant" ? OUTRA_ORG : ORG,
      channel_session_id: scenario === "other_channel" ? USER : CANAL,
      state: scenario === "failed" ? "failed" : "processing",
      lease_until: new Date(Date.now() + (scenario === "expired" ? -60000 : 60000)).toISOString() };
    const db = makeDb({ rows: { channel_connection_requests: [receipt] } });
    const waha = wahaOk(db);const { DELETE } = await import("./route");
    expect((await DELETE(reqDelete(), ctx())).status).toBe(200);
    expect(waha.logoutSession).toHaveBeenCalledWith("org_2222_abc");expect(waha.deleteSession).toHaveBeenCalledWith("org_2222_abc");
  });

  it("erro ao consultar reserva retorna503 sem revogar nem escrever", async () => {
    authOk();const db = makeDb({ readError: (table) => table === "channel_connection_requests" ? { message: "DB unavailable" } : null });
    const waha = wahaOk(db);const { DELETE } = await import("./route");
    expect((await DELETE(reqDelete(), ctx())).status).toBe(503);
    expect(db.escritas).toEqual([]);expect(waha.logoutSession).not.toHaveBeenCalled();expect(waha.deleteSession).not.toHaveBeenCalled();
  });

  it("canal de outra organização → 404, nenhuma escrita, nenhuma revogação", async () => {
    authOk();
    const db = makeDb({ sessions: [canal({ organization_id: OUTRA_ORG })] });
    const waha = wahaOk(db);
    const { DELETE } = await import("./route");
    expect((await DELETE(reqDelete(), ctx())).status).toBe(404);
    expect(db.escritas).toEqual([]);
    expect(waha.logoutSession).not.toHaveBeenCalled();
  });

  it("erro do banco no arquivamento → 500 (não vira 200 mentiroso)", async () => {
    authOk();
    const db = makeDb({
      rows: { messages: [{ id: "m1", organization_id: ORG, channel_session_id: CANAL }] },
      writeError: () => ({ code: "42501", message: "permission denied" }),
    });
    wahaOk(db);
    const { DELETE } = await import("./route");
    expect((await DELETE(reqDelete(), ctx())).status).toBe(500);
  });

  /**
   * ⭐ #2419 — excluir canal social pela Central deixava a assinatura de webhook
   * viva no Zernio: a assinatura é por chave, não por conta, e a rota arquivava
   * a linha e rotacionava o token sem removê-la — o provedor seguia entregando
   * numa URL que virou 404, sem erro do nosso lado.
   */
  describe("canal SOCIAL → apaga a assinatura no Zernio ANTES de mexer na linha", () => {
    const social = (over: Linha = {}) =>
      canal({ provider: "zernio_social", waha_session_name: null, ...over });

    it("apaga a assinatura antes da escrita e registra na auditoria", async () => {
      authOk();
      const db = makeDb({ sessions: [social({ webhook_path_token: "tokenantigo" })] });
      vi.mocked(apagarAssinaturaSocial).mockImplementation(async () => {
        db.eventos.push("social:apagar-assinatura");
        return "apagada";
      });
      const waha = wahaOk(db);
      const { DELETE } = await import("./route");
      const res = await DELETE(reqDelete(), ctx());

      expect(res.status).toBe(200);
      expect(apagarAssinaturaSocial).toHaveBeenCalledWith(expect.anything(), ORG, CANAL);
      // Revoga ANTES de mexer no banco — se a ordem invertesse, a reconciliação
      // pela URL do token não acharia mais a assinatura.
      expect(db.eventos[0]).toBe("social:apagar-assinatura");
      expect(waha.logoutSession).not.toHaveBeenCalled();
      expect(vi.mocked(decryptWebhookSecret)).not.toHaveBeenCalled();
      expect(audit).toHaveBeenCalledWith(
        expect.objectContaining({
          metadata: expect.objectContaining({ social_webhook: "apagada" }),
        }),
      );
    });

    it("provedor fora do ar → best-effort: 200 e a falha vai para a auditoria", async () => {
      authOk();
      const db = makeDb({ sessions: [social()] });
      vi.mocked(apagarAssinaturaSocial).mockRejectedValue(new Error("zernio_500"));
      wahaOk(db);
      const { DELETE } = await import("./route");
      const res = await DELETE(reqDelete(), ctx());

      expect(res.status).toBe(200);
      expect(db.escritas).not.toHaveLength(0);
      expect(audit).toHaveBeenCalledWith(
        expect.objectContaining({
          metadata: expect.objectContaining({ social_webhook: "falhou" }),
        }),
      );
    });

    it("no arquivamento rotaciona o token da URL sem tocar na credencial da Meta", async () => {
      authOk();
      const db = makeDb({
        sessions: [social({ webhook_path_token: "tokenantigo" })],
        rows: { conversations: [{ id: "c1", organization_id: ORG, channel_session_id: CANAL }] },
      });
      vi.mocked(apagarAssinaturaSocial).mockResolvedValue("apagada");
      wahaOk(db);
      const { DELETE } = await import("./route");
      const res = await DELETE(reqDelete(), ctx());

      expect((await res.json()).data.archived).toBe(true);
      const patch = db.escritas[0]?.patch as Linha;
      expect(patch.webhook_path_token).toMatch(/^[a-f0-9]{32}$/);
      expect(patch.webhook_path_token).not.toBe("tokenantigo");
      expect(patch).not.toHaveProperty("meta_token_encrypted");
    });
  });
});

describe("GET /api/v1/channel-sessions/[id]", () => {
  it("erro de identidade/transporte não publica status nem grava saúde", async () => {
    authOk();const db = makeDb();const waha = wahaOk(db);
    waha.getVerifiedSession.mockRejectedValue(new Error("session_identity_mismatch"));
    const { GET } = await import("./route");
    const res = await GET(reqGet(), ctx());
    expect(res.status).toBe(502);expect((await res.json()).error.code).toBe("connection_status_failed");
    expect(waha.getVerifiedSession).toHaveBeenCalledWith("org_2222_abc");expect(db.escritas).toEqual([]);
  });

  it("?impact=1 devolve o preflight — o diálogo sabe o desfecho ANTES do clique", async () => {
    authOk();
    const db = makeDb({
      rows: {
        conversations: [{ id: "c1", organization_id: ORG, channel_session_id: CANAL }],
        ai_routers: [{ id: "r1", organization_id: ORG, channel_session_id: CANAL }],
        // Duas ligações semeadas de propósito: a contagem tem de vir do BANCO.
        // Com `voice_calls: 0` na expectativa, a rota podia ter parado de contar
        // e o caso continuaria verde — e o histórico de voz sumiria no cascade
        // sem o diálogo avisar, que é exatamente o defeito que esta onda fecha.
        voice_calls: [
          { id: "v1", organization_id: ORG, channel_session_id: CANAL },
          { id: "v2", organization_id: ORG, channel_session_id: CANAL },
        ],
      },
    });
    wahaOk(db);
    const { GET } = await import("./route");
    const body = await (await GET(reqGet("?impact=1"), ctx())).json();

    expect(body.data.deletion_impact).toEqual({
      outcome: "archive",
      history: { conversations: 1, messages: 0, agent_versions: 0, voice_calls: 2 },
      configuration: { ai_routers: 1, channel_knobs: 0, before_send_traces: 0 },
    });
  });

  it("sem ?impact=1 não paga as contagens — a rota é pollada esperando o QR", async () => {
    authOk();
    const db = makeDb();
    wahaOk(db);
    const { GET } = await import("./route");
    const body = await (await GET(reqGet(), ctx())).json();
    expect(body.data.deletion_impact).toBeUndefined();
  });

  /**
   * ⭐ O update do health check tinha o `error` DESCARTADO: a linha nova ficava
   * sem telefone e nada na tela dizia por quê.
   */
  it("número já usado por outro canal ativo → regrava sem o telefone e NOMEIA o conflito", async () => {
    authOk();
    const db = makeDb({
      sessions: [canal({ phone_number: null })],
      writeError: (n) => (n === 1 ? { code: "23505", message: "duplicate key" } : null),
    });
    wahaOk(db);
    const { GET } = await import("./route");
    const res = await GET(reqGet(), ctx());
    const body = await res.json();

    expect(res.status).toBe(200);
    expect(body.data.phone_number_conflict).toBe(true);
    expect(body.data.phone_number).toBeNull();
    expect(db.escritas).toHaveLength(2);
    expect(db.escritas[1]?.patch).not.toHaveProperty("phone_number");
  });

  it("erro de banco que NÃO é conflito de número → 500, nunca engolido", async () => {
    authOk();
    const db = makeDb({
      sessions: [canal({ phone_number: null })],
      writeError: () => ({ code: "42501", message: "permission denied" }),
    });
    wahaOk(db);
    const { GET } = await import("./route");
    expect((await GET(reqGet(), ctx())).status).toBe(500);
    // Uma tentativa só: 42501 não é conflito de número, então não há o que regravar.
    expect(db.escritas).toHaveLength(1);
  });

  it("canal OFICIAL não consulta o transporte — ele não tem sessão lá", async () => {
    authOk();
    const db = makeDb({
      sessions: [
        canal({ provider: "meta_cloud", waha_session_name: null, meta_phone_number_id: "123456" }),
      ],
    });
    const waha = wahaOk(db);
    const { GET } = await import("./route");
    const res = await GET(reqGet(), ctx());

    expect(res.status).toBe(200);
    expect(waha.getVerifiedSession).not.toHaveBeenCalled();
    expect((await res.json()).data.waha_configured).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// A conexão removida não deixa o aviso dela para trás (#1023)
// ---------------------------------------------------------------------------

/**
 * Arquivar/excluir tira o ÚNICO emissor que existia — a sessão que manda
 * `session.status` — e, no arquivamento, a própria rota de webhook passa a
 * recusar evento do canal. Se ninguém fechar o episódio aberto aqui, ele fica
 * para sempre na Central, crítico, apontando para uma linha que a tela já não
 * carrega. Aqui se mede o fechamento, a contenção entre conexões e — o que mais
 * importa para o caminho comum — que nada é escrito quando não há aviso.
 */
describe("#1023 — a conexão removida fecha o próprio aviso", () => {
  const avisoAberto = (refId: string, id = "i1"): Linha => ({
    id,
    organization_id: ORG,
    kind: "channel_number_alert",
    severity: "critical",
    title: "WhatsApp fora do ar (STOPPED)",
    ref_kind: "channel_session",
    ref_id: refId,
    status: "open",
  });

  it("⭐ canal ARQUIVADO com aviso crítico aberto → aviso resolvido e episódio limpo", async () => {
    authOk();
    const db = makeDb({
      rows: {
        conversations: [{ id: "c1", organization_id: ORG, channel_session_id: CANAL }],
        agent_inbox_items: [avisoAberto(CANAL)],
        channel_session_health: [
          { organization_id: ORG, channel_session_id: CANAL, escalated_status: "FAILED" },
        ],
      },
    });
    wahaOk(db);
    const { DELETE } = await import("./route");
    const res = await DELETE(reqDelete(), ctx());

    expect(res.status).toBe(200);
    expect(db.linhas("agent_inbox_items")[0]?.status).toBe("resolved");
    expect(db.linhas("channel_session_health")[0]?.escalated_status).toBe(null);
    expect(audit).toHaveBeenCalledWith(
      expect.objectContaining({
        action: "channel.archived",
        metadata: expect.objectContaining({ avisos_fechados: "resolvido" }),
      }),
    );
  });

  /**
   * O ramo que o relato descreve: a conexão foi EXCLUÍDA de vez, não arquivada —
   * canal sem histórico nem configuração dá `outcome: "delete"` e cai no
   * `.delete()` da rota. É o único ramo em que a linha de saúde já nem existe
   * mais (o `on delete cascade` levou junto), então o resquício que pode sobrar
   * é SÓ o item da Central — e é ele que o fecho tem de alcançar, vindo DEPOIS
   * da exclusão. Sem esta chamada neste ramo, o teste falha: a exclusão de uma
   * conexão virgem deixaria o crítico aberto para sempre, que é o sintoma da
   * issue.
   */
  it("⭐ canal EXCLUÍDO de vez com aviso aberto → nenhum aviso daquela conexão sobra", async () => {
    authOk();
    const db = makeDb({
      rows: {
        agent_inbox_items: [
          avisoAberto(CANAL, "i1"),
          avisoAberto("99999999-9999-4999-8999-999999999999", "i2"),
        ],
      },
    });
    wahaOk(db);
    const { DELETE } = await import("./route");
    const res = await DELETE(reqDelete(), ctx());
    const body = await res.json();

    expect(res.status).toBe(200);
    // Hard delete de verdade: a linha some, ninguém ganha `archived_at`.
    expect(body.data.archived).toBe(false);
    expect(db.linhas("channel_sessions")).toEqual([]);
    // O fecho roda DEPOIS da exclusão — é o que separa este ramo do arquivamento.
    expect(db.eventos).toEqual([
      "waha:logout",
      "waha:delete",
      "delete:channel_sessions",
      "update:agent_inbox_items",
    ]);
    expect(
      db.linhas("agent_inbox_items").filter((l) => l.ref_id === CANAL && l.status === "open"),
    ).toEqual([]);
    // O aviso de OUTRA conexão segue aberto: o fecho é por `ref_id`, nunca por org.
    expect(db.linhas("agent_inbox_items").find((l) => l.id === "i2")?.status).toBe("open");
    // A auditoria diz o que aconteceu, e não só que a exclusão saiu.
    expect(audit).toHaveBeenCalledWith(
      expect.objectContaining({
        action: "channel.deleted",
        metadata: expect.objectContaining({ avisos_fechados: "resolvido" }),
      }),
    );
  });

  it("⭐ canal VIRGEM (o caso comum) não escreve nada a mais", async () => {
    authOk();
    const db = makeDb();
    wahaOk(db);
    const { DELETE } = await import("./route");
    await DELETE(reqDelete(), ctx());

    // Só a linha do canal. Sem aviso aberto e sem episódio, não há update.
    expect(db.escritas.map((e) => `${e.tipo}:${e.table}`)).toEqual([
      "delete:channel_sessions",
    ]);
  });

  it("o aviso de OUTRA conexão continua aberto — ela pode seguir caída", async () => {
    authOk();
    const db = makeDb({
      rows: {
        conversations: [{ id: "c1", organization_id: ORG, channel_session_id: CANAL }],
        agent_inbox_items: [
          avisoAberto(CANAL, "i1"),
          avisoAberto("99999999-9999-4999-8999-999999999999", "i2"),
        ],
      },
    });
    wahaOk(db);
    const { DELETE } = await import("./route");
    await DELETE(reqDelete(), ctx());

    const outro = db.linhas("agent_inbox_items").find((l) => l.id === "i2");
    expect(outro?.status).toBe("open");
  });

  it("erro ao LER os avisos não desfaz a exclusão que o operador pediu", async () => {
    authOk();
    const warn = vi.spyOn(logger, "warn").mockImplementation(() => undefined);
    const db = makeDb({
      rows: {
        conversations: [{ id: "c1", organization_id: ORG, channel_session_id: CANAL }],
        agent_inbox_items: [avisoAberto(CANAL)],
      },
      readError: (table) => (table === "agent_inbox_items" ? { message: "boom" } : null),
    });
    wahaOk(db);
    const { DELETE } = await import("./route");
    const res = await DELETE(reqDelete(), ctx());

    // O canal já saiu do transporte e a linha já mudou: o melhor-esforço não
    // pode transformar uma exclusão bem-sucedida em erro para o operador.
    expect(res.status).toBe(200);
    expect(db.linhas("channel_sessions")[0]?.archived_at).toEqual(expect.any(String));
    // O supabase-js não lança em erro do PostgREST: se a função engolisse o
    // `error`, a leitura falha viraria "nenhum aviso aberto" e a auditoria
    // diria "sem_mudanca" com o crítico ainda na Central.
    expect(db.linhas("agent_inbox_items")[0]?.status).toBe("open");
    expect(audit).toHaveBeenCalledWith(
      expect.objectContaining({
        metadata: expect.objectContaining({ avisos_fechados: "falhou" }),
      }),
    );
    expect(warn).toHaveBeenCalledWith(
      "Falha ao fechar os avisos de saúde da conexão removida",
      expect.objectContaining({ channel_session_id: CANAL, organization_id: ORG }),
    );
  });

  it("erro ao RESOLVER os avisos → auditoria diz \"falhou\", não \"resolvido\"", async () => {
    authOk();
    const warn = vi.spyOn(logger, "warn").mockImplementation(() => undefined);
    const db = makeDb({
      rows: {
        conversations: [{ id: "c1", organization_id: ORG, channel_session_id: CANAL }],
        agent_inbox_items: [avisoAberto(CANAL)],
      },
      writeError: (_n, table) =>
        table === "agent_inbox_items" ? { code: "57014", message: "boom" } : null,
    });
    wahaOk(db);
    const { DELETE } = await import("./route");
    const res = await DELETE(reqDelete(), ctx());

    expect(res.status).toBe(200);
    expect(db.linhas("channel_sessions")[0]?.archived_at).toEqual(expect.any(String));
    expect(db.linhas("agent_inbox_items")[0]?.status).toBe("open");
    expect(audit).toHaveBeenCalledWith(
      expect.objectContaining({
        action: "channel.archived",
        metadata: expect.objectContaining({ avisos_fechados: "falhou" }),
      }),
    );
    expect(warn).toHaveBeenCalledWith(
      "Falha ao fechar os avisos de saúde da conexão removida",
      expect.objectContaining({ erro: expect.stringContaining("57014") }),
    );
  });
});

// ---------------------------------------------------------------------------
// O canal oficial devolve o webhook do número antes de perder a credencial (#1334)
// ---------------------------------------------------------------------------

/**
 * Conectar um canal oficial aponta o webhook DESTE número para a URL desta
 * instalação (`registrarWebhookDoNumero`). Arquivar/excluir zerava
 * `meta_token_encrypted` e rotacionava o `webhook_path_token` — que é
 * exatamente o que autoriza aquela URL — sem nunca devolver o número à URL do
 * app: a Meta seguiria entregando num endereço que passa a responder 404, para
 * sempre e sem erro do nosso lado.
 *
 * Aqui se mede o que o operador não vê: (a) a Graph é chamada para o número
 * DAQUELA sessão com `override_callback_uri` vazio; (b) a chamada acontece
 * ANTES da escrita que apaga a credencial — é a única janela em que o token e o
 * `phone_number_id` existem, e é o que faz o desfazer ser possível; (c) a falha
 * da Meta não desfaz a exclusão que o operador pediu.
 *
 * A inscrição na WABA (`subscribed_apps`) fica de fora de propósito: é por WABA
 * e compartilhada, então desfazê-la em uma exclusão derrubaria as outras.
 */
describe("#1334 — a conexão oficial devolve o webhook do número à Meta", () => {
  const OFICIAL = (): Linha =>
    canal({
      provider: "meta_cloud",
      waha_session_name: null,
      meta_phone_number_id: "1234567890",
      meta_token_encrypted: "cifra-da-credencial",
    });

  /** Com histórico, o desfecho é ARQUIVAR; sem nada, a linha some de vez. */
  const historico = {
    conversations: [{ id: "c1", organization_id: ORG, channel_session_id: CANAL }],
  };

  interface ChamadaDaGraph {
    url: string;
    corpo: Record<string, unknown>;
    autorizacao: string | undefined;
  }

  /**
   * Dublê do `fetch` GLOBAL — e não do módulo: quem decide a URL e o corpo do
   * desfazer é `desfazerWebhookDoNumero`, e é isso que precisa ser medido. Um
   * dublê do módulo mediria só que alguém o chamou.
   */
  function graphDublado(
    registro: Registro,
    resposta?: { status: number; body: unknown },
  ): ChamadaDaGraph[] {
    const chamadas: ChamadaDaGraph[] = [];
    vi.stubGlobal(
      "fetch",
      vi.fn(async (url: string | URL, init?: { body?: string; headers?: unknown }) => {
        const alvo = String(url);
        chamadas.push({
          url: alvo,
          corpo: JSON.parse(String(init?.body ?? "{}")) as Record<string, unknown>,
          autorizacao: (init?.headers as Record<string, string> | undefined)?.Authorization,
        });
        // Marca no MESMO registro de ordem do dublê de banco: é assim que a
        // precedência vira asserção, e não decoração.
        if (alvo.includes("graph.facebook.com")) registro.eventos.push("meta:override");
        return new Response(JSON.stringify(resposta?.body ?? { success: true }), {
          status: resposta?.status ?? 200,
          headers: { "content-type": "application/json" },
        });
      }),
    );
    return chamadas;
  }

  beforeEach(() => {
    vi.mocked(decryptWebhookSecret).mockResolvedValue("token-da-linha");
  });

  afterEach(() => {
    vi.unstubAllGlobals();
    vi.mocked(decryptWebhookSecret).mockReset();
  });

  it("⭐ ARQUIVADO → a Graph recebe override vazio ANTES de a credencial ser apagada", async () => {
    authOk();
    const db = makeDb({ sessions: [OFICIAL()], rows: historico });
    wahaOk(db);
    const chamadas = graphDublado(db);
    const { DELETE } = await import("./route");

    const res = await DELETE(reqDelete(), ctx());
    const corpo = (await res.json()) as { data: { archived: boolean } };

    expect(res.status).toBe(200);
    expect(corpo.data.archived).toBe(true);
    // (a) A devolução: o número da sessão volta para a URL do app.
    expect(chamadas).toHaveLength(1);
    expect(chamadas[0]?.url).toContain("/1234567890");
    expect(chamadas[0]?.corpo).toEqual({
      webhook_configuration: { override_callback_uri: "" },
    });
    // Quem autoriza a chamada é a credencial da linha intacta, decifrada.
    expect(chamadas[0]?.autorizacao).toBe("Bearer token-da-linha");
    expect(decryptWebhookSecret).toHaveBeenCalledWith(expect.anything(), "cifra-da-credencial");

    // (b) A ordem: sem a linha intacta não há token, e o desfazer vira a
    // chamada que nunca acontece — que é o bug da issue.
    const graph = db.eventos.indexOf("meta:override");
    const patch = db.eventos.indexOf("update:channel_sessions");
    expect(graph).toBeGreaterThanOrEqual(0);
    expect(patch).toBeGreaterThanOrEqual(0);
    expect(graph).toBeLessThan(patch);
    expect(db.escritas.at(-1)?.patch?.meta_token_encrypted).toBeNull();

    expect(audit).toHaveBeenCalledWith(
      expect.objectContaining({
        action: "channel.archived",
        metadata: expect.objectContaining({ webhook_override: "desfeito" }),
      }),
    );
  });

  it("⭐ EXCLUÍDO de vez → o override é desfeito antes de a linha sumir", async () => {
    authOk();
    const db = makeDb({ sessions: [OFICIAL()] });
    wahaOk(db);
    const chamadas = graphDublado(db);
    const { DELETE } = await import("./route");

    const res = await DELETE(reqDelete(), ctx());
    const corpo = (await res.json()) as { data: { archived: boolean } };

    expect(res.status).toBe(200);
    expect(corpo.data.archived).toBe(false);
    expect(chamadas).toHaveLength(1);
    expect(chamadas[0]?.corpo).toEqual({
      webhook_configuration: { override_callback_uri: "" },
    });

    const graph = db.eventos.indexOf("meta:override");
    const del = db.eventos.indexOf("delete:channel_sessions");
    expect(graph).toBeGreaterThanOrEqual(0);
    expect(graph).toBeLessThan(del);

    expect(audit).toHaveBeenCalledWith(
      expect.objectContaining({
        action: "channel.deleted",
        metadata: expect.objectContaining({ webhook_override: "desfeito" }),
      }),
    );
  });

  it("⭐ a Meta recusa o desfazer → a exclusão continua e a falha fica registrada", async () => {
    authOk();
    const warn = vi.spyOn(logger, "warn").mockImplementation(() => undefined);
    const db = makeDb({ sessions: [OFICIAL()], rows: historico });
    wahaOk(db);
    graphDublado(db, { status: 400, body: { error: { message: "token inválido" } } });
    const { DELETE } = await import("./route");

    const res = await DELETE(reqDelete(), ctx());

    // Melhor-esforço: o canal que não devolve o webhook continua arquivado —
    // recusar a exclusão seria trocar um problema por dois.
    expect(res.status).toBe(200);
    expect(db.linhas("channel_sessions")[0]?.archived_at).toEqual(expect.any(String));
    expect(db.escritas.at(-1)?.patch?.meta_token_encrypted).toBeNull();
    expect(warn).toHaveBeenCalledWith(
      "A Meta recusou desfazer o override do webhook do número",
      expect.objectContaining({
        channel_session_id: CANAL,
        organization_id: ORG,
        phone_number_id: "1234567890",
        etapa: "configuracao_do_numero",
        motivo: "token inválido",
      }),
    );
    expect(audit).toHaveBeenCalledWith(
      expect.objectContaining({
        action: "channel.archived",
        metadata: expect.objectContaining({ webhook_override: "falhou" }),
      }),
    );
    warn.mockRestore();
  });

  it("canal oficial já sem credencial → não chama a Graph e diz por quê", async () => {
    authOk();
    const db = makeDb({
      sessions: [canal({ provider: "meta_cloud", waha_session_name: null, meta_phone_number_id: "1234567890", meta_token_encrypted: null })],
      rows: historico,
    });
    wahaOk(db);
    const chamadas = graphDublado(db);
    const { DELETE } = await import("./route");

    const res = await DELETE(reqDelete(), ctx());

    expect(res.status).toBe(200);
    expect(chamadas).toHaveLength(0);
    expect(decryptWebhookSecret).not.toHaveBeenCalled();
    expect(audit).toHaveBeenCalledWith(
      expect.objectContaining({
        metadata: expect.objectContaining({ webhook_override: "sem_credencial" }),
      }),
    );
  });
});
