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

import { chanceGlobal, pontuarChance, TAXA_DA_PLATAFORMA, taxaDaEmpresaNaConta, variaveisDaChance, type DadosDaChanceGlobal } from "./chance-global.ts";

const AGORA = "2026-09-25T12:00:00.000Z";
const horasAntes = (horas: number) => new Date(Date.parse(AGORA) - horas * 3_600_000).toISOString();

type Mudancas = Omit<Partial<DadosDaChanceGlobal>, "cliente" | "empresa"> & {
  cliente?: Partial<DadosDaChanceGlobal["cliente"]>;
  empresa?: Partial<DadosDaChanceGlobal["empresa"]>;
};

function dados(mudar: Mudancas = {}): DadosDaChanceGlobal {
  const { cliente, empresa, ...resto } = mudar;
  return {
    agora: AGORA,
    criado_em: horasAntes(1),
    total: 450,
    status: "draft",
    lista_de_preco: false,
    pdv: false,
    pedido_em: null,
    cliente_generico: false,
    ...resto,
    /* Sem dizer o recente, os testes tratam o ano inteiro como recente. */
    empresa: {
      n: 0,
      g: 0,
      n_novo: 0,
      g_novo: 0,
      ...empresa,
      n_recente: empresa?.n_recente ?? empresa?.n ?? 0,
      g_recente: empresa?.g_recente ?? empresa?.g ?? 0,
    },
    cliente: {
      pedidos: 0,
      ticket_medio: null,
      ultima_compra: null,
      pedidos_no_ano: 0,
      meses_no_ano: 0,
      orcamentos: 0,
      fechados: 0,
      recentes: 0,
      mais_novo_em: null,
      ...cliente,
    },
  };
}

function aberta(entrada: DadosDaChanceGlobal) {
  const resultado = chanceGlobal(entrada);
  assert.equal(resultado.estado, "aberto");
  return resultado as Extract<ReturnType<typeof chanceGlobal>, { estado: "aberto" }>;
}

const efeito = (resultado: ReturnType<typeof aberta>, chave: string) => resultado.fatores.find((fator) => fator.chave === chave)?.efeito;

describe("chanceGlobal", () => {
  it("os pesos medidos: empresa na média, cliente novo, valor típico, recém-criado dá 39,4%", () => {
    /* Valor conferido contra o roteiro do treino (.cache/chance-global/final2.mjs). */
    assert.equal(aberta(dados()).chance.toFixed(4), "0.3937");
  });

  it("a empresa que fecha mais tem chance maior, e com poucos orçamentos a taxa puxa para a média", () => {
    const fraca = aberta(dados({ empresa: { n: 100, g: 15 } })).chance;
    const media = aberta(dados({ empresa: { n: 100, g: 42 } })).chance;
    const forte = aberta(dados({ empresa: { n: 100, g: 70 } })).chance;
    assert.ok(fraca < media && media < forte);
    assert.ok(Math.abs(taxaDaEmpresaNaConta(0, 0) - TAXA_DA_PLATAFORMA) < 1e-9);
    assert.ok(taxaDaEmpresaNaConta(1, 1) < 1, "um orçamento fechado não vira 100%");
  });

  it("o que a empresa anda fazendo agora pesa mais que a média do ano", () => {
    /* Mesmo ano (20% de 100), mas no último mês fechando 60%: a conta segue o recente. */
    const melhorando = aberta(dados({ empresa: { n: 100, g: 20, n_recente: 30, g_recente: 18 } }));
    const parada = aberta(dados({ empresa: { n: 100, g: 20, n_recente: 30, g_recente: 6 } }));
    assert.ok(melhorando.chance > parada.chance);
    assert.equal(melhorando.empresa.taxaRecente, 0.6);
    assert.equal(melhorando.empresa.taxa, 0.2);
    assert.ok(melhorando.fatores.find((fator) => fator.chave === "empresa")?.titulo.includes("60%"));
  });

  it("a referência é um orçamento típico da plataforma com o mesmo tempo aberto", () => {
    const resultado = aberta(dados({ empresa: { n: 100, g: 70 }, criado_em: horasAntes(72) }));
    const tipico = aberta(dados({ criado_em: horasAntes(72) }));
    assert.ok(Math.abs(resultado.referencia - tipico.chance) < 1e-9);
    assert.ok(resultado.chance > resultado.referencia);
  });

  it("cliente que já comprou e já aceitou orçamento sobe; o efeito de cada fator aparece em pontos", () => {
    const novo = aberta(dados({ empresa: { n: 80, g: 30 } }));
    const fiel = aberta(dados({ empresa: { n: 80, g: 30 }, cliente: { pedidos: 4, ticket_medio: 500, orcamentos: 3, fechados: 2 } }));
    assert.ok(fiel.chance > novo.chance);
    assert.equal(fiel.cliente.tipo, "voltou");
    assert.ok((efeito(fiel, "cliente") ?? 0) > 0);
    assert.ok((efeito(fiel, "fechou-antes") ?? 0) > 0);
    /* Cliente novo aparece como informação, sem peso próprio. */
    assert.equal(efeito(novo, "cliente"), null);
  });

  it("cliente genérico (Consumidor final, cliente pendente) cai muito e não herda o histórico", () => {
    const generico = aberta(dados({ cliente_generico: true, empresa: { n: 80, g: 30 }, cliente: { pedidos: 900, ticket_medio: 80, fechados: 40, orcamentos: 60 } }));
    const comum = aberta(dados({ empresa: { n: 80, g: 30 } }));
    assert.equal(generico.cliente.tipo, "generico");
    assert.ok(generico.chance < comum.chance / 2);
    const v = variaveisDaChance(dados({ cliente_generico: true, cliente: { pedidos: 900, fechados: 40, mais_novo_em: AGORA } }));
    assert.equal(v.comprou + v.fechouOrcamento + v.maisNovo + v.ticketAcima, 0);
  });

  it("um orçamento mais novo para o mesmo cliente derruba o antigo", () => {
    const sozinho = aberta(dados({ criado_em: horasAntes(48) }));
    const refeito = aberta(dados({ criado_em: horasAntes(48), cliente: { mais_novo_em: horasAntes(2) } }));
    assert.ok(refeito.chance < sozinho.chance);
    assert.ok((efeito(refeito, "mais-novo") ?? 0) < 0);
  });

  it("valor muito acima do ticket médio do cliente pesa contra; valor maior que o típico também", () => {
    const perto = aberta(dados({ total: 900, cliente: { pedidos: 2, ticket_medio: 800 } }));
    const longe = aberta(dados({ total: 9000, cliente: { pedidos: 2, ticket_medio: 800 } }));
    assert.ok(longe.chance < perto.chance);
    assert.ok((efeito(longe, "ticket") ?? 0) < 0);
    assert.ok(aberta(dados({ total: 5000 })).chance < aberta(dados({ total: 100 })).chance);
  });

  it("o tempo derruba a chance: amanhã e em uma semana ficam abaixo de hoje", () => {
    const hoje = aberta(dados({ criado_em: horasAntes(30) }));
    assert.ok(hoje.amanha < hoje.chance && hoje.emUmaSemana < hoje.amanha && hoje.emUmMes < hoje.emUmaSemana);
    assert.ok(hoje.chance < hoje.naCriacao);
    const velho = aberta(dados({ criado_em: horasAntes(24 * 40) }));
    assert.ok(velho.chance < 0.1);
    /* Menos de 1 hora conta como 1 hora: o marco mais cedo que o modelo mediu. */
    assert.equal(pontuarChance(variaveisDaChance(dados()), 0.1), pontuarChance(variaveisDaChance(dados()), 1));
  });

  it("orçamento fechado, recusado, aprovado, do PDV ou de lista de preços não tem conta de chance", () => {
    assert.equal(chanceGlobal(dados({ pedido_em: AGORA })).estado, "fechado");
    /* Aprovado no painel sem pedido ainda não fechou; com pedido, fechou. */
    assert.equal(chanceGlobal(dados({ status: "approved" })).estado, "aprovado");
    assert.equal(chanceGlobal(dados({ status: "approved", pedido_em: AGORA })).estado, "fechado");
    assert.equal(chanceGlobal(dados({ status: "rejected" })).estado, "recusado");
    assert.equal(chanceGlobal(dados({ lista_de_preco: true })).estado, "sem-conta");
    assert.equal(chanceGlobal(dados({ pdv: true })).estado, "sem-conta");
    assert.equal(chanceGlobal(null).estado, "sem-conta");
  });

  it("precisão pelo tamanho do histórico da empresa", () => {
    assert.equal(aberta(dados({ empresa: { n: 5, g: 2 } })).precisao, "baixa");
    assert.equal(aberta(dados({ empresa: { n: 30, g: 10 } })).precisao, "media");
    assert.equal(aberta(dados({ empresa: { n: 120, g: 50 } })).precisao, "alta");
  });

  it("recorrência e orçamentos sem fechar aparecem como fato, sem peso", () => {
    const resultado = aberta(dados({ cliente: { pedidos: 3, meses_no_ano: 2, pedidos_no_ano: 3, orcamentos: 0 } }));
    assert.ok(resultado.semPeso.some((fato) => fato.titulo === "Recorrência"));
    const semFechar = aberta(dados({ cliente: { orcamentos: 3, fechados: 0 } }));
    assert.ok(semFechar.semPeso.some((fato) => fato.detalhe.includes("não fechou nenhum")));
  });
});
