/**
 * A CONFIG DE ATENDIMENTO tem porta, e a porta não desliga a restrição sozinha
 * (issue #144).
 *
 * ## O que estava quebrado
 *
 * `settings.visibility_mode` — o que faz um atendente ver só os leads dele — era
 * LIDO em `app/app/layout.tsx` e escrito por NINGUÉM. A RLS existia
 * (`fn_can_view_lead`), o schema existia, a doutrina existia, e o único jeito de
 * ligar era `UPDATE` à mão no Postgres. Num produto self-host, feature sem
 * caminho no produto é feature que não existe.
 *
 * ## O que se guarda aqui
 *
 * A regra do PATCH, que é onde mora o risco de a restrição cair por acidente:
 * um cliente antigo, que só conhece o roteamento, manda um corpo SEM
 * `visibility_mode`. Se o handler tratasse ausência como "volte ao default", a
 * org perderia a restrição sem ninguém pedir — e ninguém notaria, porque o
 * sintoma é dado a MAIS na tela, não erro.
 */
import { describe, expect, it } from "vitest";

import {
  VISIBILITY_MODES,
  atendimentoConfigPatchSchema,
  mesclarSettingsDeAtendimento,
  routingConfigSchema,
} from "@/lib/schemas/routing";
import { PRAZO_MAX_MINUTOS_DEVOLUCAO, PRAZO_MIN_MINUTOS } from "@/lib/escalacao/devolucao-automatica";
import { DEFAULT_VISIBILITY_MODE, type VisibilityMode } from "@/lib/auth/types";

/**
 * A regra de merge do handler, extraída para poder ser exercitada sem HTTP.
 * Espelha `app/api/v1/settings/routing/route.ts` — e o caso de vacuidade abaixo
 * cobra que ela não vire uma cópia que passou a discordar do original.
 */
function proximoSettings(
  atual: Record<string, unknown>,
  corpo: unknown,
): Record<string, unknown> {
  // A MESMA função da rota (antes este helper era uma cópia dela, e cópia
  // diverge sem avisar).
  return mesclarSettingsDeAtendimento(atual, atendimentoConfigPatchSchema.parse(corpo)).settings;
}

describe("config de atendimento — routing + visibilidade na mesma porta", () => {
  it("os quatro modos de visibilidade do schema são os mesmos do tipo de auth", () => {
    // As duas listas moram em arquivos diferentes por motivo real (uma valida
    // input externo, a outra tipa a sessão). Divergir significaria a tela
    // oferecer um modo que a RLS não entende — e a RLS cai no `else`, que é
    // "não vê nada".
    const doTipo: VisibilityMode[] = ["all", "own_and_unassigned", "own", "own_and_sector"];
    expect([...VISIBILITY_MODES]).toEqual(doTipo);
    expect(VISIBILITY_MODES).toContain(DEFAULT_VISIBILITY_MODE);
  });

  it("corpo SEM visibility_mode preserva a que já valia", () => {
    const atual = { visibility_mode: "own", routing: { mode: "manual" }, outra_chave: 1 };
    const proximo = proximoSettings(atual, { mode: "round_robin", max_retries: 5, backoff_seconds: 60 });

    expect(
      proximo.visibility_mode,
      "cliente que só conhece roteamento não pode desligar a restrição sem pedir",
    ).toBe("own");
    expect((proximo.routing as { mode: string }).mode).toBe("round_robin");
    // Merge não-destrutivo: as demais chaves do jsonb de settings sobrevivem.
    expect(proximo.outra_chave).toBe(1);
  });

  it("corpo COM visibility_mode grava o valor pedido", () => {
    const proximo = proximoSettings(
      { visibility_mode: "all" },
      { mode: "round_robin", max_retries: 5, backoff_seconds: 60, visibility_mode: "own" },
    );
    expect(proximo.visibility_mode).toBe("own");
  });

  it("modo de visibilidade inválido é recusado, não normalizado", () => {
    // Aceitar e cair num default seria o pior desfecho: a tela confirmaria o
    // salvamento e a org ficaria num modo que ninguém escolheu.
    expect(() =>
      atendimentoConfigPatchSchema.parse({
        mode: "manual",
        max_retries: 5,
        backoff_seconds: 60,
        visibility_mode: "somente_o_chefe",
      }),
    ).toThrow();
  });

  it("o schema de roteamento segue aceitando o corpo antigo (guarda de compatibilidade)", () => {
    // O contrato v1 já estava publicado. Se o corpo de antes deixasse de valer,
    // a mudança seria quebra de API disfarçada de feature nova.
    const antigo = { mode: "round_robin", max_retries: 3, backoff_seconds: 30 };
    // O corpo antigo não conhece o prazo de devolução, "a conversa fica com
    // quem atendeu", o prazo do silêncio manual nem a devolução AO RODÍZIO
    // (#2985): no PATCH eles ficam OMITIDOS (e a mescla preserva o que vale), no
    // schema do jsonb eles têm o padrão (null, false, null e null).
    const {
      handoff_return_after_minutes: _padrao,
      conversation_stays_with_attendant: _fica,
      manual_reply_silence_minutes: _silencio,
      unanswered_return_minutes: _devolveAoRodizio,
      ...semPrazo
    } = routingConfigSchema.parse(antigo);
    expect(atendimentoConfigPatchSchema.parse(antigo)).toEqual(semPrazo);
  });

  it("corpo SEM unanswered_return_minutes preserva a devolução ao rodízio que já valia", () => {
    // A omissão não pode LIGAR a regra para quem nunca a configurou (o padrão
    // do jsonb é null, desligada) nem DESLIGAR quem já a tinha ligada.
    const atual = { routing: { mode: "manual", unanswered_return_minutes: 45 } };
    const proximo = proximoSettings(atual, { mode: "round_robin", max_retries: 5, backoff_seconds: 60 });
    expect(
      (proximo.routing as { unanswered_return_minutes: number | null }).unanswered_return_minutes,
      "cliente que só conhece roteamento não pode mexer na devolução ao rodízio por omissão",
    ).toBe(45);
    const semConfigurar = proximoSettings(
      { routing: { mode: "manual" } },
      { mode: "round_robin", max_retries: 5, backoff_seconds: 60 },
    );
    expect((semConfigurar.routing as { unanswered_return_minutes: unknown }).unanswered_return_minutes).toBeNull();
  });

  it("unanswered_return_minutes: grava o valor, null desliga, fora da faixa é recusado", () => {
    const base = { mode: "manual", max_retries: 5, backoff_seconds: 60 };
    const com60 = proximoSettings(
      { routing: { mode: "manual" } },
      { ...base, unanswered_return_minutes: 60 },
    );
    expect((com60.routing as { unanswered_return_minutes: unknown }).unanswered_return_minutes).toBe(60);
    const nulo = proximoSettings(com60, { ...base, unanswered_return_minutes: null });
    expect((nulo.routing as { unanswered_return_minutes: unknown }).unanswered_return_minutes).toBeNull();
    for (const fora of [4, 1441, 7.5]) {
      expect(() => atendimentoConfigPatchSchema.parse({ ...base, unanswered_return_minutes: fora })).toThrow();
    }
  });

  it("corpo SEM handoff_return_after_minutes preserva o prazo que já valia", () => {
    const atual = { routing: { mode: "manual", handoff_return_after_minutes: 60 } };
    const proximo = proximoSettings(atual, { mode: "round_robin", max_retries: 5, backoff_seconds: 60 });
    expect(
      (proximo.routing as { handoff_return_after_minutes: number | null }).handoff_return_after_minutes,
      "cliente que só conhece roteamento não pode desligar a devolução automática por omissão",
    ).toBe(60);
  });

  it("handoff_return_after_minutes: null desliga; fora da faixa é recusado", () => {
    const atual = { routing: { mode: "manual", handoff_return_after_minutes: 60 } };
    const proximo = proximoSettings(atual, {
      mode: "manual",
      max_retries: 5,
      backoff_seconds: 60,
      handoff_return_after_minutes: null,
    });
    expect((proximo.routing as { handoff_return_after_minutes: unknown }).handoff_return_after_minutes).toBeNull();
    for (const fora of [4, PRAZO_MIN_MINUTOS - 1, PRAZO_MAX_MINUTOS_DEVOLUCAO + 1, 7.5]) {
      expect(() =>
        atendimentoConfigPatchSchema.parse({ mode: "manual", max_retries: 5, backoff_seconds: 60, handoff_return_after_minutes: fora }),
      ).toThrow();
    }
  });

  /**
   * O TETO DE 24 HORAS DO PRAZO DE DEVOLUÇÃO CAIU (#3313, item 3).
   *
   * Este arquivo prendia `1441 → recusa`, e a recusa era o defeito: quem
   * administra uma agência de viagens não conseguia salvar os 5.000 minutos de
   * uma cotação com fornecedor (mais de três dias). O teto agora é de 30 dias
   * (`PRAZO_MAX_MINUTOS_DEVOLUCAO`), e o valor do meio continua recusado.
   *
   * ─── SABOTAGEM ────────────────────────────────────────────────────────────
   *   - voltar `.max(PRAZO_MAX_MINUTOS)`: o caso do 5.000 acusa (a tela salvaria
   *     menos do que a operação pediu, e o cron descartaria o que passasse).
   */
  it("handoff_return_after_minutes aceita 5.000 minutos (dias) — e o teto de 30 dias continua valendo", () => {
    const base = { mode: "manual", max_retries: 5, backoff_seconds: 60 } as const;
    expect(
      atendimentoConfigPatchSchema.parse({ ...base, handoff_return_after_minutes: 5000 }),
      "5.000 minutos são ~3,5 dias: é o caso medido na issue",
    ).toMatchObject({ handoff_return_after_minutes: 5000 });
    expect(PRAZO_MAX_MINUTOS_DEVOLUCAO).toBe(30 * 24 * 60);
    expect(
      atendimentoConfigPatchSchema.parse({ ...base, handoff_return_after_minutes: PRAZO_MAX_MINUTOS_DEVOLUCAO }),
    ).toMatchObject({ handoff_return_after_minutes: PRAZO_MAX_MINUTOS_DEVOLUCAO });
    // O teto do SILÊNCIO do celular continua de 24 h: os dois knobs deixaram de
    // compartilhar a mesma constante de propósito.
    expect(() =>
      atendimentoConfigPatchSchema.parse({ ...base, manual_reply_silence_minutes: PRAZO_MAX_MINUTOS_DEVOLUCAO }),
    ).toThrow();
  });

  it("corpo SEM manual_reply_silence_minutes preserva o prazo do silêncio que já valia", () => {
    const atual = { routing: { mode: "manual", manual_reply_silence_minutes: 15 } };
    const proximo = proximoSettings(atual, { mode: "round_robin", max_retries: 5, backoff_seconds: 60 });
    expect(
      (proximo.routing as { manual_reply_silence_minutes: number | null }).manual_reply_silence_minutes,
      "cliente que só conhece roteamento não pode voltar a clínica para 60 min por omissão",
    ).toBe(15);
  });

  it("manual_reply_silence_minutes: grava o valor, null volta ao padrão, fora da faixa é recusado", () => {
    const atual = { routing: { mode: "manual" } };
    const base = { mode: "manual", max_retries: 5, backoff_seconds: 60 };
    const com15 = proximoSettings(atual, { ...base, manual_reply_silence_minutes: 15 });
    expect((com15.routing as { manual_reply_silence_minutes: unknown }).manual_reply_silence_minutes).toBe(15);
    const nulo = proximoSettings(com15, { ...base, manual_reply_silence_minutes: null });
    expect((nulo.routing as { manual_reply_silence_minutes: unknown }).manual_reply_silence_minutes).toBeNull();
    for (const fora of [4, 1441, 7.5]) {
      expect(() => atendimentoConfigPatchSchema.parse({ ...base, manual_reply_silence_minutes: fora })).toThrow();
    }
  });
});
