import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { NextRequest } from "next/server";

/**
 * GET /api/v1/health — a rota é PÚBLICA e não pode publicar o endereço interno.
 *
 * O campo `target` (protocolo + host + porta) já era escondido de propósito: só
 * sai com `?verbose=1` mais o segredo interno, porque o endereço dos serviços
 * externos de uma instalação é superfície de ataque. O `error`, ao lado dele,
 * saía cru — e carrega o MESMO endereço quando o `.env` está numa das formas
 * erradas mais comuns do self-host.
 *
 * A condição de alcance existe porque, das três variáveis de endereço que a
 * rota consulta, só a do banco é validada como URL (`lib/env.ts:69`, `.url()`);
 * as outras duas (`lib/env.ts:140` e `155`) são `required()` puro. Um valor sem
 * esquema, ou com as aspas do `.env` sobrando, passa pelo Zod e explode no
 * `fetch` com o host dentro da mensagem:
 *
 *   "servico-interno.vps-do-cliente.com"
 *     -> "Failed to parse URL from servico-interno.vps-do-cliente.com"
 *
 * ESCOPO, dito em voz alta: isto vigia a SAÍDA da rota, não a validação do env.
 * Um endereço com esquema válido e host inalcançável devolve `"fetch failed"` e
 * guarda o host em `e.cause`, que a rota nunca devolveu — esse caso nunca vazou,
 * e este arquivo não o cobre porque não há o que cobrir.
 *
 * Um cuidado de forma: este arquivo exercita SÓ o caminho da fila, e não o do
 * outro serviço externo, porque nomeá-lo aqui reprovaria `lint:channels`
 * (doutrina restrição-de-canal, invariante 1 — gate obrigatório). A perda é
 * nenhuma: o vazamento é do `error` em `semAlvo()`, que é o mesmo código para os
 * três checks; um caminho basta para prová-lo, e a asserção é sobre o corpo
 * inteiro, não sobre um campo.
 *
 * Achado por @prevprocesso-maker no PR #465.
 */

/**
 * Sem esquema de propósito: é esta forma que faz o `fetch` do Node embutir o
 * endereço na mensagem, e é a forma que um `.env` mal preenchido produz.
 */
const HOST_VAZADO = "servico-interno.vps-do-cliente.com";
const SEGREDO = "segredo-interno-de-teste-com-tamanho-suficiente";

vi.mock("@/lib/env", () => ({
  env: {
    NEXT_PUBLIC_SUPABASE_URL: "https://projeto-do-cliente.supabase.co",
    SUPABASE_SERVICE_ROLE_KEY: "chave-de-teste",
    UPSTASH_REDIS_REST_URL: "servico-interno.vps-do-cliente.com",
    UPSTASH_REDIS_REST_TOKEN: "token-de-teste",
    INTERNAL_CRON_SECRET: "segredo-interno-de-teste-com-tamanho-suficiente",
    INTERNAL_SECRET: "",
  },
}));

function pedido(query = "", headers: Record<string, string> = {}): NextRequest {
  return new NextRequest(`https://crm.exemplo.com.br/api/v1/health${query}`, { headers });
}

describe("GET /api/v1/health — o endereço interno não sai para quem não tem o segredo", () => {
  beforeEach(() => vi.resetModules());

  it("não publica o host numa resposta anônima, nem pelo error", async () => {
    const { GET } = await import("./route");
    const corpo = JSON.stringify(await (await GET(pedido())).json());

    // A asserção é sobre o CORPO INTEIRO, não sobre um campo: se amanhã alguém
    // acrescentar outro lugar por onde o endereço saia, este caso reprova.
    expect(corpo).not.toContain(HOST_VAZADO);
    expect(corpo).not.toContain("Failed to parse URL");
  });

  it("mantém o diagnóstico útil — o motivo continua saindo", async () => {
    const { GET } = await import("./route");
    const { data } = await (await GET(pedido())).json();

    // Redigir não pode virar apagar: quem monitora de fora precisa seguir
    // distinguindo "não alcancei" de "fui barrado". Sem isto, a redação seria
    // uma regressão de observabilidade disfarçada de conserto.
    expect(data.checks.redis.reason).toBeTruthy();
    expect(data.checks.redis.status).toBe("down");
  });

  it("devolve o texto original a quem tem o segredo interno", async () => {
    const { GET } = await import("./route");
    const req = pedido("?verbose=1", { authorization: `Bearer ${SEGREDO}` });
    const corpo = JSON.stringify(await (await GET(req)).json());

    // O outro lado da mesma moeda: se a redação passasse a valer também no modo
    // verboso, o operador perderia o diagnóstico e ninguém notaria — o caso
    // acima ficaria verde do mesmo jeito.
    expect(corpo).toContain(HOST_VAZADO);
  });
});

describe("GET /api/v1/health — indisponibilidade do projeto Supabase", () => {
  beforeEach(() => vi.resetModules());
  afterEach(() => vi.unstubAllGlobals());

  it("522 vira projeto_indisponivel, não tempo_esgotado", async () => {
    vi.stubGlobal("fetch", () => Promise.resolve(new Response("", { status: 522 })));
    const { GET } = await import("./route");
    const { NextRequest } = await import("next/server");
    const { data } = await (await GET(new NextRequest("https://crm.exemplo.com.br/api/v1/health"))).json();
    expect(data.checks.supabase.status).toBe("down");
    expect(data.checks.supabase.reason).toBe("projeto_indisponivel");
  });

  it("PGRST002 no corpo vira projeto_indisponivel", async () => {
    vi.stubGlobal("fetch", () =>
      Promise.resolve(
        new Response(JSON.stringify({ code: "PGRST002", message: "Could not query the database for the schema cache. Retrying." }), {
          status: 503,
        }),
      ),
    );
    const { GET } = await import("./route");
    const { NextRequest } = await import("next/server");
    const { data } = await (await GET(new NextRequest("https://crm.exemplo.com.br/api/v1/health"))).json();
    expect(data.checks.supabase.status).toBe("down");
    expect(data.checks.supabase.reason).toBe("projeto_indisponivel");
  });

  it("timeout puro continua tempo_esgotado", async () => {
    vi.stubGlobal("fetch", () => new Promise(() => {}));
    const { GET } = await import("./route");
    const { NextRequest } = await import("next/server");
    const { data } = await (await GET(new NextRequest("https://crm.exemplo.com.br/api/v1/health"))).json();
    expect(data.checks.supabase.status).toBe("down");
    expect(data.checks.supabase.reason).toBe("tempo_esgotado");
  });
});

describe("GET /api/v1/health — corpo cortado no meio não vira timeout", () => {
  beforeEach(() => vi.resetModules());
  afterEach(() => vi.unstubAllGlobals());

  function corpoCortado() {
    return new ReadableStream({
      start(c) {
        c.error(Object.assign(new Error("socket hang up"), { code: "ECONNRESET" }));
      },
    });
  }

  async function checkComResposta(res: () => Response) {
    vi.stubGlobal("fetch", () => Promise.resolve(res()));
    const { GET } = await import("./route");
    const { NextRequest } = await import("next/server");
    const { data } = await (await GET(new NextRequest("https://crm.exemplo.com.br/api/v1/health"))).json();
    return data.checks.supabase;
  }

  it("200 com o corpo cortado mantém o motivo de alcance da conexão", async () => {
    const supabase = await checkComResposta(() => new Response(corpoCortado(), { status: 200 }));
    expect(supabase.status).toBe("down");
    expect(supabase.reason).toBe("host_inalcancavel");
  });

  it("500 com o corpo cortado continua sendo a resposta que chegou", async () => {
    const supabase = await checkComResposta(() => new Response(corpoCortado(), { status: 500 }));
    expect(supabase.status).toBe("down");
    expect(supabase.reason).toBe("resposta_inesperada");
  });
});
