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

import { precoDaLinha, somarAdicionais } from "../adicionais.ts";
import { lerArgumentos, paraJsonSchema, type Campos } from "./esquema.ts";
import {
  avisoDeEstoque,
  casarEscolhas,
  centavosDoFormulario,
  comparavel,
  comoOPrecoSeForma,
  contaDaLinha,
  efeitoNoPreco,
  escolhasDoItem,
  listaDeEscolhas,
  montarSelectedOptions,
  obrigatoriosFaltando,
  precoDoPainel,
  regraDeQuantidade,
  totaisDoOrcamento,
  totalDaLinhaEmCentavos,
  type ProdutoDoOrcamento,
} from "./ferramentas/conta-do-orcamento.ts";

/* ----------------------------------------------------------------- fixtures */

/* O caso da Espia Elas: o produto está zerado e o preço vem das opções. */
const IMPRESSAO: ProdutoDoOrcamento = {
  id: "11111111-1111-4111-8111-111111111111",
  name: "Impressão A4",
  price: 0,
  cost_total: 0.2,
  unit_type: "unit",
  product_attributes: [
    {
      name: "Cor",
      is_required: true,
      allow_multiple: false,
      position: 1,
      options: [
        { id: "c0000000-0000-4000-8000-000000000001", label: "Preto e branco", price: 0.3, price_applies_per_quantity: true, position: 0 },
        { id: "c0000000-0000-4000-8000-000000000002", label: "Colorida", price: 1.1, price_applies_per_quantity: true, position: 1 },
      ],
    },
    {
      name: "Papel",
      is_required: true,
      allow_multiple: false,
      position: 0,
      options: [
        { id: "a0000000-0000-4000-8000-000000000001", label: "Sulfite 90 g", price: 0.1, price_applies_per_quantity: true, position: 0 },
        { id: "a0000000-0000-4000-8000-000000000002", label: "Couchê 180 g", price: 0.45, price_applies_per_quantity: true, position: 1 },
      ],
    },
    {
      name: "Acabamento",
      is_required: false,
      allow_multiple: true,
      position: 2,
      options: [
        { id: "b0000000-0000-4000-8000-000000000001", label: "Refile", price: 15, price_unit: "order", position: 0 },
        { id: "b0000000-0000-4000-8000-000000000002", label: "Furo", price: 0.05, price_unit: "piece", position: 1 },
      ],
    },
  ],
  product_price_tiers: [],
};

/* Produto por m² com faixas pela área total e adicionais nas três unidades. */
const ADESIVO: ProdutoDoOrcamento = {
  id: "22222222-2222-4222-8222-222222222222",
  name: "Adesivo vinil",
  price: 68,
  cost_total: 30,
  unit_type: "square_meter",
  product_price_tiers: [
    { min_qty: 0, max_qty: 5, unit_price: 68, sort_order: 0, active: true },
    { min_qty: 5, max_qty: 20, unit_price: 60, sort_order: 1, active: true },
    { min_qty: 20, max_qty: null, unit_price: 52, sort_order: 2, active: true },
  ],
  product_attributes: [
    {
      name: "Recorte",
      is_required: true,
      allow_multiple: false,
      position: 0,
      options: [
        { id: "3d15105c-68bf-4c55-9f77-24b2901aef56", label: "Reto", price: 0, stock: 0, position: 0, is_required: false, price_applies_per_quantity: true },
        { id: "33acc065-d9c2-4ab7-a205-246ad4167ed1", label: "No contorno da arte", price: 12, stock: 0, position: 1, is_required: false, price_applies_per_quantity: true },
      ],
    },
    {
      name: "Laminação",
      is_required: false,
      allow_multiple: false,
      position: 1,
      options: [
        { id: "2af3ecdd-e442-4fc6-ab11-1f3e152ab62f", label: "Sem laminação", price: 0, stock: 0, position: 0, is_required: false, price_applies_per_quantity: true },
        { id: "1396e513-ffb0-4be8-8783-cda505d733a5", label: "Laminação brilho", price: 14, stock: 0, position: 1, is_required: false, price_applies_per_quantity: true },
      ],
    },
    {
      name: "Extras",
      is_required: false,
      allow_multiple: true,
      position: 2,
      options: [
        { id: "e0000000-0000-4000-8000-000000000001", label: "Arte", price: 30, price_unit: "order", position: 0 },
        { id: "e0000000-0000-4000-8000-000000000002", label: "Aplicação", price: 2, price_unit: "piece", position: 1 },
      ],
    },
  ],
};

const perto = (a: number, b: number, oQue: string) => assert.ok(Math.abs(a - b) < 1e-9, `${oQue}: ${a} != ${b}`);

/* ----------------------------------------------------------- a conta (paridade) */

test("impressão zerada: o preço vem das opções, igual à conta única (lib/adicionais.ts)", () => {
  const casado = casarEscolhas(IMPRESSAO, [
    { atributo: "papel", opcao: "couche 180g" },
    { atributo: "COR", opcao: "colorida" },
  ]);
  assert.ok(casado.ok);
  const escolhas = casado.escolhas;
  assert.deepEqual(escolhas, { Papel: ["Couchê 180 g"], Cor: ["Colorida"] });

  const preco = precoDoPainel(IMPRESSAO, { quantidade: 100 }, escolhas);
  /* A conta à mão: (0 + 0,45 + 1,10) por unidade × 100. */
  perto(preco, 1.55, "preço unitário");
  /* E a mesma conta pela função que a tela chama, com as opções que a tela soma. */
  const daTela = precoDaLinha({
    precoBase: 0,
    faixas: [],
    pecas: 100,
    quantidadeEfetiva: 100,
    adicionais: somarAdicionais([{ price: 0.45 }, { price: 1.1 }], "unit"),
    soVariacao: false,
  }).precoPorMedida;
  perto(preco, daTela, "paridade com a tela");
  assert.equal(centavosDoFormulario(preco) * 100, 155);
});

test("90 g preto e branco sai mais barato que 180 g colorida, e o acabamento por pedido é rateado", () => {
  const barato = casarEscolhas(IMPRESSAO, [{ opcao: "90" }, { opcao: "preto e branco" }]);
  assert.ok(barato.ok);
  perto(precoDoPainel(IMPRESSAO, { quantidade: 50 }, barato.escolhas), 0.4, "90 g P&B");

  const comAcabamento = casarEscolhas(IMPRESSAO, [{ opcao: "Sulfite 90 g" }, { opcao: "Colorida" }, { atributo: "acabamento", opcao: "refile" }, { atributo: "acabamento", opcao: "furo" }]);
  assert.ok(comAcabamento.ok);
  assert.deepEqual(comAcabamento.escolhas.Acabamento, ["Refile", "Furo"], "atributo de várias opções junta as duas");
  /* (0,10 + 1,10) × 200 + 0,05 × 200 peças + 15 uma vez = 240 + 10 + 15 = 265 -> 1,325 por unidade. */
  const preco = precoDoPainel(IMPRESSAO, { quantidade: 200 }, comAcabamento.escolhas);
  perto(preco, 265 / 200, "rateio");
  assert.equal(centavosDoFormulario(preco), 1.33, "o campo de dinheiro da tela arredonda em centavos");
});

test("m²: faixa pela área total, adicional por m², por peça e uma vez no pedido", () => {
  const casado = casarEscolhas(ADESIVO, [
    { atributo: "Recorte", opcao: "no contorno" },
    { atributo: "laminacao", opcao: "brilho" },
    { atributo: "extras", opcao: "Arte" },
    { atributo: "extras", opcao: "aplicacao" },
  ]);
  assert.ok(casado.ok);
  /* 3 peças de 1,20 × 0,80 = 2,88 m²: primeira faixa (68). */
  const medidas = { quantidade: 3, largura: 1.2, altura: 0.8 };
  const conta = contaDaLinha(ADESIVO, medidas, casado.escolhas);
  const totalAMao = (68 + 12 + 14) * 2.88 + 2 * 3 + 30;
  perto(conta.total, totalAMao, "total da linha");
  perto(precoDoPainel(ADESIVO, medidas, casado.escolhas), totalAMao / 2.88, "preço por m² rateado");

  /* 10 peças de 1 × 1 = 10 m²: segunda faixa (60), pela ÁREA e não pelas peças. */
  const grande = contaDaLinha(ADESIVO, { quantidade: 10, largura: 1, altura: 1 }, { Recorte: ["Reto"] });
  perto(grande.total, 60 * 10, "faixa pela área");
  assert.equal(grande.faixa?.unit_price, 60);
});

test("preço só pelas opções: o preço base não entra; sob consulta dá zero", () => {
  const soOpcoes = { ...ADESIVO, use_variation_price_only: true };
  perto(precoDoPainel(soOpcoes, { quantidade: 1, largura: 1, altura: 1 }, { Recorte: ["No contorno da arte"] }), 12, "só a opção");
  assert.equal(precoDoPainel({ ...ADESIVO, price_on_request: true }, { quantidade: 1 }, { Recorte: ["Reto"] }), 0);
});

test("o campo de dinheiro arredonda como o Intl da tela (pt-BR)", () => {
  const daTela = (valor: number) => Number(new Intl.NumberFormat("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(valor).replace(/\./g, "").replace(",", "."));
  for (const valor of [0, 1.005, 1.255, 2.675, 106.4999999, 1234567.895, 0.125, 16.9, 1 / 3]) {
    assert.equal(centavosDoFormulario(valor), daTela(valor), `valor ${valor}`);
  }
});

test("totais: desconto em reais ou em %, frete, e o total da linha em centavos (30 × 16,90 = 507)", () => {
  const linhas = [
    { quantity: 30, unit_type: "unit", width_m: null, height_m: null, length_m: null, unit_price: 16.9, unit_cost: 5 },
    { quantity: 2, unit_type: "square_meter", width_m: 1.5, height_m: 1, length_m: null, unit_price: 80, unit_cost: 20 },
  ];
  const emReais = totaisDoOrcamento(linhas, { desconto: 50, frete: 20 });
  perto(emReais.subtotal, 507 + 240, "subtotal");
  perto(emReais.total, 747 - 50 + 20, "total");
  perto(emReais.custo, 150 + 60, "custo");
  const emPercentual = totaisDoOrcamento(linhas, { descontoPercentual: 10, frete: 0 });
  perto(emPercentual.desconto, 74.7, "10% do subtotal");
  assert.equal(totalDaLinhaEmCentavos({ ...linhas[0] }), 507);
  assert.equal(totalDaLinhaEmCentavos({ ...linhas[1] }), 240);
});

/* ----------------------------------------------------- selected_options */

test("selected_options sai byte a byte como o da tela (orçamento #277 da Aurora, feito pelo painel)", () => {
  const doPainel = {
    options: [
      { label: "Reto", price: 0, option_id: "3d15105c-68bf-4c55-9f77-24b2901aef56", price_unit: null, affects_stock: false, attribute_name: "Recorte", price_applies_per_quantity: true },
      { label: "Sem laminação", price: 0, option_id: "2af3ecdd-e442-4fc6-ab11-1f3e152ab62f", price_unit: null, affects_stock: false, attribute_name: "Laminação", price_applies_per_quantity: true },
    ],
    inventory_scope: "product",
    inventory_attribute_name: null,
  };
  const casado = casarEscolhas(ADESIVO, [{ atributo: "Recorte", opcao: "reto" }, { atributo: "Laminação", opcao: "sem laminacao" }]);
  assert.ok(casado.ok);
  const montado = montarSelectedOptions(ADESIVO, casado.escolhas);
  assert.deepEqual(montado, doPainel);
  /* A ordem das chaves de cada opção é a mesma que a tela escreve. */
  assert.deepEqual(Object.keys(montado!.options[0]), ["option_id", "attribute_name", "label", "price", "price_applies_per_quantity", "price_unit", "affects_stock"]);
  /* E lido de volta dá as mesmas escolhas. */
  assert.deepEqual(escolhasDoItem(montado), casado.escolhas);
  assert.equal(montarSelectedOptions(ADESIVO, {}), null, "sem escolha nenhuma a tela grava null");
});

test("estoque por opção: affects_stock só no atributo que controla, e o nome do atributo vai junto", () => {
  const camiseta: ProdutoDoOrcamento = {
    id: "33333333-3333-4333-8333-333333333333",
    name: "Camiseta",
    price: 40,
    cost_total: 10,
    unit_type: "unit",
    inventory_scope: "option",
    inventory_attribute_name: "Tamanho",
    product_attributes: [
      { name: "Tamanho", is_required: true, position: 0, options: [{ id: "t1", label: "P", stock: 3 }, { id: "t2", label: "M", stock: 0 }] },
      { name: "Cor", is_required: false, position: 1, options: ["Branca", "Preta"] },
    ],
  };
  const montado = montarSelectedOptions(camiseta, { Tamanho: ["P"], Cor: ["Preta"] });
  assert.equal(montado?.inventory_scope, "option");
  assert.equal(montado?.inventory_attribute_name, "Tamanho");
  assert.deepEqual(montado?.options.map((o) => [o.attribute_name, o.label, o.affects_stock, o.option_id]), [
    ["Tamanho", "P", true, "t1"],
    ["Cor", "Preta", false, null],
  ]);
  assert.equal(avisoDeEstoque(camiseta, { Tamanho: ["P"] }, 2), null);
  assert.match(avisoDeEstoque(camiseta, { Tamanho: ["P"] }, 5) ?? "", /Estoque insuficiente para Tamanho: P\. Disponível: 3/);
  assert.match(avisoDeEstoque(camiseta, { Tamanho: ["M"] }, 1) ?? "", /Sem estoque/);
});

/* ------------------------------------------------- casar pelo nome */

test("casar escolhas: acento, maiúscula, dois-pontos e espaço não importam; id também vale", () => {
  assert.equal(comparavel("  Papel: "), "papel");
  assert.equal(comparavel("COUCHÊ  180 g"), "couche 180 g");
  const porId = casarEscolhas(IMPRESSAO, [{ opcao_id: "C0000000-0000-4000-8000-000000000002" }]);
  assert.ok(porId.ok);
  assert.deepEqual(porId.escolhas, { Cor: ["Colorida"] });
  const comDoisPontos = casarEscolhas(IMPRESSAO, [{ atributo: "Papel:", opcao: "SULFITE 90G" }]);
  assert.ok(comDoisPontos.ok);
  assert.deepEqual(comDoisPontos.escolhas, { Papel: ["Sulfite 90 g"] });
});

test("casar escolhas: o que não existe é recusado com a lista das escolhas, para perguntar à pessoa", () => {
  const semAtributo = casarEscolhas(IMPRESSAO, [{ atributo: "Gramatura", opcao: "90" }]);
  assert.ok(!semAtributo.ok);
  assert.match(semAtributo.erro, /não tem o atributo "Gramatura"/);
  assert.match(semAtributo.erro, /Papel \(obrigatório, uma opção\): Sulfite 90 g \(\+ R\$\s0,10 por unidade\); Couchê 180 g/);

  const semOpcao = casarEscolhas(IMPRESSAO, [{ atributo: "Papel", opcao: "300 g" }]);
  assert.ok(!semOpcao.ok);
  assert.match(semOpcao.erro, /não tem a opção "300 g"/);

  const duas = casarEscolhas(IMPRESSAO, [{ atributo: "Cor", opcao: "Colorida" }, { atributo: "Cor", opcao: "Preto e branco" }]);
  assert.ok(!duas.ok);
  assert.match(duas.erro, /aceita uma opção só/);

  const semOpcoes = casarEscolhas({ ...IMPRESSAO, product_attributes: [] }, [{ opcao: "x" }]);
  assert.ok(!semOpcoes.ok);
});

test("casar escolhas: no alterar, o atributo mencionado troca e os outros ficam", () => {
  const antes = { Papel: ["Sulfite 90 g"], Cor: ["Preto e branco"] };
  const depois = casarEscolhas(IMPRESSAO, [{ atributo: "cor", opcao: "colorida" }], antes);
  assert.ok(depois.ok);
  assert.deepEqual(depois.escolhas, { Papel: ["Sulfite 90 g"], Cor: ["Colorida"] });
  /* Escolha antiga que saiu do cadastro cai fora. */
  const velha = casarEscolhas(IMPRESSAO, [], { Papel: ["Papel que não existe mais"] });
  assert.ok(velha.ok);
  assert.deepEqual(velha.escolhas, {});
});

test("obrigatórios faltando e regra de quantidade: o que barra o salvar na tela", () => {
  assert.deepEqual(obrigatoriosFaltando(IMPRESSAO, { Papel: ["Sulfite 90 g"] }).map((a) => a.name), ["Cor"]);
  assert.deepEqual(obrigatoriosFaltando(IMPRESSAO, { Papel: ["Sulfite 90 g"], Cor: ["Colorida"] }), []);
  const comLimites = { ...IMPRESSAO, min_sale_qty_enabled: true, min_sale_qty: 50, allowed_sale_qty_enabled: false };
  assert.equal(regraDeQuantidade(comLimites, 10), "Quantidade mínima: 50.");
  assert.equal(regraDeQuantidade(comLimites, 50), null);
  const permitidas = { ...IMPRESSAO, allowed_sale_qty_enabled: true, allowed_sale_quantities: [100, 250, 500] };
  assert.equal(regraDeQuantidade(permitidas, 120), "Use uma quantidade permitida: 100, 250, 500.");
});

test("explicar o preço: efeito de cada opção e a frase para o assistente", () => {
  const semEspaco = (texto: string) => texto.replace(/\s/g, " ");
  assert.equal(semEspaco(efeitoNoPreco({ price: 12, price_applies_per_quantity: true }, "square_meter")), "+ R$ 12,00/m²");
  assert.equal(semEspaco(efeitoNoPreco({ price: 30, price_unit: "order" }, "square_meter")), "+ R$ 30,00 no pedido");
  assert.equal(semEspaco(efeitoNoPreco({ price: 2, price_unit: "piece" }, "square_meter")), "+ R$ 2,00 por peça");
  assert.equal(semEspaco(efeitoNoPreco({ price: -6 }, "square_meter")), "- R$ 6,00/m²");
  assert.equal(efeitoNoPreco({ price: 0 }, "unit"), "não muda o preço");
  assert.match(comoOPrecoSeForma(IMPRESSAO), /preço base zero: o preço vem das opções/);
  assert.match(comoOPrecoSeForma(IMPRESSAO), /Pergunte à pessoa/);
  assert.match(comoOPrecoSeForma(ADESIVO), /quantidade total em m²/);
  assert.match(listaDeEscolhas(IMPRESSAO), /^Papel \(obrigatório, uma opção\)/, "na ordem da tela (position)");
});

/* ------------------------------------------------------------ esquema: objeto */

const COM_OBJETO: Campos = {
  condicoes: {
    tipo: "objeto",
    descricao: "Condições.",
    campos: {
      esquema: { tipo: "opcao", descricao: "Esquema.", opcoes: ["a_vista", "parcelado"] },
      parcelas: { tipo: "inteiro", descricao: "Parcelas.", min: 2, max: 24 },
      formas: { tipo: "textos", descricao: "Formas." },
    },
  },
  itens: {
    tipo: "lista",
    descricao: "Itens.",
    itens: {
      quantidade: { tipo: "numero", descricao: "Qtd.", obrigatorio: true },
      opcoes: { tipo: "lista", descricao: "Opções.", itens: { atributo: { tipo: "texto", descricao: "A." }, opcao: { tipo: "texto", descricao: "O." } } },
      arte: { tipo: "objeto", descricao: "Arte.", campos: { titulo: { tipo: "texto", descricao: "T.", max: 10 } } },
    },
  },
};

test("esquema: objeto vira um JSON Schema fechado, dentro e fora de listas", () => {
  const esquema = paraJsonSchema(COM_OBJETO) as { properties: Record<string, Record<string, unknown>> };
  const condicoes = esquema.properties.condicoes as { type: string; additionalProperties: boolean; description: string; properties: Record<string, { enum?: string[] }> };
  assert.equal(condicoes.type, "object");
  assert.equal(condicoes.additionalProperties, false);
  assert.equal(condicoes.description, "Condições.");
  assert.deepEqual(condicoes.properties.esquema.enum, ["a_vista", "parcelado"]);
  const item = (esquema.properties.itens.items as { properties: Record<string, { type: string }> }).properties;
  assert.equal(item.arte.type, "object");
  assert.equal(item.opcoes.type, "array");
});

test("esquema: objeto é lido com as mesmas regras, e o erro diz onde", () => {
  const lido = lerArgumentos(COM_OBJETO, { condicoes: { parcelas: "3", esquema: "PARCELADO" }, itens: [{ quantidade: "2,5", arte: { titulo: "Frente" } }] });
  assert.ok(lido.ok);
  assert.deepEqual(lido.valor, { condicoes: { esquema: "parcelado", parcelas: 3 }, itens: [{ quantidade: 2.5, arte: { titulo: "Frente" } }] });

  const errado = lerArgumentos(COM_OBJETO, { condicoes: { parcelas: 1 } });
  assert.ok(!errado.ok);
  assert.match(errado.erro, /^condicoes: "parcelas" não pode ser menor que 2/);

  const estranho = lerArgumentos(COM_OBJETO, { condicoes: { juros: 1 } });
  assert.ok(!estranho.ok);
  assert.match(estranho.erro, /condicoes: Campo desconhecido: "juros"/);

  const naoObjeto = lerArgumentos(COM_OBJETO, { condicoes: ["a"] });
  assert.ok(!naoObjeto.ok);
  assert.match(naoObjeto.erro, /"condicoes" precisa ser um objeto JSON/);

  const noItem = lerArgumentos(COM_OBJETO, { itens: [{ quantidade: 1, arte: { titulo: "Um título longo demais" } }] });
  assert.ok(!noItem.ok);
  assert.match(noItem.erro, /^itens\[1\]: arte: "titulo" passou do tamanho máximo/);
});
