import { test } from "node:test";
import assert from "node:assert/strict";

import { gerarChave, hashDaChave, lerEscopos, lerNome, lerValidade, pareceChave, situacaoDaChave, temEscopo } from "./chave.ts";
import { aceitaChave, CHAVE_DE_EXEMPLO, comoConectar, comoConectarComChave, PROGRAMAS, PROGRAMAS_DA_CHAVE } from "./como-conectar.ts";
import { caminhoDaEscrita, canonico, conferirCodigo, emitirCodigo, marcaDaPrevia } from "./confirmacao.ts";
import { lerArgumentos, paraJsonSchema, type Campos } from "./esquema.ts";
import { criarGuardaDeSessoes, type SessaoGuardada } from "./guarda-de-sessoes.ts";
import { assistentesLiberados } from "./liberacao.ts";
import { clienteDoInitialize, tratarCorpo, tratarMensagem, type Servidor } from "./protocolo.ts";
import { criarFreio, criarLembranca, enderecoDeQuemChama, faixaDoEndereco } from "./tentativas.ts";

/* ------------------------------------------------------------------ chave */

test("a chave nasce com o prefixo, o tamanho certo e um hash que confere", () => {
  const a = gerarChave();
  const b = gerarChave();
  assert.ok(a.chave.startsWith("ktl_"));
  assert.equal(a.chave.length, 47);
  assert.ok(pareceChave(a.chave));
  assert.notEqual(a.chave, b.chave, "duas chaves nunca são iguais");
  assert.equal(a.hash, hashDaChave(a.chave));
  assert.equal(a.hash.length, 64);
  assert.equal(a.prefixo, a.chave.slice(0, 12));
  assert.ok(!a.hash.includes(a.chave.slice(4)), "o hash não carrega o segredo");
});

test("o que não tem a forma de uma chave nem vai ao banco", () => {
  for (const lixo of ["", "ktl_", "ktl_curta", "sk_live_abc", `ktl_${"a".repeat(42)}`, `ktl_${"a".repeat(44)}`, `ktl_${"a".repeat(42)}!`, "Bearer ktl_x"]) {
    assert.equal(pareceChave(lixo), false, `"${lixo}" não é chave`);
  }
});

test("escopos: leitura entra sempre, e nome desconhecido é recusado", () => {
  assert.deepEqual(lerEscopos("leitura"), ["leitura"]);
  assert.deepEqual(lerEscopos("escrita"), ["leitura", "escrita"]);
  assert.deepEqual(lerEscopos(["escrita"]), ["leitura", "escrita"]);
  assert.deepEqual(lerEscopos(["leitura", "escrita"]), ["leitura", "escrita"]);
  assert.deepEqual(lerEscopos([]), ["leitura"]);
  assert.equal(lerEscopos(["admin"]), null);
  assert.equal(lerEscopos("tudo"), null);
  assert.equal(lerEscopos(undefined), null);
  assert.equal(temEscopo(["leitura"], "escrita"), false);
  assert.equal(temEscopo(["leitura", "escrita"], "escrita"), true);
  assert.equal(temEscopo(null, "leitura"), false);
});

test("nome e validade da chave", () => {
  assert.deepEqual(lerNome("  Claude   do notebook "), { ok: true, nome: "Claude do notebook" });
  assert.equal(lerNome("").ok, false);
  assert.equal(lerNome("x".repeat(81)).ok, false);

  const agora = new Date("2026-10-01T12:00:00Z");
  assert.deepEqual(lerValidade(null, agora), { ok: true, expiraEm: null });
  assert.deepEqual(lerValidade("sem", agora), { ok: true, expiraEm: null });
  assert.deepEqual(lerValidade(30, agora), { ok: true, expiraEm: "2026-10-31T12:00:00.000Z" });
  assert.deepEqual(lerValidade("365", agora), { ok: true, expiraEm: "2027-10-01T12:00:00.000Z" });
  assert.equal(lerValidade(7, agora).ok, false, "só as validades que a tela oferece");
  assert.equal(lerValidade(-1, agora).ok, false);
});

test("situação da chave: revogada vence vencida, e o instante do vencimento já não vale", () => {
  const agora = Date.parse("2026-10-01T12:00:00Z");
  assert.equal(situacaoDaChave({}, agora), "ativa");
  assert.equal(situacaoDaChave({ expira_em: "2026-10-02T00:00:00Z" }, agora), "ativa");
  assert.equal(situacaoDaChave({ expira_em: "2026-10-01T12:00:00Z" }, agora), "vencida");
  assert.equal(situacaoDaChave({ expira_em: "2026-09-01T00:00:00Z" }, agora), "vencida");
  assert.equal(situacaoDaChave({ revogada_em: "2026-09-30T00:00:00Z", expira_em: "2026-09-01T00:00:00Z" }, agora), "revogada");
});

/* --------------------------------------------------------------- liberação */

test("liberação: fechado por padrão, lista de ids, ou todas", () => {
  assert.equal(assistentesLiberados(["a", "b"], undefined), false);
  assert.equal(assistentesLiberados(["a", "b"], ""), false);
  assert.equal(assistentesLiberados(["a", "b"], "todas"), true);
  assert.equal(assistentesLiberados([], "todas"), true);
  assert.equal(assistentesLiberados(["a", null], "x, A ,y"), true, "ignora maiúscula e espaço");
  assert.equal(assistentesLiberados([null, "b"], "x;b"), true);
  assert.equal(assistentesLiberados(["a", "b"], "c,d"), false);
  assert.equal(assistentesLiberados([null, undefined], "c,d"), false);
});

/* ------------------------------------------------------------------ esquema */

const CAMPOS: Campos = {
  cliente_id: { tipo: "id", descricao: "Id do cliente.", obrigatorio: true },
  titulo: { tipo: "texto", descricao: "Título.", max: 20 },
  quantidade: { tipo: "numero", descricao: "Quantidade.", min: 0.001 },
  paginas: { tipo: "inteiro", descricao: "Páginas.", min: 1, max: 10 },
  urgente: { tipo: "booleano", descricao: "Urgente." },
  situacao: { tipo: "opcao", descricao: "Situação.", opcoes: ["rascunho", "enviado"] },
  dia: { tipo: "data", descricao: "Dia." },
  hora: { tipo: "hora", descricao: "Hora." },
  itens: {
    tipo: "lista",
    descricao: "Itens.",
    minItens: 1,
    maxItens: 2,
    itens: { titulo: { tipo: "texto", descricao: "Nome do item.", obrigatorio: true }, preco: { tipo: "numero", descricao: "Preço.", min: 0 } },
  },
};

const ID = "609a0082-1f55-4a79-a76a-cc0017a4ea23";

test("esquema: o JSON Schema diz o que é obrigatório e fecha campos extras", () => {
  const esquema = paraJsonSchema(CAMPOS) as { required: string[]; additionalProperties: boolean; properties: Record<string, Record<string, unknown>> };
  assert.deepEqual(esquema.required, ["cliente_id"]);
  assert.equal(esquema.additionalProperties, false);
  assert.equal(esquema.properties.cliente_id.format, "uuid");
  assert.deepEqual(esquema.properties.situacao.enum, ["rascunho", "enviado"]);
  assert.equal(esquema.properties.paginas.type, "integer");
  assert.equal((esquema.properties.itens.items as { type: string }).type, "object");
  assert.deepEqual(paraJsonSchema({}), { type: "object", properties: {}, additionalProperties: false });
});

test("esquema: lê, normaliza e devolve só o que veio, na ordem declarada", () => {
  const lido = lerArgumentos(CAMPOS, {
    itens: [{ titulo: " Caneca ", preco: "12,50" }],
    situacao: "ENVIADO",
    urgente: "sim",
    paginas: "3",
    quantidade: 2.5,
    titulo: "  Evento  ",
    cliente_id: ID.toUpperCase(),
    dia: "2026-12-24",
    hora: "09:30:00",
  });
  assert.ok(lido.ok);
  if (!lido.ok) return;
  assert.deepEqual(Object.keys(lido.valor), ["cliente_id", "titulo", "quantidade", "paginas", "urgente", "situacao", "dia", "hora", "itens"]);
  assert.deepEqual(lido.valor, {
    cliente_id: ID,
    titulo: "Evento",
    quantidade: 2.5,
    paginas: 3,
    urgente: true,
    situacao: "enviado",
    dia: "2026-12-24",
    hora: "09:30",
    itens: [{ titulo: "Caneca", preco: 12.5 }],
  });
});

test("esquema: campo vazio é campo ausente, e ausente obrigatório é erro", () => {
  const so = lerArgumentos(CAMPOS, { cliente_id: ID, titulo: "  ", dia: null });
  assert.deepEqual(so, { ok: true, valor: { cliente_id: ID } });
  const falta = lerArgumentos(CAMPOS, {});
  assert.equal(falta.ok, false);
  if (!falta.ok) assert.match(falta.erro, /cliente_id/);
  assert.deepEqual(lerArgumentos({}, undefined), { ok: true, valor: {} });
});

test("esquema: recusa o que não confere, dizendo o campo", () => {
  const erroDe = (extra: Record<string, unknown>) => {
    const lido = lerArgumentos(CAMPOS, { cliente_id: ID, ...extra });
    assert.equal(lido.ok, false, JSON.stringify(extra));
    return lido.ok ? "" : lido.erro;
  };
  assert.match(erroDe({ cliente: "x" }), /Campo desconhecido: "cliente".*cliente_id/);
  assert.match(erroDe({ cliente_id: "275" }), /cliente_id/);
  assert.match(erroDe({ titulo: "x".repeat(21) }), /titulo/);
  assert.match(erroDe({ quantidade: "muitas" }), /quantidade/);
  assert.match(erroDe({ quantidade: 0 }), /quantidade/);
  assert.match(erroDe({ paginas: 2.5 }), /inteiro/);
  assert.match(erroDe({ paginas: 11 }), /paginas/);
  assert.match(erroDe({ urgente: "talvez" }), /urgente/);
  assert.match(erroDe({ situacao: "aprovado" }), /rascunho, enviado/);
  assert.match(erroDe({ dia: "24/12/2026" }), /dia/);
  assert.match(erroDe({ dia: "2026-02-30" }), /dia/, "data que não existe");
  assert.match(erroDe({ hora: "25:00" }), /hora/);
  assert.match(erroDe({ itens: "caneca" }), /lista/);
  assert.match(erroDe({ itens: [] }), /ao menos 1/);
  assert.match(erroDe({ itens: [{ titulo: "a" }, { titulo: "b" }, { titulo: "c" }] }), /no máximo 2/);
  assert.match(erroDe({ itens: [{ titulo: "a" }, { preco: 3 }] }), /itens\[2\].*titulo/);
  assert.match(erroDe({ itens: [{ titulo: "a", valor: 3 }] }), /itens\[1\].*Campo desconhecido/);
  assert.equal(lerArgumentos(CAMPOS, "texto").ok, false);
  assert.equal(lerArgumentos(CAMPOS, [1, 2]).ok, false);
});

/* -------------------------------------------------------------- confirmação */

const ALVO = { segredo: "segredo-do-servidor", chaveId: "chave-1", ferramenta: "criar_orcamento" };

test("canônico: a ordem das chaves não muda o texto, a dos itens muda", () => {
  assert.equal(canonico({ b: 1, a: [{ d: 2, c: 1 }] }), canonico({ a: [{ c: 1, d: 2 }], b: 1 }));
  assert.notEqual(canonico({ a: [1, 2] }), canonico({ a: [2, 1] }));
  assert.equal(canonico({ a: undefined, b: null }), '{"b":null}');
  assert.equal(canonico(10), canonico(10.0));
});

test("confirmação: vale para os mesmos dados, na mesma chave e ferramenta, dentro do prazo", () => {
  const agora = Date.parse("2026-10-01T12:00:00Z");
  const dados = { cliente_id: ID, itens: [{ titulo: "Caneca", quantidade: 100, preco_unitario: 12.5 }] };
  const previa = { cliente: "Maria", total: 1250 };
  const { codigo, valeAte } = emitirCodigo({ ...ALVO, argumentos: dados, previa, agora });
  assert.equal(valeAte, "2026-10-01T12:10:00.000Z");

  const conferir = (mudanca: Record<string, unknown>, quando = agora + 60_000) =>
    conferirCodigo({ ...ALVO, argumentos: dados, codigo, agora: quando, ...mudanca }).veredito;

  assert.equal(conferir({}), "ok");
  assert.equal(conferir({ argumentos: { itens: dados.itens, cliente_id: ID } }), "ok", "reordenar o JSON não invalida");
  assert.equal(conferir({ argumentos: { ...dados, itens: [{ ...dados.itens[0], preco_unitario: 1 }] } }), "invalido", "mudou o preço");
  assert.equal(conferir({ argumentos: { ...dados, desconto: 5 } }), "invalido", "campo a mais");
  assert.equal(conferir({ chaveId: "chave-2" }), "invalido", "outra chave");
  assert.equal(conferir({ ferramenta: "criar_cliente" }), "invalido", "outra ferramenta");
  assert.equal(conferir({ segredo: "outro" }), "invalido", "outro servidor");
  assert.equal(conferir({}, agora + 10 * 60_000), "vencido");
  assert.equal(conferir({ codigo: "" }), "invalido");
  assert.equal(conferir({ codigo: "abc" }), "invalido");
  assert.equal(conferir({ codigo: `${codigo}.x` }), "invalido");
  /* Esticar a validade ou trocar a marca da prévia à mão quebra a assinatura. */
  const [validade, marca, assinatura] = codigo.split(".");
  assert.equal(conferir({ codigo: `zzzzzz.${marca}.${assinatura}` }), "invalido");
  assert.equal(conferir({ codigo: `${validade}.${marcaDaPrevia({ cliente: "Maria", total: 1 })}.${assinatura}` }), "invalido");
});

test("confirmação: o código carrega a marca da prévia, e ela muda se o que foi mostrado mudar", () => {
  const agora = Date.parse("2026-10-01T12:00:00Z");
  const dados = { cliente_id: ID, itens: [{ produto_id: ID, quantidade: 100 }] };
  const previa = { cliente: "Maria", itens: [{ titulo: "Caneca", preco_unitario: 12.5, total: 1250 }], total: 1250 };
  const { codigo } = emitirCodigo({ ...ALVO, argumentos: dados, previa, agora });

  const conferido = conferirCodigo({ ...ALVO, argumentos: dados, codigo, agora: agora + 1000 });
  /* A checagem abaixo também diz ao TypeScript que este é o caso com marca. */
  assert.ok(conferido.veredito === "ok");
  assert.equal(conferido.venceEm, agora + 10 * 60_000);
  assert.equal(conferido.marca, marcaDaPrevia(previa), "a mesma prévia tem a mesma marca");
  assert.equal(conferido.marca, marcaDaPrevia({ total: 1250, itens: previa.itens, cliente: "Maria" }), "a ordem das chaves não muda a marca");

  /* O preço do cadastro subiu entre a prévia e a confirmação: os dados enviados
     são os mesmos (o código confere), mas a prévia de agora é outra. É por essa
     diferença que a gravação é recusada. */
  const previaDeAgora = { ...previa, itens: [{ titulo: "Caneca", preco_unitario: 14, total: 1400 }], total: 1400 };
  assert.notEqual(conferido.marca, marcaDaPrevia(previaDeAgora));
});

test("caminho da escrita: o sensível pelo resultado vira prévia, e quem traz código vai à conferência", () => {
  const base = { sempre: false, sensivelPelosDados: false, temCodigo: false };
  assert.equal(caminhoDaEscrita(base), "gravar", "o comum grava direto");
  assert.equal(caminhoDaEscrita({ ...base, sensivelPeloResultado: false }), "gravar");
  assert.equal(caminhoDaEscrita({ ...base, sensivelPeloResultado: true }), "previa", "um cupom que fica grande pede prévia");
  assert.equal(caminhoDaEscrita({ ...base, sensivelPelosDados: true }), "previa");
  assert.equal(caminhoDaEscrita({ ...base, sempre: true }), "previa", "a empresa escolheu sempre");
  /* A segunda chamada, com o código da prévia que o resultado pediu: não
     grava direto, passa pela conferência do código e da marca. */
  assert.equal(caminhoDaEscrita({ ...base, temCodigo: true }), "conferir");
  assert.equal(caminhoDaEscrita({ ...base, temCodigo: true, sensivelPeloResultado: true }), "conferir");
  assert.equal(caminhoDaEscrita({ sempre: true, sensivelPelosDados: true, temCodigo: true }), "conferir");
});

/* ---------------------------------------------------------------- protocolo */

function servidorDeTeste(): Servidor & { chamadas: Array<{ nome: string; argumentos: unknown }> } {
  const chamadas: Array<{ nome: string; argumentos: unknown }> = [];
  return {
    chamadas,
    nome: "ktalogu",
    titulo: "Ktalogu",
    versao: "1.0.0",
    instrucoes: "Instruções.",
    ferramentas: () => [{ name: "ver_empresa", description: "Mostra a empresa.", inputSchema: { type: "object", properties: {} } }],
    chamar: async (nome, argumentos) => {
      chamadas.push({ nome, argumentos });
      if (nome === "ver_empresa") return { texto: '{"nome":"Aurora"}' };
      if (nome === "falha") return { texto: "Sem permissão.", erro: true };
      if (nome === "explode") throw new Error("banco fora do ar");
      return null;
    },
  };
}

test("protocolo: initialize devolve a versão pedida quando a conhece, e a padrão quando não", async () => {
  const servidor = servidorDeTeste();
  const pedir = (versao: string) =>
    tratarMensagem({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: versao, capabilities: {}, clientInfo: { name: "claude-code", version: "2" } } }, servidor);

  for (const versao of ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"]) {
    const resposta = await pedir(versao);
    assert.equal((resposta?.result as { protocolVersion: string }).protocolVersion, versao);
  }
  const desconhecida = await pedir("2030-01-01");
  const resultado = desconhecida?.result as { protocolVersion: string; capabilities: unknown; serverInfo: { name: string }; instructions: string };
  assert.equal(resultado.protocolVersion, "2025-06-18");
  assert.deepEqual(resultado.capabilities, { tools: { listChanged: false } });
  assert.equal(resultado.serverInfo.name, "ktalogu");
  assert.equal(resultado.instructions, "Instruções.");
});

test("protocolo: ping, lista e chamada de ferramenta", async () => {
  const servidor = servidorDeTeste();
  assert.deepEqual(await tratarMensagem({ jsonrpc: "2.0", id: "a", method: "ping" }, servidor), { jsonrpc: "2.0", id: "a", result: {} });

  const lista = await tratarMensagem({ jsonrpc: "2.0", id: 2, method: "tools/list" }, servidor);
  assert.equal((lista?.result as { tools: Array<{ name: string }> }).tools[0].name, "ver_empresa");

  const chamada = await tratarMensagem({ jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "ver_empresa", arguments: { a: 1 } } }, servidor);
  assert.deepEqual(chamada?.result, { content: [{ type: "text", text: '{"nome":"Aurora"}' }], isError: false });
  assert.deepEqual(servidor.chamadas, [{ nome: "ver_empresa", argumentos: { a: 1 } }]);

  /* Erro da ferramenta é resultado com isError, não erro de protocolo: é para o assistente ler. */
  const recusada = await tratarMensagem({ jsonrpc: "2.0", id: 4, method: "tools/call", params: { name: "falha" } }, servidor);
  assert.deepEqual(recusada?.result, { content: [{ type: "text", text: "Sem permissão." }], isError: true });
});

test("protocolo: o que não existe e o que quebra viram erro JSON-RPC, sem vazar detalhe", async () => {
  const servidor = servidorDeTeste();
  const codigo = async (mensagem: unknown) => (await tratarMensagem(mensagem, servidor))?.error?.code;

  assert.equal(await codigo({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "apagar_tudo" } }), -32602);
  assert.equal(await codigo({ jsonrpc: "2.0", id: 1, method: "tools/call", params: {} }), -32602);
  assert.equal(await codigo({ jsonrpc: "2.0", id: 1, method: "resources/list" }), -32601);
  assert.equal(await codigo({ id: 1, method: "ping" }), -32600);
  assert.equal(await codigo("texto"), -32600);
  assert.equal(await codigo({ jsonrpc: "2.0", id: 1 }), -32600);

  const quebrou = await tratarMensagem({ jsonrpc: "2.0", id: 9, method: "tools/call", params: { name: "explode" } }, servidor);
  assert.equal(quebrou?.error?.code, -32603);
  assert.ok(!JSON.stringify(quebrou).includes("banco fora do ar"));
});

test("protocolo: notificação não tem resposta, e lote só de notificações vira 202", async () => {
  const servidor = servidorDeTeste();
  assert.equal(await tratarMensagem({ jsonrpc: "2.0", method: "notifications/initialized" }, servidor), null);
  assert.equal(await tratarMensagem({ jsonrpc: "2.0", method: "qualquer/coisa" }, servidor), null);
  assert.equal(await tratarMensagem({ jsonrpc: "2.0", id: 7, result: {} }, servidor), null, "resposta do cliente é ignorada");

  assert.deepEqual(await tratarCorpo([{ jsonrpc: "2.0", method: "notifications/initialized" }], servidor), { corpo: null });
  const lote = await tratarCorpo(
    [
      { jsonrpc: "2.0", id: 1, method: "ping" },
      { jsonrpc: "2.0", method: "notifications/initialized" },
      { jsonrpc: "2.0", id: 2, method: "ping" },
    ],
    servidor
  );
  assert.equal(Array.isArray(lote.corpo) && lote.corpo.length, 2);
  assert.equal((await tratarCorpo([], servidor)).corpo && !Array.isArray((await tratarCorpo([], servidor)).corpo), true);
  assert.equal(((await tratarCorpo(new Array(21).fill({ jsonrpc: "2.0", id: 1, method: "ping" }), servidor)).corpo as { error?: { code: number } }).error?.code, -32600);
});

test("protocolo: um lote executa no máximo um comando, para o limite de uso valer", async () => {
  const servidor = servidorDeTeste();
  const comando = (id: number) => ({ jsonrpc: "2.0", id, method: "tools/call", params: { name: "ver_empresa", arguments: {} } });

  const dois = await tratarCorpo([comando(1), comando(2)], servidor);
  assert.equal((dois.corpo as { error?: { code: number } }).error?.code, -32600);
  assert.equal(servidor.chamadas.length, 0, "nenhum dos dois foi executado");

  const um = await tratarCorpo([{ jsonrpc: "2.0", id: 1, method: "ping" }, comando(2), { jsonrpc: "2.0", method: "notifications/initialized" }], servidor);
  assert.equal(Array.isArray(um.corpo) && um.corpo.length, 2, "um comando junto de outras mensagens segue valendo");
  assert.equal(servidor.chamadas.length, 1);
});

test("protocolo: o nome do programa sai do initialize", () => {
  const inicio = { jsonrpc: "2.0", id: 1, method: "initialize", params: { clientInfo: { name: "  claude-code  ", version: "2.0" } } };
  assert.equal(clienteDoInitialize(inicio), "claude-code");
  assert.equal(clienteDoInitialize([{ jsonrpc: "2.0", method: "notifications/initialized" }, inicio]), "claude-code");
  assert.equal(clienteDoInitialize({ jsonrpc: "2.0", id: 1, method: "tools/list" }), null);
  assert.equal(clienteDoInitialize({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} }), null);
  assert.equal(clienteDoInitialize(null), null);
});

/* -------------------------------------------------------- guarda de sessões */

function guardaDeTeste(opcoes: { renovaComo?: (refreshToken: string) => SessaoGuardada | null } = {}) {
  const relogio = { agora: 0 };
  const chamadas = { criar: 0, renovar: [] as string[] };
  const HORA = 60 * 60 * 1000;
  const guarda = criarGuardaDeSessoes({
    agora: () => relogio.agora,
    criar: async (usuario) => {
      chamadas.criar += 1;
      return { accessToken: `acesso-${chamadas.criar}-de-${usuario.id}`, refreshToken: `renovacao-${chamadas.criar}`, venceEm: relogio.agora + HORA };
    },
    renovar: async (refreshToken) => {
      chamadas.renovar.push(refreshToken);
      if (opcoes.renovaComo) return opcoes.renovaComo(refreshToken);
      return { accessToken: `renovado-${chamadas.renovar.length}`, refreshToken: `renovacao-r${chamadas.renovar.length}`, venceEm: relogio.agora + HORA };
    },
  });
  return { guarda, relogio, chamadas, HORA };
}

test("sessão: uma conversa inteira gasta uma sessão só", async () => {
  const { guarda, relogio, chamadas } = guardaDeTeste();
  const maria = { id: "u1" };
  const primeira = await guarda.obter(maria);
  relogio.agora += 30 * 60 * 1000;
  assert.equal(await guarda.obter(maria), primeira, "meia hora depois, a mesma");
  assert.equal(chamadas.criar, 1);
  assert.deepEqual(chamadas.renovar, []);
  assert.notEqual(await guarda.obter({ id: "u2" }), primeira, "cada pessoa tem a sua");
  assert.equal(chamadas.criar, 2);
});

test("sessão: perto de vencer, renova a mesma em vez de gerar outro link de entrada", async () => {
  const { guarda, relogio, chamadas, HORA } = guardaDeTeste();
  const maria = { id: "u1" };
  await guarda.obter(maria);
  relogio.agora += HORA - 4 * 60 * 1000;
  assert.equal(await guarda.obter(maria), "renovado-1", "com menos de cinco minutos de folga, renova");
  assert.equal(chamadas.criar, 1, "sem link novo");
  assert.deepEqual(chamadas.renovar, ["renovacao-1"]);
  relogio.agora += HORA;
  assert.equal(await guarda.obter(maria), "renovado-2", "e já vencida, também");
  assert.deepEqual(chamadas.renovar, ["renovacao-1", "renovacao-r1"], "cada renovação usa o token que a anterior devolveu");
  assert.equal(chamadas.criar, 1);
});

test("sessão: se a antiga não renova mais, cria outra", async () => {
  const { guarda, relogio, chamadas, HORA } = guardaDeTeste({ renovaComo: () => null });
  const maria = { id: "u1" };
  await guarda.obter(maria);
  relogio.agora += HORA;
  assert.equal(await guarda.obter(maria), "acesso-2-de-u1");
  assert.equal(chamadas.criar, 2);
  assert.equal(chamadas.renovar.length, 1);
});

test("sessão: pedidos simultâneos esperam a mesma criação", async () => {
  const { guarda, chamadas } = guardaDeTeste();
  const maria = { id: "u1" };
  const tokens = await Promise.all([guarda.obter(maria), guarda.obter(maria), guarda.obter(maria)]);
  assert.equal(new Set(tokens).size, 1);
  assert.equal(chamadas.criar, 1, "três pedidos, um link só");
});

test("sessão: falha ao criar não fica guardada, e o pedido seguinte tenta de novo", async () => {
  let tentativas = 0;
  const guarda = criarGuardaDeSessoes({
    criar: async () => {
      tentativas += 1;
      if (tentativas === 1) throw new Error("banco fora do ar");
      return { accessToken: "acesso", refreshToken: null, venceEm: Date.now() + 60 * 60 * 1000 };
    },
    renovar: async () => null,
  });
  const maria = { id: "u1" };
  await assert.rejects(guarda.obter(maria), /banco fora do ar/);
  assert.equal(await guarda.obter(maria), "acesso");
});

/* ------------------------------------------------- freio de chave errada */

test("freio: quem erra a chave em série é recusado até a janela virar", () => {
  const freio = criarFreio(3, 60_000);
  const inicio = 1_000_000;
  assert.equal(freio.bloqueado("1.2.3.4", inicio), false);
  freio.anotar("1.2.3.4", inicio);
  freio.anotar("1.2.3.4", inicio + 1_000);
  assert.equal(freio.bloqueado("1.2.3.4", inicio + 2_000), false, "dois erros ainda passam");
  freio.anotar("1.2.3.4", inicio + 2_000);
  assert.equal(freio.bloqueado("1.2.3.4", inicio + 3_000), true, "no terceiro, fecha");
  assert.equal(freio.bloqueado("5.6.7.8", inicio + 3_000), false, "outro endereço não paga pelo vizinho");
  assert.equal(freio.bloqueado("1.2.3.4", inicio + 59_999), true);
  assert.equal(freio.bloqueado("1.2.3.4", inicio + 60_000), false, "virou a janela, abre de novo");
  freio.anotar("1.2.3.4", inicio + 60_001);
  assert.equal(freio.bloqueado("1.2.3.4", inicio + 60_002), false, "a contagem recomeça do zero");
});

test("freio: sem endereço conhecido não há o que frear", () => {
  const freio = criarFreio(1, 60_000);
  freio.anotar(null);
  freio.anotar(null);
  assert.equal(freio.bloqueado(null), false);
});

test("freio: uma lembrança pode ter prazo próprio, como a da chave que estourou o limite do minuto", () => {
  const noLimite = criarLembranca(60_000);
  const inicio = 1_000_000;
  noLimite.lembrar("hash-a", inicio, 12_000);
  assert.equal(noLimite.conhecida("hash-a", inicio + 11_999), true, "até o minuto virar, recusada sem ir ao banco");
  assert.equal(noLimite.conhecida("hash-a", inicio + 12_000), false, "virou o minuto, volta a ser conferida");
});

test("freio: a chave que funcionou há pouco é lembrada por um tempo, e esquecida quando deixa de valer", () => {
  const boas = criarLembranca(10 * 60_000);
  const inicio = 1_000_000;
  assert.equal(boas.conhecida("hash-a", inicio), false);
  boas.lembrar("hash-a", inicio);
  assert.equal(boas.conhecida("hash-a", inicio + 9 * 60_000), true);
  assert.equal(boas.conhecida("hash-b", inicio), false, "só a que funcionou");
  assert.equal(boas.conhecida("hash-a", inicio + 10 * 60_000), false, "passado o prazo, volta a ser desconhecida");
  boas.lembrar("hash-a", inicio);
  boas.esquecer("hash-a");
  assert.equal(boas.conhecida("hash-a", inicio + 1), false, "revogada ou vencida, sai na hora");
});

test("freio: o endereço de quem chama vem do Cloudflare, ou do primeiro do encaminhamento", () => {
  const cabecalhos = (valores: Record<string, string>) => ({ get: (nome: string) => valores[nome] ?? null });
  assert.equal(enderecoDeQuemChama(cabecalhos({ "cf-connecting-ip": " 9.9.9.9 ", "x-forwarded-for": "1.1.1.1, 2.2.2.2" })), "9.9.9.9");
  assert.equal(enderecoDeQuemChama(cabecalhos({ "x-forwarded-for": "1.1.1.1, 2.2.2.2" })), "1.1.1.1");
  assert.equal(enderecoDeQuemChama(cabecalhos({})), null);
  assert.equal(enderecoDeQuemChama(cabecalhos({ "x-forwarded-for": "x".repeat(500) }))?.length, 64, "um cabeçalho gigante não vira chave gigante");
});

/* ---------------------------------------------------------- como conectar */

test("como conectar com chave: todo programa recebe o endereço e a chave, uma vez cada", () => {
  const endereco = "https://mcp.ktalogu.com.br/mcp";
  const { chave } = gerarChave();
  for (const { id } of PROGRAMAS_DA_CHAVE) {
    const { trecho, passos } = comoConectarComChave(id, endereco, chave);
    assert.equal(trecho.split(endereco).length - 1, 1, `${id}: o endereço aparece uma vez`);
    assert.equal(trecho.split(chave).length - 1, 1, `${id}: a chave aparece uma vez`);
    assert.ok(trecho.includes(`Bearer ${chave}`), `${id}: a chave vai como Bearer`);
    assert.ok(passos.length >= 2, `${id}: tem passo a passo`);
    assert.ok(!passos.join(" ").includes(chave), `${id}: a chave não vaza para o texto dos passos`);

    const { outroJeito } = comoConectarComChave(id, endereco, chave);
    if (outroJeito?.trecho) {
      assert.equal(outroJeito.trecho.split(endereco).length - 1, 1, `${id}, outro jeito: o endereço aparece uma vez`);
      assert.equal(outroJeito.trecho.split(chave).length - 1, 1, `${id}, outro jeito: a chave aparece uma vez`);
      assert.ok(!`${outroJeito.chamada} ${outroJeito.passos.join(" ")}`.includes(chave), `${id}, outro jeito: a chave não vaza para o texto`);
    }
  }
});

test("como conectar com chave: sem chave em mãos, o trecho mostra onde ela entra", () => {
  for (const { id } of PROGRAMAS_DA_CHAVE) {
    assert.ok(comoConectarComChave(id, "https://mcp.ktalogu.com.br/mcp").trecho.includes(`Bearer ${CHAVE_DE_EXEMPLO}`));
  }
  assert.deepEqual(
    PROGRAMAS_DA_CHAVE.map(({ id }) => id),
    PROGRAMAS.map(({ id }) => id).filter(aceitaChave),
    "a lista da chave é a do botão sem o Claude e o ChatGPT, que não aceitam chave"
  );
  assert.equal(aceitaChave("claude"), false);
  assert.equal(aceitaChave("chatgpt"), false);
});

test("como conectar com chave: cada trecho tem o formato que o programa lê", () => {
  const endereco = "https://mcp.ktalogu.com.br/mcp";
  const { chave } = gerarChave();

  const claude = comoConectarComChave("claude-code", endereco, chave).trecho;
  assert.equal(claude, `claude mcp add --transport http --scope user ktalogu ${endereco} --header "Authorization: Bearer ${chave}"`);
  assert.ok(!claude.includes("\n"), "o comando do terminal é uma linha só");

  /* Quem usa o Claude Code pelo aplicativo não tem o comando: entra pelo .mcp.json. */
  const peloArquivo = JSON.parse(comoConectarComChave("claude-code", endereco, chave).outroJeito?.trecho ?? "null");
  assert.deepEqual(peloArquivo, { mcpServers: { ktalogu: { type: "http", url: endereco, headers: { Authorization: `Bearer ${chave}` } } } });

  const codex = comoConectarComChave("codex", endereco, chave).trecho.split("\n");
  assert.deepEqual(codex, ["[mcp_servers.ktalogu]", `url = "${endereco}"`, `http_headers = { "Authorization" = "Bearer ${chave}" }`]);

  const cursor = JSON.parse(comoConectarComChave("cursor", endereco, chave).trecho);
  assert.deepEqual(cursor, { mcpServers: { ktalogu: { url: endereco, headers: { Authorization: `Bearer ${chave}` } } } });
});

test("como conectar pelo botão: o endereço aparece, e nenhuma chave em lugar nenhum", () => {
  const endereco = "https://mcp.ktalogu.com.br/mcp";
  for (const { id } of PROGRAMAS) {
    const instrucao = comoConectar(id, endereco);
    assert.equal(instrucao.trecho.split(endereco).length - 1, 1, `${id}: o endereço aparece uma vez`);
    assert.ok(instrucao.passos.length >= 3, `${id}: tem passo a passo`);
    const tudo = JSON.stringify(instrucao);
    assert.ok(!tudo.includes(CHAVE_DE_EXEMPLO) && !tudo.includes("Bearer") && !tudo.includes("Authorization"), `${id}: o botão não pede chave`);
    assert.ok(/Permitir/.test(instrucao.passos.join(" ")), `${id}: o passo a passo chega à tela de permitir`);
  }
});

test("como conectar pelo botão: cada trecho tem o formato que o programa lê", () => {
  const endereco = "https://mcp.ktalogu.com.br/mcp";

  assert.equal(comoConectar("claude", endereco).trecho, endereco, "no Claude, só o endereço");
  assert.equal(comoConectar("chatgpt", endereco).trecho, endereco, "no ChatGPT, só o endereço");
  assert.equal(comoConectar("outro", endereco).trecho, endereco);

  const claudeCode = comoConectar("claude-code", endereco).trecho;
  assert.equal(claudeCode, `claude mcp add --transport http --scope user ktalogu ${endereco}`);
  assert.ok(!claudeCode.includes("\n"), "o comando do terminal é uma linha só");

  assert.deepEqual(comoConectar("codex", endereco).trecho.split("\n"), [`codex mcp add ktalogu --url ${endereco}`, "codex mcp login ktalogu"]);
  assert.deepEqual(JSON.parse(comoConectar("cursor", endereco).trecho), { mcpServers: { ktalogu: { url: endereco } } });

  /* Os programas que aceitam chave lembram que ela existe. */
  for (const id of ["claude-code", "codex", "cursor", "outro"] as const) {
    assert.ok(comoConectar(id, endereco).outroJeito, `${id}: oferece o caminho da chave`);
  }
});

test("faixa do endereço: no IPv6 conta a faixa da casa (/64), no IPv4 o endereço", () => {
  assert.equal(faixaDoEndereco("200.10.20.30"), "200.10.20.30");
  assert.equal(faixaDoEndereco("2804:14c:5b80:8a1f:1234:5678:9abc:def0"), "2804:14c:5b80:8a1f::/64");
  assert.equal(faixaDoEndereco("2804:14c:5b80:8a1f::1"), "2804:14c:5b80:8a1f::/64", "o mesmo /64, escrito abreviado");
  assert.equal(faixaDoEndereco("2804:014c:5b80:8a1f:ffff::"), "2804:14c:5b80:8a1f::/64", "zeros à esquerda não mudam a faixa");
  assert.equal(faixaDoEndereco("2001:db8::1"), "2001:db8:0:0::/64");
  assert.equal(faixaDoEndereco("[2001:db8::1]"), "2001:db8:0:0::/64");
  assert.equal(faixaDoEndereco("::ffff:200.10.20.30"), "200.10.20.30", "IPv4 escrito dentro do IPv6 é IPv4");
  assert.equal(faixaDoEndereco("::1"), "0:0:0:0::/64");
  assert.equal(faixaDoEndereco(null), null);
  assert.notEqual(faixaDoEndereco("2804:14c:5b80:8a1f::1"), faixaDoEndereco("2804:14c:5b80:8a20::1"), "faixas vizinhas são casas diferentes");
});
