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

import { agregar, canalDaVenda, etapaDoPedido, lerColunasDoQuadro, pedidoConta, pedidoEmAberto, situacaoDoVencimento, variacao, type Linha } from "./agregar.ts";
import { calcularMetrica, catalogoPublico, criarFonte, METRICAS, metricaPorId, MetricaSemPermissao } from "./metricas.ts";
import { casasDoTempo, diaNoBrasil, granularidadeDe, resolverPeriodo } from "./periodos.ts";
import { conferirWidget, conferirWidgets, lerWidgetsGravados, moverWidget, type Widget } from "./widgets.ts";

/* ---------------------------------------------------------------- períodos */

test("períodos: este mês compara com o mês passado até o mesmo dia", () => {
  const p = resolverPeriodo("este_mes", "2026-10-14");
  assert.deepEqual([p.de, p.ate], ["2026-10-01", "2026-10-14"]);
  assert.deepEqual(p.anterior, { de: "2026-09-01", ate: "2026-09-14" });
});

test("períodos: dia 31 encosta no fim do mês anterior", () => {
  const p = resolverPeriodo("este_mes", "2026-03-31");
  assert.deepEqual(p.anterior, { de: "2026-02-01", ate: "2026-02-28" });
});

test("períodos: mês passado inteiro, comparado ao retrasado inteiro; virada de ano", () => {
  const p = resolverPeriodo("mes_passado", "2026-01-10");
  assert.deepEqual([p.de, p.ate], ["2025-12-01", "2025-12-31"]);
  assert.deepEqual(p.anterior, { de: "2025-11-01", ate: "2025-11-30" });
});

test("períodos: hoje, 7d, 30d e este ano", () => {
  assert.deepEqual(resolverPeriodo("hoje", "2026-10-01").anterior, { de: "2026-09-30", ate: "2026-09-30" });
  const s = resolverPeriodo("7d", "2026-10-14");
  assert.deepEqual([s.de, s.ate, s.anterior.de, s.anterior.ate], ["2026-10-08", "2026-10-14", "2026-10-01", "2026-10-07"]);
  const t = resolverPeriodo("30d", "2026-10-14");
  assert.equal(t.de, "2026-09-15");
  const a = resolverPeriodo("este_ano", "2026-10-14");
  assert.deepEqual([a.de, a.anterior.de, a.anterior.ate], ["2026-01-01", "2025-01-01", "2025-10-14"]);
});

test("períodos: personalizado confere as datas e compara com a janela anterior de mesmo tamanho", () => {
  const p = resolverPeriodo("personalizado", "2026-10-14", { de: "2026-09-10", ate: "2026-09-19" });
  assert.deepEqual(p.anterior, { de: "2026-08-31", ate: "2026-09-09" });
  assert.throws(() => resolverPeriodo("personalizado", "2026-10-14", { de: "2026-09-10" }));
  assert.throws(() => resolverPeriodo("personalizado", "2026-10-14", { de: "2026-09-10", ate: "2026-09-01" }));
  assert.throws(() => resolverPeriodo("personalizado", "2026-10-14", { de: "2020-01-01", ate: "2026-01-01" }));
});

test("períodos: dia do Brasil a partir do instante em UTC", () => {
  assert.equal(diaNoBrasil("2026-10-15T02:30:00+00:00"), "2026-10-14");
  assert.equal(diaNoBrasil("2026-10-15T03:00:00Z"), "2026-10-15");
  assert.equal(diaNoBrasil("2026-10-15"), "2026-10-15");
  assert.equal(diaNoBrasil(null), null);
});

test("períodos: linha do tempo por dia até 2 meses, por mês acima", () => {
  assert.equal(granularidadeDe({ de: "2026-10-01", ate: "2026-10-14" }), "dia");
  assert.equal(casasDoTempo({ de: "2026-10-01", ate: "2026-10-03" }, "dia").length, 3);
  assert.equal(granularidadeDe({ de: "2026-01-01", ate: "2026-10-14" }), "mes");
  assert.deepEqual(casasDoTempo({ de: "2025-11-20", ate: "2026-02-03" }, "mes"), ["2025-11", "2025-12", "2026-01", "2026-02"]);
});

/* ---------------------------------------------------------------- agregar */

const venda = (dia: string, valor: number, vendedor: string, canal = "painel"): Linha => ({
  dia,
  dims: { vendedor: { chave: vendedor, rotulo: vendedor.toUpperCase() }, canal: { chave: canal, rotulo: canal } },
  num: valor,
  den: 1,
});

test("agregar: soma, razão e percentual", () => {
  const linhas = [venda("2026-10-01", 100, "ana"), venda("2026-10-02", 300, "bia")];
  assert.equal(agregar(linhas, "soma", null).valor, 400);
  assert.equal(agregar(linhas, "razao", null).valor, 200);
  const conv: Linha[] = [1, 0, 0, 1].map((n) => ({ dia: "2026-10-01", dims: {}, num: n, den: 1 }));
  assert.equal(agregar(conv, "percentual", null).valor, 50);
  assert.equal(agregar([], "razao", null).valor, 0);
});

test("agregar: por grupo, do maior para o menor, com o resto em Outros", () => {
  const linhas = [venda("2026-10-01", 50, "ana"), venda("2026-10-01", 500, "bia"), venda("2026-10-01", 20, "caio"), venda("2026-10-02", 10, "ana")];
  const { grupos } = agregar(linhas, "soma", "vendedor", { limite: 2 });
  assert.deepEqual(grupos?.map((g) => [g.rotulo, g.valor]), [["BIA", 500], ["ANA", 60], ["Outros (1)", 20]]);
  assert.equal(grupos?.reduce((s, g) => s + g.valor, 0), 580);
});

test("agregar: ticket médio por vendedor usa a razão de cada grupo", () => {
  const linhas = [venda("2026-10-01", 100, "ana"), venda("2026-10-01", 300, "ana"), venda("2026-10-01", 50, "bia")];
  const { grupos } = agregar(linhas, "razao", "vendedor");
  assert.deepEqual(grupos?.map((g) => [g.chave, g.valor]), [["ana", 200], ["bia", 50]]);
});

test("agregar: no tempo, todas as casas do período aparecem, inclusive vazias", () => {
  const linhas = [venda("2026-10-01", 10, "ana"), venda("2026-10-03", 5, "ana"), venda("2026-09-30", 999, "ana")];
  const { grupos } = agregar(linhas, "soma", "tempo", { intervalo: { de: "2026-10-01", ate: "2026-10-04" } });
  assert.deepEqual(grupos?.map((g) => [g.rotulo, g.valor]), [["01/10", 10], ["02/10", 0], ["03/10", 5], ["04/10", 0]]);
});

test("agregar: colunas extras somam junto", () => {
  const linhas: Linha[] = [
    { dia: "2026-10-01", dims: { tipo: { chave: "entradas", rotulo: "Entradas" } }, num: 100, extra: { entradas: 100, saidas: 0 } },
    { dia: "2026-10-01", dims: { tipo: { chave: "saidas", rotulo: "Saídas" } }, num: -40, extra: { entradas: 0, saidas: 40 } },
  ];
  const r = agregar(linhas, "soma", "tempo", { intervalo: { de: "2026-10-01", ate: "2026-10-01" } });
  assert.equal(r.valor, 60);
  assert.deepEqual(r.grupos?.[0].extra, { entradas: 100, saidas: 40 });
});

test("variação contra o período anterior", () => {
  assert.equal(variacao(150, 100), 50);
  assert.equal(variacao(50, 100), -50);
  assert.equal(variacao(0, 0), 0);
  assert.equal(variacao(10, 0), null);
  assert.equal(variacao(10, null), null);
});

test("canal da venda pelas marcas que o sistema já grava", () => {
  assert.equal(canalDaVenda({ notasDoOrcamento: "origem:pdv" }), "pdv");
  assert.equal(canalDaVenda({ tituloDoOrcamento: "Venda PDV 05/09 10:00" }), "pdv");
  assert.equal(canalDaVenda({ notasDoPedido: "Venda rápida PDV finalizada automaticamente" }), "pdv");
  assert.equal(canalDaVenda({ notasDoOrcamento: "origem:catalogo\ncatalog_order_x" }), "loja");
  assert.equal(canalDaVenda({ notasDoPedido: "origem:catalogo\ncupom:X" }), "loja");
  assert.equal(canalDaVenda({ notasDoOrcamento: "origem:chat\nchat_thread_id:1" }), "chat");
  assert.equal(canalDaVenda({ tituloDoOrcamento: "Banner 2x1", notasDoOrcamento: "Cliente pediu pelo WhatsApp" }), "painel");
});

test("pedido que conta como venda e pedido em aberto", () => {
  assert.equal(pedidoConta("canceled"), false);
  assert.equal(pedidoConta("draft"), false);
  assert.equal(pedidoConta(null), true);
  assert.equal(pedidoConta("delivered"), true);
  assert.equal(pedidoEmAberto("delivered"), false);
  assert.equal(pedidoEmAberto("finished"), true);
});

test("etapa do pedido: coluna do quadro, ou a situação", () => {
  const colunas = lerColunasDoQuadro([{ id: "stage-1", label: "Impressão", status: "in_production" }, { id: 3 }, null]);
  assert.equal(colunas.length, 1);
  assert.equal(etapaDoPedido({ status: "in_production", production_stage_id: "stage-1" }, colunas).rotulo, "Impressão");
  assert.equal(etapaDoPedido({ status: "finished", production_stage_id: "__kanban_archived__" }, colunas).rotulo, "Pronto");
  assert.equal(etapaDoPedido({ status: "pending_production", production_stage_id: "stage-sumiu" }, colunas).rotulo, "Aguardando produção");
});

test("situação do vencimento", () => {
  assert.equal(situacaoDoVencimento("2026-10-13", "2026-10-14").chave, "vencida");
  assert.equal(situacaoDoVencimento("2026-10-21", "2026-10-14").chave, "semana");
  assert.equal(situacaoDoVencimento("2026-10-22", "2026-10-14").chave, "depois");
  assert.equal(situacaoDoVencimento(null, "2026-10-14").chave, "sem_data");
});

/* ---------------------------------------------------------------- catálogo */

test("catálogo: ids únicos, padrão de agrupamento aceito e as métricas pedidas", () => {
  const ids = METRICAS.map((m) => m.id);
  assert.equal(new Set(ids).size, ids.length);
  for (const id of [
    "faturamento",
    "quantidade_de_pedidos",
    "ticket_medio",
    "orcamentos_criados",
    "taxa_de_conversao",
    "vendas_por_vendedor",
    "vendas_por_canal",
    "produtos_mais_vendidos",
    "clientes_novos",
    "pedidos_por_etapa",
    "pedidos_atrasados",
    "contas_a_receber_em_aberto",
    "contas_a_pagar_em_aberto",
    "fluxo_de_caixa",
  ]) {
    assert.ok(metricaPorId(id), id);
  }
  for (const m of METRICAS) {
    if (m.agruparPadrao) assert.ok(m.dimensoes.includes(m.agruparPadrao), m.id);
    assert.ok(m.tipos.includes(m.tipoSugerido), m.id);
    if (!m.usaPeriodo) assert.ok(!m.dimensoes.includes("tempo") && !m.tipos.includes("linha"), m.id);
    /* Todo tipo aceito precisa dar um quadro válido com os padrões. */
    for (const tipo of m.tipos) {
      const r = conferirWidget({ metrica: m.id, tipo });
      assert.ok(r.ok, `${m.id}/${tipo}: ${r.ok ? "" : r.erro}`);
    }
  }
  assert.equal(catalogoPublico().length, METRICAS.length);
  assert.ok(!("ler" in catalogoPublico()[0]));
});

/* ---------------------------------------------------------------- quadros */

test("quadro: padrões da métrica", () => {
  const r = conferirWidget({ metrica: "vendas_por_vendedor" });
  assert.ok(r.ok);
  if (!r.ok) return;
  assert.equal(r.valor.tipo, "barras");
  assert.equal(r.valor.agrupar_por, "vendedor");
  assert.equal(r.valor.periodo, "este_mes");
  assert.equal(r.valor.limite, 10);
  assert.equal(r.valor.titulo, "Vendas por vendedor");
});

test("quadro: número nunca agrupa; linha é sempre no tempo", () => {
  const n = conferirWidget({ metrica: "faturamento", tipo: "numero", agrupar_por: "vendedor" });
  assert.ok(n.ok && n.valor.agrupar_por === null);
  const l = conferirWidget({ metrica: "faturamento", tipo: "linha" });
  assert.ok(l.ok && l.valor.agrupar_por === "tempo" && l.valor.limite === null);
  assert.equal(conferirWidget({ metrica: "faturamento", tipo: "linha", agrupar_por: "canal" }).ok, false);
});

test("quadro: recusa o que não confere, com explicação", () => {
  const casos: Array<[unknown, RegExp]> = [
    [{ metrica: "lucro_magico" }, /não existe/],
    [{ metrica: "faturamento", tipo: "radar" }, /tipo/],
    [{ metrica: "faturamento", tipo: "barras", agrupar_por: "fornecedor" }, /não pode ser agrupada/],
    [{ metrica: "faturamento", tipo: "pizza", agrupar_por: "tempo" }, /pizza/],
    [{ metrica: "pedidos_atrasados", tipo: "linha" }, /não pode ser mostrada/],
    [{ metrica: "faturamento", periodo: "personalizado", de: "2026-01-01" }, /personalizado/],
    [{ metrica: "faturamento", tipo: "barras", agrupar_por: "canal", limite: 500 }, /limite/],
    [{ metrica: "clientes_novos", filtros: { canal: "loja" } }, /não aceita o filtro/],
    [{ metrica: "faturamento", filtros: { canal: "orkut" } }, /canal/],
    [{ metrica: "faturamento", filtros: { vendedor: "x' or 1=1" } }, /vendedor/],
    ["texto", /formato/],
  ];
  for (const [bruto, erro] of casos) {
    const r = conferirWidget(bruto);
    assert.equal(r.ok, false, JSON.stringify(bruto));
    if (!r.ok) assert.match(r.erro, erro);
  }
});

test("quadro: título limpo de HTML e ids repetidos ganham outro", () => {
  const r = conferirWidgets([
    { id: "a", metrica: "faturamento", titulo: "<b>Vendas</b> do mês" },
    { id: "a", metrica: "ticket_medio" },
  ]);
  assert.ok(r.ok);
  if (!r.ok) return;
  assert.equal(r.valor[0].titulo, "Vendas do mês");
  assert.notEqual(r.valor[0].id, r.valor[1].id);
  assert.equal(conferirWidgets(new Array(25).fill({ metrica: "faturamento" })).ok, false);
});

test("quadros gravados que não conferem mais ficam de fora; mover de lugar", () => {
  const lidos = lerWidgetsGravados([{ id: "a", metrica: "faturamento" }, { id: "b", metrica: "retirada" }, { id: "c", metrica: "ticket_medio" }]);
  assert.deepEqual(lidos.map((w) => w.id), ["a", "c"]);
  const movidos = moverWidget(lidos as Widget[], "c", -1);
  assert.deepEqual(movidos.map((w) => w.id), ["c", "a"]);
  assert.deepEqual(moverWidget(lidos as Widget[], "a", -1).map((w) => w.id), ["a", "c"]);
});

/* ------------------------------------------------- cálculo com banco falso */

test("calcular: métrica do financeiro sem permissão é recusada antes de ler o banco", async () => {
  let leu = false;
  const db = new Proxy({}, { get: () => () => { leu = true; throw new Error("não devia ler"); } });
  const fonte = criarFonte({ db: db as never, tenantIds: ["t"], companyId: "c", donoId: "d", hoje: "2026-10-14", pode: () => false });
  const metrica = metricaPorId("fluxo_de_caixa")!;
  await assert.rejects(calcularMetrica(fonte, metrica, { intervalo: { de: "2026-10-01", ate: "2026-10-14" }, agruparPor: null }), MetricaSemPermissao);
  assert.equal(leu, false);
});

/** Um banco de mentira que responde como o Supabase, guardando os filtros pedidos. */
function bancoFalso(tabelas: Record<string, unknown[]>, filtrosVistos: string[]) {
  return {
    from(tabela: string) {
      const consulta = {
        select: () => consulta,
        in: (coluna: string) => (filtrosVistos.push(`${tabela}.${coluna}`), consulta),
        eq: () => consulta,
        neq: () => consulta,
        not: () => consulta,
        is: () => consulta,
        gte: () => consulta,
        lt: () => consulta,
        lte: () => consulta,
        order: () => consulta,
        maybeSingle: async () => ({ data: null, error: null }),
        range: async () => ({ data: tabelas[tabela] ?? [], error: null }),
        then: (ok: (v: unknown) => unknown) => Promise.resolve({ data: tabelas[tabela] ?? [], error: null }).then(ok),
      };
      return consulta;
    },
  };
}

test("calcular: fluxo de caixa no tempo com entradas e saídas, sempre preso à empresa", async () => {
  const vistos: string[] = [];
  const db = bancoFalso(
    {
      cash_flow: [
        { date: "2026-10-01", type: "IN", origin: "PEDIDO", amount: 100 },
        { date: "2026-10-01", type: "OUT", origin: "PAGAR", amount: 30 },
        { date: "2026-10-02", type: "IN", origin: "AJUSTE", amount: 5 },
      ],
    },
    vistos
  );
  const fonte = criarFonte({ db: db as never, tenantIds: ["t"], companyId: "c", donoId: "d", hoje: "2026-10-14", pode: () => true });
  const r = await calcularMetrica(fonte, metricaPorId("fluxo_de_caixa")!, { intervalo: { de: "2026-10-01", ate: "2026-10-02" }, agruparPor: "tempo" });
  assert.equal(r.valor, 75);
  assert.deepEqual(r.grupos?.map((g) => [g.valor, g.extra]), [[70, { entradas: 100, saidas: 30 }], [5, { entradas: 5, saidas: 0 }]]);
  assert.ok(vistos.includes("cash_flow.company_id"));
});

test("calcular: faturamento sem cancelados, por canal, com filtro de vendedor", async () => {
  const vistos: string[] = [];
  const pedido = (id: string, valor: number, status: string, notas: string | null, vendedor: string | null) => ({
    id,
    number: 1,
    status,
    total_amount: valor,
    paid_amount: 0,
    financial_status: "unpaid",
    created_at: "2026-10-05T15:00:00Z",
    delivery_date: null,
    client_id: null,
    quote_id: null,
    primary_seller_user_id: vendedor,
    internal_notes: notas,
    production_stage_id: null,
  });
  const ana = "11111111-1111-4111-8111-111111111111";
  const db = bancoFalso(
    {
      orders: [
        pedido("p1", 100, "delivered", "origem:catalogo", ana),
        pedido("p2", 200, "pending_production", null, ana),
        pedido("p3", 999, "canceled", null, ana),
        pedido("p4", 50, "finished", "Venda rápida PDV finalizada", null),
      ],
      company_users: [{ user_id: ana, email: "ana@grafica.com" }],
      employee_profiles: [{ user_id: ana, full_name: "Ana Souza" }],
    },
    vistos
  );
  const fonte = criarFonte({ db: db as never, tenantIds: ["t"], companyId: "c", donoId: "d", hoje: "2026-10-14", pode: () => true });
  const intervalo = { de: "2026-10-01", ate: "2026-10-14" };
  const total = await calcularMetrica(fonte, metricaPorId("faturamento")!, { intervalo, agruparPor: "canal" });
  assert.equal(total.valor, 350);
  assert.deepEqual(total.grupos?.map((g) => [g.chave, g.valor]), [["painel", 200], ["loja", 100], ["pdv", 50]]);
  const daAna = await calcularMetrica(fonte, metricaPorId("vendas_por_vendedor")!, { intervalo, agruparPor: "vendedor", filtros: { canal: "loja" } });
  assert.deepEqual(daAna.grupos?.map((g) => [g.rotulo, g.valor]), [["Ana Souza", 100]]);
  assert.ok(vistos.includes("orders.user_id"));
});
