import { test } from "node:test";
import assert from "node:assert/strict";
import {
  areaDaPeca,
  calcularCustoDaComposicao,
  custoNoTamanho,
  custoPorM2NoTamanho,
  medidaDaBase,
  type LinhaDeMaterial,
  type LinhaDeProcesso,
} from "./composicao.ts";

/**
 * O banner de lona é o caso que este módulo existe para resolver: sete itens,
 * quatro unidades de medida e quatro jeitos de consumir. A lona acompanha a
 * área; o torsal e o sarrafo correm ao longo da largura — é assim que o ramo
 * trabalha, e é o que faz dois banners de mesma área custarem diferente; a
 * ponteira é por peça; o preparo é por pedido. Se ele fecha, o resto fecha.
 */
const LONA: LinhaDeMaterial = {
  id: "lona", nome: "Lona 440g", unidade: "m2", custoUnitario: 18,
  consumo: 1, base: "por_m2", perdaPct: 5,
};
const TINTA: LinhaDeMaterial = {
  id: "tinta", nome: "Tinta ecosolvente", unidade: "l", custoUnitario: 500,
  consumo: 0.012, base: "por_m2",
};
const MADEIRA: LinhaDeMaterial = {
  /* Sarrafo em cima e embaixo: 2 m de madeira por metro de largura. */
  id: "madeira", nome: "Sarrafo", unidade: "m", custoUnitario: 6,
  consumo: 2, base: "por_m_largura",
};
const TORSAL: LinhaDeMaterial = {
  /* 1,5 m por metro de largura: a corda em cima, com a sobra dos laços. */
  id: "torsal", nome: "Torsal", unidade: "m", custoUnitario: 1.5,
  consumo: 1.5, base: "por_m_largura",
};
const PONTEIRA: LinhaDeMaterial = {
  id: "ponteira", nome: "Ponteira", unidade: "un", custoUnitario: 0.8,
  consumo: 4, base: "por_peca",
};
const IMPRESSAO: LinhaDeProcesso = {
  id: "imp", nome: "Impressão", tipo: "maquina", custoHora: 30,
  minutos: 4, base: "por_m2", minutosDeSetup: 15,
};
const ACABAMENTO: LinhaDeProcesso = {
  id: "acab", nome: "Acabamento", tipo: "pessoa", custoHora: 22.67,
  minutos: 10, base: "por_peca",
};

const BANNER = {
  materiais: [LONA, TINTA, MADEIRA, TORSAL, PONTEIRA],
  processos: [IMPRESSAO, ACABAMENTO],
  quantidade: 1,
  larguraM: 2,
  alturaM: 3,
};

test("banner de lona 2x3: sete itens, quatro jeitos de consumir", () => {
  const r = calcularCustoDaComposicao(BANNER);
  assert.equal(r.areaUnitaria, 6);
  assert.equal(r.areaTotal, 6);
  assert.equal(r.itens.length, 7);

  const por = (nome: string) => r.itens.find((i) => i.nome === nome)!.custo;

  /* Lona: 1 m² por m², mais 5% de refile, sobre 6 m², a R$ 18. */
  assert.equal(por("Lona 440g"), 113.4);
  /* Tinta: 12 ml por m² sobre 6 m², a R$ 500 o litro. */
  assert.equal(por("Tinta ecosolvente"), 36);
  /* Sarrafo: 2 m por metro de largura, largura 2 m, a R$ 6 o metro. */
  assert.equal(por("Sarrafo"), 24);
  /* Torsal: 1,5 m por metro de largura, largura 2 m, a R$ 1,50. */
  assert.equal(por("Torsal"), 4.5);
  /* Ponteira: 4 por peça, tamanho nenhum muda isso. */
  assert.equal(por("Ponteira"), 3.2);
  /* Impressão: 4 min por m² sobre 6 m², mais 15 de preparo, a R$ 30/h. */
  assert.equal(por("Impressão"), 19.5);
  /* Acabamento: 10 min por peça a R$ 22,67/h. */
  assert.equal(por("Acabamento"), 3.78);

  assert.equal(r.custoTotal, 204.38);
  assert.equal(r.custoPorPeca, 204.38);
});

test("o exemplo do torsal: banner 0,70 x 1,00 desconta 70 centimetros", () => {
  /* O caso que definiu a base: o torsal corre na largura. Num banner de 70 cm
     de largura saem 70 cm de torsal do rolo — não uma unidade, não um m². */
  const soTorsal: LinhaDeMaterial = {
    id: "t", nome: "Torsal", unidade: "m", custoUnitario: 0.4,
    consumo: 1, base: "por_m_largura",
  };
  const r = calcularCustoDaComposicao({
    materiais: [soTorsal], processos: [], quantidade: 1, larguraM: 0.7, alturaM: 1,
  });
  assert.equal(r.custoTotal, 0.28);

  /* E esticar o banner para baixo não gasta um centímetro a mais de torsal. */
  const maisAlto = calcularCustoDaComposicao({
    materiais: [soTorsal], processos: [], quantidade: 1, larguraM: 0.7, alturaM: 2,
  });
  assert.equal(maisAlto.custoTotal, 0.28);
});

test("mesma area, custo diferente: 4x1,5 leva o dobro de torsal de 2x3", () => {
  /* A razão de a área não bastar como medida. Os dois têm 6 m² de lona; o
     comprido tem 4 m de largura para vestir de madeira e torsal. */
  const comprido = calcularCustoDaComposicao({ ...BANNER, larguraM: 4, alturaM: 1.5 });
  const normal = calcularCustoDaComposicao(BANNER);

  const custoDe = (r: typeof normal, nome: string) => r.itens.find((i) => i.nome === nome)!.custo;

  assert.equal(comprido.areaTotal, normal.areaTotal);
  assert.equal(custoDe(comprido, "Lona 440g"), custoDe(normal, "Lona 440g"));
  assert.equal(custoDe(comprido, "Torsal"), custoDe(normal, "Torsal") * 2);
  assert.ok(comprido.custoTotal > normal.custoTotal);
});

test("perimetro e a volta inteira: bainha em 2x3 sao 10 metros", () => {
  const r = calcularCustoDaComposicao({
    materiais: [{
      id: "fita", nome: "Fita de borda", unidade: "m", custoUnitario: 0.5,
      consumo: 1, base: "por_perimetro",
    }],
    processos: [{
      id: "bainha", nome: "Bainha", tipo: "pessoa", custoHora: 30,
      minutos: 2, base: "por_perimetro",
    }],
    quantidade: 1, larguraM: 2, alturaM: 3,
  });
  /* Fita: 10 m a R$ 0,50. Bainha: 2 min por metro de contorno, 20 min a R$ 30/h. */
  assert.equal(r.itens.find((i) => i.nome === "Fita de borda")!.custo, 5);
  assert.equal(r.itens.find((i) => i.nome === "Bainha")!.custo, 10);
});

test("dobrar a área não dobra o que é por peça nem o que é pela largura", () => {
  const grande = calcularCustoDaComposicao({ ...BANNER, larguraM: 2, alturaM: 6 });
  const pequeno = calcularCustoDaComposicao(BANNER);

  const custoDe = (r: typeof grande, nome: string) => r.itens.find((i) => i.nome === nome)!.custo;

  assert.equal(grande.areaTotal, 12);
  assert.equal(custoDe(grande, "Lona 440g"), custoDe(pequeno, "Lona 440g") * 2);
  assert.equal(custoDe(grande, "Ponteira"), custoDe(pequeno, "Ponteira"));
  /* Mesma largura: o torsal também não muda, mesmo com o dobro de área. */
  assert.equal(custoDe(grande, "Torsal"), custoDe(pequeno, "Torsal"));
});

test("o preparo dilui na tiragem: é por pedido, não por peça", () => {
  const uma = calcularCustoDaComposicao(BANNER);
  const vinte = calcularCustoDaComposicao({ ...BANNER, quantidade: 20 });

  assert.equal(uma.custoDeSetup, 7.5);
  assert.equal(vinte.custoDeSetup, 7.5);
  assert.ok(
    vinte.custoPorPeca < uma.custoPorPeca,
    `a peça deveria baratear na tiragem: ${vinte.custoPorPeca} vs ${uma.custoPorPeca}`
  );
});

test("quantidade multiplica tudo que é por peça e as medidas junto", () => {
  const r = calcularCustoDaComposicao({ ...BANNER, quantidade: 10 });
  assert.equal(r.areaTotal, 60);
  assert.equal(r.itens.find((i) => i.nome === "Ponteira")!.custo, 32);
  /* Torsal: 10 banners de 2 m de largura são 20 m de largura para cobrir. */
  assert.equal(r.itens.find((i) => i.nome === "Torsal")!.custo, 45);
});

test("perda entra no consumo, não no preço", () => {
  /* 5% de refile significa comprar 5% a mais de lona, e não vender 5% mais
     caro: o desperdício é custo, não margem. */
  const semPerda = calcularCustoDaComposicao({
    ...BANNER, materiais: [{ ...LONA, perdaPct: 0 }],
    processos: [],
  });
  const comPerda = calcularCustoDaComposicao({
    ...BANNER, materiais: [LONA], processos: [],
  });
  assert.equal(semPerda.custoTotal, 108);
  assert.equal(comPerda.custoTotal, 113.4);
});

test("produto sem medida ignora o que depende dela em vez de errar", () => {
  /* Caneca, camiseta, chaveiro: não têm área nem largura, e o que depende
     delas simplesmente não se aplica. Zerar é a resposta certa; multiplicar
     por 1 seria inventar uma dimensão que não existe. */
  const r = calcularCustoDaComposicao({
    materiais: [LONA, MADEIRA, TORSAL, PONTEIRA], processos: [], quantidade: 5,
  });
  assert.equal(r.areaTotal, 0);
  assert.equal(r.itens.length, 1);
  assert.equal(r.itens[0].nome, "Ponteira");
  assert.equal(r.custoTotal, 16);
});

test("linha sem custo ou sem consumo não vira item de zero", () => {
  /* Uma lista cheia de zeros esconde as linhas que importam. */
  const r = calcularCustoDaComposicao({
    materiais: [
      { ...PONTEIRA, custoUnitario: 0 },
      { ...PONTEIRA, id: "x", nome: "Sem consumo", consumo: 0 },
      PONTEIRA,
    ],
    processos: [{ ...ACABAMENTO, custoHora: 0 }],
    quantidade: 1,
  });
  assert.equal(r.itens.length, 1);
  assert.equal(r.itens[0].nome, "Ponteira");
});

test("área é largura por altura, e valores sujos não viram área", () => {
  assert.equal(areaDaPeca(2, 3), 6);
  assert.equal(areaDaPeca(0, 3), 0);
  assert.equal(areaDaPeca(null, 3), 0);
  assert.equal(areaDaPeca(-2, 3), 0);
});

test("medidaDaBase: cada base mede a sua dimensão, e dimensão ausente vale zero", () => {
  assert.equal(medidaDaBase("por_peca", 2, 3), 1);
  assert.equal(medidaDaBase("por_m2", 2, 3), 6);
  assert.equal(medidaDaBase("por_m_largura", 2, 3), 2);
  assert.equal(medidaDaBase("por_m_altura", 2, 3), 3);
  assert.equal(medidaDaBase("por_perimetro", 2, 3), 10);
  /* Perímetro sem uma das dimensões não é 2×largura: é indefinido, e
     indefinido aqui vale zero. */
  assert.equal(medidaDaBase("por_perimetro", 2, 0), 0);
  assert.equal(medidaDaBase("por_m_largura", 0, 3), 0);
});

test("custo por peça divide pelo número de peças, não pela área", () => {
  /* É assim que a pessoa pensa o produto, mesmo cobrando por metro. */
  const r = calcularCustoDaComposicao({ ...BANNER, quantidade: 4 });
  assert.equal(r.custoPorPeca, Math.round((r.custoTotal / 4) * 100) / 100);
});

test("as naturezas separam cada velocidade do custo", () => {
  /* É a estrutura que faz a tabela de tamanhos existir: sem separar, o custo
     vira uma média que só vale para o tamanho em que foi medida. */
  const r = calcularCustoDaComposicao(BANNER);

  /* Lona 18,90 + tinta 6,00 + impressão 2,00 (4 min a R$ 30/h). */
  assert.equal(r.naturezas.porM2, 26.9);
  /* Sarrafo 12,00 + torsal 2,25, por metro de largura. */
  assert.equal(r.naturezas.porLargura, 14.25);
  assert.equal(r.naturezas.porAltura, 0);
  assert.equal(r.naturezas.porPerimetro, 0);
  /* Ponteira 3,20 + acabamento 3,78. */
  assert.equal(r.naturezas.porPeca, 6.98);
  /* 15 min de acerto da impressora a R$ 30/h. */
  assert.equal(r.naturezas.porPedido, 7.5);
});

test("as naturezas remontam o custo total: e a mesma conta, nao outra", () => {
  /* Se a tabela de tamanhos e o resumo discordassem em um centavo, quem confere
     pararia de confiar nos dois. */
  for (const [w, h, q] of [[2, 3, 1], [0.5, 1, 1], [4, 1.5, 1], [3, 4, 7], [1.2, 2.4, 25]] as const) {
    const r = calcularCustoDaComposicao({ ...BANNER, larguraM: w, alturaM: h, quantidade: q });
    assert.equal(custoNoTamanho(r.naturezas, w, h, q), r.custoTotal);
  }
});

test("o m2 fica mais caro no tamanho pequeno: e a curva, nao um numero", () => {
  /* O erro que custa dinheiro no ramo: cobrar o mesmo valor por m² no banner
     pequeno e no grande. Com os mesmos insumos, o custo do m² despenca. */
  const { naturezas } = calcularCustoDaComposicao(BANNER);

  assert.equal(custoPorM2NoTamanho(naturezas, 1, 1.5), 46.05);
  assert.equal(custoPorM2NoTamanho(naturezas, 2, 3), 34.06);
  assert.equal(custoPorM2NoTamanho(naturezas, 4, 6), 29.88);
});

test("custoPorM2 e o custo do tamanho de referencia, nao a taxa por m2", () => {
  /* R$ 34,06 e não R$ 26,90: o que vai para o produto precisa carregar o
     sarrafo, o torsal e o preparo, senão o preço nasce sem eles. */
  const r = calcularCustoDaComposicao(BANNER);
  assert.equal(r.custoPorM2, 34.06);
  assert.equal(r.custoPorM2, Math.round((r.custoTotal / r.areaTotal) * 100) / 100);
});

test("produto sem medida nao inventa custo por m2", () => {
  const r = calcularCustoDaComposicao({
    materiais: [PONTEIRA], processos: [ACABAMENTO], quantidade: 5,
  });
  assert.equal(r.custoPorM2, 0);
  assert.equal(r.naturezas.porM2, 0);
  assert.equal(r.naturezas.porPeca, 6.98);
});

test("a tiragem dilui o pedido, e o tamanho dilui a peca: sao eixos diferentes", () => {
  const { naturezas } = calcularCustoDaComposicao(BANNER);

  /* Mais peças do mesmo tamanho: só o preparo se dilui. */
  assert.ok(custoPorM2NoTamanho(naturezas, 2, 3, 20) < custoPorM2NoTamanho(naturezas, 2, 3, 1));
  /* Mais área na mesma peça: o preparo e o que é por peça se diluem juntos. */
  assert.ok(custoPorM2NoTamanho(naturezas, 4, 6, 1) < custoPorM2NoTamanho(naturezas, 2, 3, 20));
});
