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

import {
  avaliarCupom,
  condicoesDoCupom,
  escolherCupom,
  escolherCupons,
  erroDoCupom,
  fraseDoCupom,
  gerarCodigo,
  itemEntraNoCupom,
  lerCupom,
  relogioDeBrasilia,
  rotuloDoBeneficio,
  situacaoDoCupom,
  textoDosDias,
  ufDoCep,
  valeNoDiaENoHorario,
  type ContextoDoCupom,
  type ItemDoCupom,
} from "./regras-do-cupom.ts";

/* 16/10/2026 é sexta-feira; 15h de Brasília = 18h UTC. */
const SEXTA_15H = Date.parse("2026-10-16T18:00:00Z");

const CANECA: ItemDoCupom = { product_id: "p-caneca", category_id: "c-canecas", total: 60, quantity: 2 };
const CAMISA: ItemDoCupom = { product_id: "p-camisa", category_id: "c-camisetas", total: 40, quantity: 1 };
const BANNER: ItemDoCupom = { product_id: "p-banner", category_id: "c-banner-lona", total: 100, quantity: 1 };
const MAES = { "c-banner-lona": "c-banners", "c-banners": null, "c-canecas": null, "c-camisetas": null };

function ctx(parcial: Partial<ContextoDoCupom> = {}): ContextoDoCupom {
  return { itens: [CANECA, CAMISA], maes: MAES, cliente: null, frete: null, agora: SEXTA_15H, ...parcial };
}

/* A linha como sai do banco antes da migration 200 (sem as colunas novas). */
const ANTIGO = { id: "a", code: "bemvindo10", type: "percent", value: 10, min_total: 0, min_items: 0, active: true, starts_at: null, ends_at: null };

test("cupom antigo: lido com os padrões de sempre", () => {
  const c = lerCupom(ANTIGO);
  assert.equal(c.code, "BEMVINDO10");
  assert.equal(c.scope, "all");
  assert.equal(c.audience, "all");
  assert.equal(c.application, "code");
  assert.equal(c.min_scope, "cart");
  assert.equal(c.usage_limit, null);
  assert.deepEqual(c.weekdays, []);
  assert.equal(c.show_in_store, false);
});

test("cupom antigo: mesma conta e mesmas mensagens de antes", () => {
  const pct = avaliarCupom(lerCupom(ANTIGO), ctx());
  assert.deepEqual(pct.ok && [pct.desconto, pct.descontoNoFrete], [10, 0]);

  const fixo = avaliarCupom(lerCupom({ ...ANTIGO, type: "value", value: 500 }), ctx());
  assert.equal(fixo.ok && fixo.desconto, 100, "fixo nunca passa do subtotal");

  const minimo = avaliarCupom(lerCupom({ ...ANTIGO, min_total: 150 }), ctx());
  assert.equal(!minimo.ok && minimo.mensagem, "Este cupom vale a partir de R$ 150,00 em produtos.");
  assert.equal(!minimo.ok && minimo.falta, 50);

  const itens = avaliarCupom(lerCupom({ ...ANTIGO, min_items: 4 }), ctx());
  assert.equal(!itens.ok && itens.mensagem, "Este cupom vale a partir de 4 unidades.");

  assert.equal((avaliarCupom(lerCupom({ ...ANTIGO, active: false }), ctx()) as { mensagem: string }).mensagem, "Cupom inválido ou inativo.");
  assert.equal(
    (avaliarCupom(lerCupom({ ...ANTIGO, ends_at: "2026-10-01T00:00:00Z" }), ctx()) as { mensagem: string }).mensagem,
    "Este cupom já venceu."
  );
  assert.equal(
    (avaliarCupom(lerCupom({ ...ANTIGO, starts_at: "2026-11-01T00:00:00Z" }), ctx()) as { mensagem: string }).mensagem,
    "Este cupom ainda não começou a valer."
  );
});

test("combo não conta como unidade no mínimo de itens", () => {
  const combo: ItemDoCupom = { product_id: "p-x", category_id: null, total: 10, quantity: 5, is_combo_item: true };
  const r = avaliarCupom(lerCupom({ ...ANTIGO, min_items: 4 }), ctx({ itens: [CANECA, CAMISA, combo] }));
  assert.equal(r.ok, false);
});

test("percentual com teto em reais", () => {
  const r = avaliarCupom(lerCupom({ ...ANTIGO, value: 50, max_discount: 20 }), ctx());
  assert.equal(r.ok && r.desconto, 20);
});

test("onde vale: categoria escolhida, com subcategoria, e exclusões", () => {
  const soBanners = lerCupom({ ...ANTIGO, scope: "categories", scope_category_ids: ["c-banners"] });
  assert.equal(itemEntraNoCupom(soBanners, BANNER, MAES), true, "subcategoria entra");
  assert.equal(itemEntraNoCupom(soBanners, CANECA, MAES), false);

  const r = avaliarCupom(lerCupom({ ...ANTIGO, value: 10, scope: "categories", scope_category_ids: ["c-canecas"] }), ctx());
  assert.equal(r.ok && r.desconto, 6, "10% só sobre as canecas (60)");

  const fixo = avaliarCupom(lerCupom({ ...ANTIGO, type: "value", value: 50, scope: "products", scope_product_ids: ["p-camisa"] }), ctx());
  assert.equal(fixo.ok && fixo.desconto, 40, "fixo não passa do subtotal dos participantes");

  const exceto = avaliarCupom(lerCupom({ ...ANTIGO, exclude_category_ids: ["c-banners"] }), ctx({ itens: [CANECA, BANNER] }));
  assert.equal(exceto.ok && exceto.desconto, 6);

  const nada = avaliarCupom(lerCupom({ ...ANTIGO, scope: "products", scope_product_ids: ["p-outro"] }), ctx());
  assert.equal(!nada.ok && nada.motivo, "sem_itens");
});

test("mínimo sobre o carrinho todo ou só sobre os participantes", () => {
  const base = { ...ANTIGO, scope: "categories", scope_category_ids: ["c-canecas"], min_total: 80 };
  assert.equal(avaliarCupom(lerCupom(base), ctx()).ok, true, "carrinho todo = 100");
  const so = avaliarCupom(lerCupom({ ...base, min_scope: "eligible" }), ctx());
  assert.equal(!so.ok && so.mensagem, "Este cupom vale a partir de R$ 80,00 em produtos participantes.");
  assert.equal(!so.ok && so.falta, 20);
});

test("frete grátis: todo, até um valor, por estado e esperando o frete", () => {
  const frete = lerCupom({ ...ANTIGO, type: "free_shipping", value: 0 });
  const sem = avaliarCupom(frete, ctx());
  assert.deepEqual(sem.ok && [sem.descontoNoFrete, sem.esperandoFrete], [0, true]);

  const todo = avaliarCupom(frete, ctx({ frete: { valor: 32.5, cep: "01310-100", retirada: false } }));
  assert.deepEqual(todo.ok && [todo.desconto, todo.descontoNoFrete, todo.beneficio], [0, 32.5, 32.5]);

  const ate = avaliarCupom(lerCupom({ ...ANTIGO, type: "free_shipping", shipping_max: 20 }), ctx({ frete: { valor: 32.5, cep: "01310100", retirada: false } }));
  assert.equal(ate.ok && ate.descontoNoFrete, 20);

  const soSul = lerCupom({ ...ANTIGO, type: "free_shipping", shipping_states: ["pr", "sc", "rs"] });
  const sp = avaliarCupom(soSul, ctx({ frete: { valor: 30, cep: "01310100", retirada: false } }));
  assert.equal(!sp.ok && sp.mensagem, "O frete grátis deste cupom vale só para PR, SC e RS.");
  const poa = avaliarCupom(soSul, ctx({ frete: { valor: 30, cep: "90010-000", retirada: false } }));
  assert.equal(poa.ok && poa.descontoNoFrete, 30);
});

test("estado pelo CEP", () => {
  assert.equal(ufDoCep("01310-100"), "SP");
  assert.equal(ufDoCep("20040-002"), "RJ");
  assert.equal(ufDoCep("70040-010"), "DF");
  assert.equal(ufDoCep("69301-000"), "RR");
  assert.equal(ufDoCep("69900-000"), "AC");
  assert.equal(ufDoCep("1234"), null);
});

test("quem pode usar: conta, primeira compra, clientes escolhidos", () => {
  const logados = lerCupom({ ...ANTIGO, audience: "logged" });
  assert.equal((avaliarCupom(logados, ctx()) as { motivo: string }).motivo, "precisa_entrar");
  assert.equal(avaliarCupom(logados, ctx({ cliente: { id: "cli-1" } })).ok, true);

  const primeira = lerCupom({ ...ANTIGO, audience: "first_purchase" });
  assert.equal((avaliarCupom(primeira, ctx({ cliente: { id: "cli-1" }, jaComprou: true })) as { mensagem: string }).mensagem, "Este cupom é só para a primeira compra na loja.");
  assert.equal(avaliarCupom(primeira, ctx({ cliente: { id: "cli-1" }, jaComprou: false })).ok, true);
  assert.equal((avaliarCupom(primeira, ctx({ cliente: { id: "cli-1" }, jaComprou: false, usosDoCliente: 1 })) as { motivo: string }).motivo, "limite_do_cliente", "primeira compra é um uso por cliente");

  const escolhidos = lerCupom({ ...ANTIGO, audience: "clients", audience_client_ids: ["cli-2"] });
  assert.equal((avaliarCupom(escolhidos, ctx({ cliente: { id: "cli-1" } })) as { motivo: string }).motivo, "cliente_fora");
  assert.equal(avaliarCupom(escolhidos, ctx({ cliente: { id: "cli-2" } })).ok, true);
});

test("limites: total e por cliente", () => {
  const cem = lerCupom({ ...ANTIGO, usage_limit: 100 });
  assert.equal(avaliarCupom(cem, ctx({ usos: 99 })).ok, true);
  assert.equal((avaliarCupom(cem, ctx({ usos: 100 })) as { motivo: string }).motivo, "esgotado");
  assert.equal(situacaoDoCupom(cem, SEXTA_15H, 100), "esgotado");

  const umPor = lerCupom({ ...ANTIGO, usage_limit_per_client: 1 });
  assert.equal((avaliarCupom(umPor, ctx()) as { motivo: string }).motivo, "precisa_entrar", "limite por cliente exige conta");
  assert.equal((avaliarCupom(umPor, ctx({ cliente: { id: "c" }, usosDoCliente: 1 })) as { mensagem: string }).mensagem, "Você já usou este cupom.");
  const dois = lerCupom({ ...ANTIGO, usage_limit_per_client: 2 });
  assert.match((avaliarCupom(dois, ctx({ cliente: { id: "c" }, usosDoCliente: 2 })) as { mensagem: string }).mensagem, /2 vezes/);
});

test("relógio de Brasília e dias da semana", () => {
  const r = relogioDeBrasilia(SEXTA_15H);
  assert.deepEqual([r.diaDaSemana, r.minutos, r.dia, r.mes], [5, 15 * 60, 16, 10]);
  /* 01h30 de sábado em Brasília = 04h30 UTC. */
  const sabadoMadrugada = Date.parse("2026-10-17T04:30:00Z");
  assert.equal(relogioDeBrasilia(sabadoMadrugada).diaDaSemana, 6);

  assert.equal(valeNoDiaENoHorario({ weekdays: [5], hours_start: null, hours_end: null }, SEXTA_15H).ok, true);
  assert.equal(valeNoDiaENoHorario({ weekdays: [0, 6], hours_start: null, hours_end: null }, SEXTA_15H).ok, false);
  assert.equal(valeNoDiaENoHorario({ weekdays: [], hours_start: "18:00", hours_end: "22:00" }, SEXTA_15H).ok, false);
  assert.equal(valeNoDiaENoHorario({ weekdays: [], hours_start: "14:00", hours_end: "16:00" }, SEXTA_15H).ok, true);
  /* Faixa que vira a noite: 22h às 2h de sexta vale na madrugada de sábado. */
  const noite = { weekdays: [5], hours_start: "22:00", hours_end: "02:00" };
  assert.equal(valeNoDiaENoHorario(noite, sabadoMadrugada).ok, true);
  assert.equal(valeNoDiaENoHorario(noite, SEXTA_15H).ok, false);

  const fds = avaliarCupom(lerCupom({ ...ANTIGO, weekdays: [0, 6] }), ctx());
  assert.equal(!fds.ok && fds.mensagem, "Este cupom vale nos fins de semana.");
  const happy = avaliarCupom(lerCupom({ ...ANTIGO, hours_start: "18:00:00", hours_end: "22:00:00" }), ctx());
  assert.equal(!happy.ok && happy.mensagem, "Este cupom vale das 18h às 22h.");

  assert.equal(textoDosDias([1, 2, 3, 4, 5]), "de segunda a sexta");
  assert.equal(textoDosDias([2, 4]), "às terças e quintas");
  assert.equal(textoDosDias([5, 6]), "às sextas e aos sábados");
  assert.equal(textoDosDias([0, 1, 2, 3, 4, 5, 6]), "");
});

test("situação para a lista do painel", () => {
  const c = lerCupom(ANTIGO);
  assert.equal(situacaoDoCupom(c, SEXTA_15H), "ativo");
  assert.equal(situacaoDoCupom({ ...c, active: false }, SEXTA_15H), "pausado");
  assert.equal(situacaoDoCupom({ ...c, starts_at: "2026-11-01T03:00:00Z" }, SEXTA_15H), "agendado");
  assert.equal(situacaoDoCupom({ ...c, ends_at: "2026-10-10T03:00:00Z" }, SEXTA_15H), "expirado");
});

test("automático: fica o melhor para o cliente; o digitado ganha empate", () => {
  const auto5 = lerCupom({ ...ANTIGO, id: "a5", code: "AUTO5", name: "Semana da caneca", value: 5, application: "automatic" });
  const auto8 = lerCupom({ ...ANTIGO, id: "a8", code: "AUTO8", value: 8, application: "automatic" });
  const digitado10 = lerCupom({ ...ANTIGO, id: "d10", code: "DEZ", value: 10 });
  const digitado5 = lerCupom({ ...ANTIGO, id: "d5", code: "CINCO", value: 5 });
  const contexto = ctx();
  const av = (c: ReturnType<typeof lerCupom>) => ({ cupom: c, avaliacao: avaliarCupom(c, contexto) });

  const so = escolherCupom(null, [av(auto5), av(auto8)]);
  assert.equal(so.escolhido?.cupom.code, "AUTO8");
  assert.equal(so.escolhido?.automatico, true);

  const comDigitado = escolherCupom(av(digitado10), [av(auto5), av(auto8)]);
  assert.equal(comDigitado.escolhido?.cupom.code, "DEZ");
  assert.equal(comDigitado.escolhido?.automatico, false);

  const pior = escolherCupom(av(digitado5), [av(auto8)]);
  assert.equal(pior.escolhido?.cupom.code, "AUTO8");
  assert.match(String(pior.aviso), /melhor que o cupom CINCO/);

  const empate = escolherCupom(av(digitado5), [av(auto5)]);
  assert.equal(empate.escolhido?.cupom.code, "CINCO");

  const invalido = lerCupom({ ...ANTIGO, code: "VELHO", ends_at: "2026-01-01T00:00:00Z" });
  const r = escolherCupom(av(invalido), [av(auto5)]);
  assert.equal(r.escolhido?.cupom.code, "AUTO5");
  assert.equal(r.aviso, "Este cupom já venceu.");

  const freteDigitado = lerCupom({ ...ANTIGO, code: "FRETE", type: "free_shipping" });
  assert.equal(escolherCupom(av(freteDigitado), [av(auto8)]).escolhido?.cupom.code, "FRETE", "sem frete escolhido ainda, o digitado fica");
});

test("como o cliente vê", () => {
  const c = lerCupom({
    ...ANTIGO,
    scope: "categories",
    scope_category_ids: ["c-canecas"],
    min_total: 50,
    ends_at: "2026-10-31T23:59:00-03:00",
  });
  assert.equal(fraseDoCupom(c, SEXTA_15H, { categorias: { "c-canecas": "Canecas" } }), "10% de desconto em Canecas, para compras a partir de R$ 50, até 31/10");
  assert.equal(rotuloDoBeneficio(c), "10% OFF");
  assert.equal(rotuloDoBeneficio(lerCupom({ ...ANTIGO, type: "value", value: 20 })), "R$ 20 OFF");
  assert.equal(rotuloDoBeneficio(lerCupom({ ...ANTIGO, type: "free_shipping" })), "Frete grátis");
  assert.equal(
    fraseDoCupom(lerCupom({ ...ANTIGO, type: "free_shipping", shipping_max: 30, min_total: 199, audience: "first_purchase" }), SEXTA_15H),
    "Frete grátis até R$ 30, para compras a partir de R$ 199 antes dos descontos, na primeira compra"
  );
  assert.equal(fraseDoCupom(lerCupom({ ...ANTIGO, exclude_category_ids: ["c-banners"] }), SEXTA_15H, { categorias: { "c-banners": "Banners" } }), "10% de desconto na loja toda, exceto Banners");
  assert.deepEqual(condicoesDoCupom(lerCupom({ ...ANTIGO, usage_limit: 100, usage_limit_per_client: 1 }), SEXTA_15H), ["Só os primeiros 100 usos", "Um uso por cliente"]);
  assert.deepEqual(condicoesDoCupom(lerCupom(ANTIGO), SEXTA_15H), []);
});

test("código aleatório e validação para salvar", () => {
  let i = 0;
  const codigo = gerarCodigo("natal", () => (i++ % 10) / 10);
  assert.match(codigo, /^NATAL-[A-Z2-9]{6}$/);
  assert.match(gerarCodigo(), /^[A-Z2-9]{6}$/);

  assert.equal(erroDoCupom(lerCupom({ ...ANTIGO, value: 0 })), "O valor do desconto precisa ser maior que zero.");
  assert.equal(erroDoCupom(lerCupom({ ...ANTIGO, type: "free_shipping", value: 0 })), null, "frete grátis não tem valor");
  assert.equal(erroDoCupom(lerCupom({ ...ANTIGO, value: 120 })), "Desconto em porcentagem vai no máximo até 100%.");
  assert.equal(erroDoCupom(lerCupom({ ...ANTIGO, scope: "categories" })), "Escolha pelo menos uma categoria onde o cupom vale.");
  assert.equal(erroDoCupom(lerCupom({ ...ANTIGO, audience: "clients" })), "Escolha pelo menos um cliente que pode usar o cupom.");
  assert.equal(erroDoCupom(lerCupom(ANTIGO)), null);
});

test("dois cupons: um de desconto e um de frete grátis no mesmo pedido", () => {
  const comFrete = ctx({ frete: { valor: 20, cep: "01310100", retirada: false } });
  const av = (c: ReturnType<typeof lerCupom>, contexto = comFrete) => ({ cupom: c, avaliacao: avaliarCupom(c, contexto) });
  const dez = lerCupom({ ...ANTIGO, id: "d", code: "DEZ" });
  const vinte = lerCupom({ ...ANTIGO, id: "v", code: "VINTE", type: "value", value: 20 });
  const frete = lerCupom({ ...ANTIGO, id: "f", code: "FRETE", type: "free_shipping" });
  const freteAuto = lerCupom({ ...ANTIGO, id: "fa", code: "FRETEAUTO", type: "free_shipping", shipping_max: 10, application: "automatic" });
  const cincoAuto = lerCupom({ ...ANTIGO, id: "ca", code: "CINCOAUTO", value: 5, application: "automatic" });

  /* digitado + digitado */
  let r = escolherCupons([av(dez), av(frete)], []);
  assert.deepEqual(r.escolhidos.map((c) => [c.cupom.code, c.desconto, c.descontoNoFrete]), [["DEZ", 10, 0], ["FRETE", 0, 20]]);

  /* automático + digitado: cada vaga com a sua regra */
  r = escolherCupons([av(dez)], [av(freteAuto), av(cincoAuto)]);
  assert.deepEqual(r.escolhidos.map((c) => [c.cupom.code, c.automatico]), [["DEZ", false], ["FRETEAUTO", true]]);

  /* automático + automático */
  r = escolherCupons([], [av(freteAuto), av(cincoAuto)]);
  assert.deepEqual(r.escolhidos.map((c) => c.cupom.code), ["CINCOAUTO", "FRETEAUTO"]);

  /* o digitado de frete ganha do automático de frete na vaga dele */
  r = escolherCupons([av(frete)], [av(freteAuto), av(cincoAuto)]);
  assert.deepEqual(r.escolhidos.map((c) => c.cupom.code), ["CINCOAUTO", "FRETE"]);

  /* nunca dois descontos: dois digitados na mesma vaga, vale o último */
  r = escolherCupons([av(dez), av(vinte)], []);
  assert.deepEqual(r.escolhidos.map((c) => c.cupom.code), ["VINTE"]);

  /* digitado inválido vira erro e não ocupa vaga */
  const vencido = lerCupom({ ...ANTIGO, id: "x", code: "VELHO", ends_at: "2026-01-01T00:00:00Z" });
  r = escolherCupons([av(vencido), av(frete)], [av(cincoAuto)]);
  assert.deepEqual(r.erros, [{ code: "VELHO", mensagem: "Este cupom já venceu." }]);
  assert.deepEqual(r.escolhidos.map((c) => c.cupom.code), ["CINCOAUTO", "FRETE"]);
});

test("cupom que não combina fica sozinho", () => {
  const comFrete = ctx({ frete: { valor: 20, cep: "01310100", retirada: false } });
  const av = (c: ReturnType<typeof lerCupom>) => ({ cupom: c, avaliacao: avaliarCupom(c, comFrete) });
  const dezSozinho = lerCupom({ ...ANTIGO, id: "d", code: "DEZ", combinable: false });
  const frete = lerCupom({ ...ANTIGO, id: "f", code: "FRETE", type: "free_shipping" });
  const freteAuto = lerCupom({ ...ANTIGO, id: "fa", code: "FRETEAUTO", type: "free_shipping", application: "automatic" });
  const freteAutoSozinho = lerCupom({ ...ANTIGO, id: "fs", code: "FRETESO", type: "free_shipping", application: "automatic", combinable: false });
  const cincoAuto = lerCupom({ ...ANTIGO, id: "ca", code: "CINCOAUTO", value: 5, application: "automatic" });

  /* o digitado que não combina ganha do automático de frete */
  let r = escolherCupons([av(dezSozinho)], [av(freteAuto)]);
  assert.deepEqual(r.escolhidos.map((c) => c.cupom.code), ["DEZ"]);
  assert.match(r.avisos.join(" "), /usado sozinho/);

  /* dois digitados, um não combina: fica o que dá mais (frete 20 > 10) */
  r = escolherCupons([av(dezSozinho), av(frete)], []);
  assert.deepEqual(r.escolhidos.map((c) => c.cupom.code), ["FRETE"]);

  /* o frete automático que não combina sai, e a vaga é preenchida por outro que combina */
  r = escolherCupons([av(lerCupom({ ...ANTIGO, id: "d2", code: "DEZ2" }))], [av(freteAutoSozinho), av(freteAuto), av(cincoAuto)]);
  assert.deepEqual(r.escolhidos.map((c) => c.cupom.code), ["DEZ2", "FRETEAUTO"]);

  assert.deepEqual(condicoesDoCupom(lerCupom({ ...ANTIGO, combinable: false }), SEXTA_15H), ["Não vale junto com outro cupom"]);
  assert.deepEqual(condicoesDoCupom(lerCupom({ ...ANTIGO, type: "free_shipping", min_total: 199 }), SEXTA_15H), [
    "Para compras a partir de R$ 199 (antes dos descontos)",
    "Vale junto com um cupom de desconto",
  ]);
});
