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

import {
  alvosDeClientesSemComprar,
  alvosDeContasAPagar,
  alvosDeContasAReceber,
  alvosDeEntradaNaEtapa,
  alvosDeEstoque,
  alvosDeOrcamentos,
  alvosDePedidosParados,
  conferirRegra,
  diaDoCompromisso,
  empresasDaFila,
  etapaDoPedido,
  fraseDaRegra,
  novosAlvos,
  opcoesDeEtapa,
} from "./regras.ts";

const AGORA = new Date("2026-10-14T15:00:00Z");
const diasAtras = (n: number) => new Date(AGORA.getTime() - n * 86_400_000).toISOString();
const ETAPAS = new Set(["stage-arte", "stage-impressao"]);

test("conferirRegra aceita a regra do orçamento e dá nome pela frase", () => {
  const r = conferirRegra({
    gatilho: { tipo: "orcamento_sem_resposta", parametros: { dias: 3 } },
    acoes: [{ tipo: "criar_compromisso", parametros: { titulo: "Ligar para o cliente" } }],
  });
  assert.equal(r.ok, true);
  if (!r.ok) return;
  assert.equal(r.valor.nome, "Quando um orçamento em aberto passar 3 dias sem resposta");
  assert.equal(fraseDaRegra(r.valor.gatilho, r.valor.acoes), 'Quando um orçamento em aberto passar 3 dias sem resposta, marcar um compromisso "Ligar para o cliente" na agenda para o mesmo dia.');
  assert.deepEqual(r.valor.acoes[0], {
    tipo: "criar_compromisso",
    parametros: { titulo: "Ligar para o cliente", daqui_a_dias: 0, lembrete_minutos: null, responsavel_id: null },
  });
});

test("conferirRegra recusa o que não faz sentido", () => {
  const casos: unknown[] = [
    { gatilho: { tipo: "vender_sozinho", parametros: {} }, acoes: [{ tipo: "avisar_pessoa" }] },
    { gatilho: { tipo: "orcamento_sem_resposta", parametros: { dias: 0 } }, acoes: [{ tipo: "avisar_pessoa" }] },
    { gatilho: { tipo: "orcamento_sem_resposta", parametros: { dias: 2.5 } }, acoes: [{ tipo: "avisar_pessoa" }] },
    { gatilho: { tipo: "orcamento_sem_resposta", parametros: { dias: 3 } }, acoes: [] },
    { gatilho: { tipo: "orcamento_sem_resposta", parametros: { dias: 3 } }, acoes: [{ tipo: "mandar_whatsapp" }] },
    { gatilho: { tipo: "orcamento_sem_resposta", parametros: { dias: 3 } }, acoes: [{ tipo: "avisar_pessoa" }, { tipo: "avisar_pessoa" }] },
    /* etiqueta precisa de cliente cadastrado */
    { gatilho: { tipo: "estoque_abaixo", parametros: { quantidade: 10 } }, acoes: [{ tipo: "adicionar_etiqueta_ao_cliente", parametros: { etiqueta: "x" } }] },
    { gatilho: { tipo: "conta_a_receber_vencida", parametros: { dias: 1 } }, acoes: [{ tipo: "adicionar_etiqueta_ao_cliente", parametros: { etiqueta: "x" } }] },
    { gatilho: { tipo: "cliente_sem_comprar", parametros: { dias: 90 } }, acoes: [{ tipo: "adicionar_etiqueta_ao_cliente", parametros: { etiqueta: "  " } }] },
    { gatilho: { tipo: "pedido_entrou_na_etapa", parametros: { etapa: "qualquer" } }, acoes: [{ tipo: "avisar_pessoa" }] },
    { gatilho: { tipo: "estoque_abaixo", parametros: { item_tipo: "produto" } }, acoes: [{ tipo: "avisar_pessoa" }] },
    { gatilho: { tipo: "orcamento_sem_resposta", parametros: { dias: 3 } }, acoes: [{ tipo: "criar_compromisso", parametros: { lembrete_minutos: 7 } }] },
    { gatilho: { tipo: "orcamento_sem_resposta", parametros: { dias: 3 } }, acoes: [{ tipo: "avisar_pessoa", parametros: { usuario_id: "fulano" } }] },
  ];
  for (const caso of casos) assert.equal(conferirRegra(caso).ok, false, JSON.stringify(caso));
});

test("conferirRegra: etiqueta no cliente sem comprar e estoque do insumo pelo mínimo", () => {
  const a = conferirRegra({
    nome: "Reativar",
    gatilho: { tipo: "cliente_sem_comprar", parametros: { dias: "90" } },
    acoes: [{ tipo: "adicionar_etiqueta_ao_cliente", parametros: { etiqueta: " reativar " } }],
  });
  assert.equal(a.ok && a.valor.nome, "Reativar");
  assert.equal(a.ok && a.valor.acoes[0].tipo === "adicionar_etiqueta_ao_cliente" && a.valor.acoes[0].parametros.etiqueta, "reativar");

  const b = conferirRegra({ gatilho: { tipo: "estoque_abaixo", parametros: {} }, acoes: [{ tipo: "avisar_pessoa" }] });
  assert.equal(b.ok, true);
  if (b.ok) assert.equal(fraseDaRegra(b.valor.gatilho, b.valor.acoes), "Quando o estoque de um insumo ficar abaixo do mínimo cadastrado, avisar o dono da empresa.");
});

test("frase usa os nomes das etapas, itens e pessoas", () => {
  const r = conferirRegra({
    gatilho: { tipo: "pedido_entrou_na_etapa", parametros: { etapa: "stage-impressao" } },
    acoes: [
      { tipo: "avisar_pessoa", parametros: { usuario_id: "11111111-1111-4111-8111-111111111111" } },
      { tipo: "criar_compromisso", parametros: { daqui_a_dias: 2 } },
    ],
  });
  assert.ok(r.ok);
  if (!r.ok) return;
  const frase = fraseDaRegra(r.valor.gatilho, r.valor.acoes, {
    etapas: { "stage-impressao": "Impressão" },
    pessoas: { "11111111-1111-4111-8111-111111111111": "Ana" },
  });
  assert.equal(frase, 'Quando um pedido entrar na etapa "Impressão", avisar Ana e marcar um compromisso na agenda para 2 dias depois.');
});

test("etapaDoPedido segue o quadro: etapa da empresa, coluna fixa, arquivado e cancelado", () => {
  assert.equal(etapaDoPedido({ status: "in_production", production_stage_id: "stage-impressao" }, ETAPAS), "stage-impressao");
  assert.equal(etapaDoPedido({ status: "in_production", production_stage_id: null }, ETAPAS), "status:in_production");
  assert.equal(etapaDoPedido({ status: "in_production", production_stage_id: "stage-apagada" }, ETAPAS), "status:in_production");
  assert.equal(etapaDoPedido({ status: "delivered", production_stage_id: "__kanban_archived__" }, ETAPAS), null);
  assert.equal(etapaDoPedido({ status: "canceled", production_stage_id: null }, ETAPAS), null);
});

test("opcoesDeEtapa põe as etapas da empresa logo depois da coluna fixa do mesmo status", () => {
  const lista = opcoesDeEtapa([
    { id: "stage-b", label: "Acabamento", status: "in_production", position: 2 },
    { id: "stage-a", label: "Impressão", status: "in_production", position: 1 },
    { id: "lixo", label: "x" },
  ]);
  assert.deepEqual(
    lista.map((e) => e.id),
    ["status:pending_production", "status:in_production", "stage-a", "stage-b", "status:finished", "status:delivered"]
  );
});

test("orçamento: só em aberto, dentro do prazo e da janela", () => {
  const alvos = alvosDeOrcamentos(
    [
      { id: "q1", number: 10, title: "Fachada", status: "sent", client_id: "c1", total: 100, updated_at: diasAtras(4), created_at: diasAtras(10) },
      { id: "q2", number: 11, title: null, status: "draft", client_id: null, total: 0, updated_at: diasAtras(2), created_at: diasAtras(2) },
      { id: "q3", number: 12, title: null, status: "approved", client_id: null, total: 0, updated_at: diasAtras(9), created_at: diasAtras(9) },
      { id: "q4", number: 13, title: null, status: "draft", client_id: null, total: 0, updated_at: diasAtras(400), created_at: diasAtras(400) },
    ],
    3,
    AGORA,
    { c1: { name: "João", company_name: "Padaria do João" } }
  );
  assert.deepEqual(alvos.map((a) => a.id), ["q1"]);
  assert.equal(alvos[0].titulo, "Orçamento #10 de Padaria do João sem resposta há 4 dias");
  assert.equal(alvos[0].clienteId, "c1");
  assert.match(alvos[0].chave, /^orcamento:q1:/);
});

test("pedido parado conta desde a entrada na etapa; sem etapa escolhida, só quem está sendo feito", () => {
  const pedidos = [
    { id: "p1", number: 1, status: "in_production", production_stage_id: "stage-impressao", client_id: null, production_updated_at: diasAtras(1), updated_at: diasAtras(1), created_at: diasAtras(20) },
    { id: "p2", number: 2, status: "in_production", production_stage_id: null, client_id: null, production_updated_at: diasAtras(1), updated_at: diasAtras(1), created_at: diasAtras(20) },
    { id: "p3", number: 3, status: "finished", production_stage_id: null, client_id: null, production_updated_at: diasAtras(5), updated_at: diasAtras(5), created_at: diasAtras(20) },
  ];
  const entrada = { p1: diasAtras(3) };
  const semEtapa = alvosDePedidosParados(pedidos, entrada, { dias: 2, etapa: null }, ETAPAS, AGORA, {}, { etapas: { "stage-impressao": "Impressão" } });
  assert.deepEqual(semEtapa.map((a) => a.id), ["p1"]);
  assert.equal(semEtapa[0].titulo, 'Pedido #1 parado há 3 dias em "Impressão"');

  const finalizados = alvosDePedidosParados(pedidos, entrada, { dias: 2, etapa: "status:finished" }, ETAPAS, AGORA);
  assert.deepEqual(finalizados.map((a) => a.id), ["p3"]);
});

test("entrada na etapa: só eventos depois da criação da regra e da etapa certa", () => {
  const alvos = alvosDeEntradaNaEtapa(
    [
      { id: "e1", order_id: "p1", status: "in_production", stage_id: "stage-impressao", started_at: diasAtras(0.1) },
      { id: "e2", order_id: "p1", status: "in_production", stage_id: "stage-impressao", started_at: diasAtras(3) },
      { id: "e3", order_id: "p2", status: "in_production", stage_id: null, started_at: diasAtras(0.1) },
    ],
    "stage-impressao",
    diasAtras(1),
    ETAPAS,
    { p1: { number: 7, client_id: null }, p2: { number: 8, client_id: null } }
  );
  assert.deepEqual(alvos.map((a) => a.chave), ["entrou:e1"]);
});

test("estoque: usa a quantidade da regra ou o mínimo do insumo; chave muda a cada 7 dias", () => {
  const linhas = [
    { id: "m1", nome: "Lona", unidade: "m²", atual: 8, minimo: null },
    { id: "m2", nome: "Tinta", unidade: "L", atual: 2, minimo: 5 },
    { id: "m3", nome: "Vinil", unidade: "m²", atual: 50, minimo: 10 },
  ];
  assert.deepEqual(alvosDeEstoque(linhas, "insumo", 10, AGORA).map((a) => a.id), ["m1", "m2"]);
  assert.deepEqual(alvosDeEstoque(linhas, "insumo", null, AGORA).map((a) => a.id), ["m2"]);
  const hoje = alvosDeEstoque(linhas, "insumo", 10, AGORA)[0].chave;
  const semanaQueVem = alvosDeEstoque(linhas, "insumo", 10, new Date(AGORA.getTime() + 7 * 86_400_000))[0].chave;
  assert.notEqual(hoje, semanaQueVem);
});

test("cliente sem comprar: a chave leva a data da última compra", () => {
  const [a] = alvosDeClientesSemComprar([{ cliente_id: "c1", nome: "João", empresa: null, ultima_compra: diasAtras(95) }], AGORA);
  assert.equal(a.titulo, "João está há 95 dias sem comprar");
  assert.equal(a.chave, `sem_comprar:c1:${diasAtras(95).slice(0, 10)}`);
});

test("contas: a pagar vencendo na janela, a receber vencida há N dias", () => {
  const pagar = alvosDeContasAPagar(
    [
      { id: "a", description: "Aluguel", due_date: "2026-10-16", status: "PENDING", valor: 1500, pessoa: null },
      { id: "b", description: "Luz", due_date: "2026-10-20", status: "PENDING", valor: 300, pessoa: null },
      { id: "c", description: "Água", due_date: "2026-10-15", status: "PAID", valor: 80, pessoa: null },
      { id: "d", description: "Velha", due_date: "2026-10-01", status: "PENDING", valor: 80, pessoa: null },
    ],
    2,
    "2026-10-14"
  );
  assert.deepEqual(pagar.map((c) => c.id), ["a"]);

  const receber = alvosDeContasAReceber(
    [
      { id: "r1", description: "Pedido 5", due_date: "2026-10-10", status: "OVERDUE", valor: 200, pessoa: "Maria" },
      { id: "r2", description: "Pedido 6", due_date: "2026-10-13", status: "PENDING", valor: 200, pessoa: null },
      { id: "r3", description: "Pedido 7", due_date: "2026-10-01", status: "PAID", valor: 200, pessoa: null },
    ],
    3,
    "2026-10-14"
  );
  assert.deepEqual(receber.map((c) => c.id), ["r1"]);
});

test("novosAlvos pula o que já foi feito e respeita o limite", () => {
  const base = alvosDeEstoque(
    [1, 2, 3, 4].map((n) => ({ id: `m${n}`, nome: `Item ${n}`, unidade: null, atual: 0, minimo: 1 })),
    "insumo",
    null,
    AGORA
  );
  const feitos = new Set([base[0].chave]);
  assert.deepEqual(novosAlvos(base, feitos, 2).map((a) => a.id), ["m2", "m3"]);
});

test("empresasDaFila: produção cuida de todas; desenvolvimento só com as liberadas; desligada por variável", () => {
  assert.equal(empresasDaFila({ NODE_ENV: "production" }), null);
  assert.equal(empresasDaFila({ NODE_ENV: "production", REGRAS_DESLIGADAS: "1" }), false);
  assert.equal(empresasDaFila({ NODE_ENV: "development" }), false);
  assert.equal(empresasDaFila({ NODE_ENV: "development", REGRAS_FILA_LOCAL: "1" }), false);
  assert.deepEqual(
    empresasDaFila({ NODE_ENV: "development", REGRAS_FILA_LOCAL: "1", REGRAS_LIBERADAS: "609A0082-1f55-4a79-a76a-cc0017a4ea23, lixo" }),
    ["609a0082-1f55-4a79-a76a-cc0017a4ea23"]
  );
});

test("diaDoCompromisso usa o dia do Brasil e nunca fica antes do dia do servidor", () => {
  /* 23h em Brasília = 02h UTC do dia seguinte. */
  const noite = new Date("2026-10-15T02:00:00Z");
  const dia = diaDoCompromisso(0, noite);
  assert.ok(dia >= "2026-10-14");
  assert.equal(diaDoCompromisso(2, new Date("2026-10-14T15:00:00Z")) >= "2026-10-16", true);
});
