import { test } from "node:test";
import assert from "node:assert/strict";
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { ROTULO_DA_ACAO, detalhesDoEvento, familiaDoEvento, rotuloDaAcao } from "./historico.ts";

/* Onde o código que grava no log mora, visto de onde o teste for rodado. */
function pastaDoCodigo(): string {
  const candidatos = ["src", join("apps", "admin", "src")];
  for (const c of candidatos) if (existsSync(join(c, "lib", "fornecedores"))) return c;
  throw new Error(
    "não achei a pasta src do admin a partir de " + process.cwd() + "; rode o teste de apps/admin ou da raiz"
  );
}

function arquivosDe(pasta: string): string[] {
  const saida: string[] = [];
  for (const nome of readdirSync(pasta)) {
    const caminho = join(pasta, nome);
    if (statSync(caminho).isDirectory()) saida.push(...arquivosDe(caminho));
    else if ((caminho.endsWith(".ts") || caminho.endsWith(".sql")) && !caminho.endsWith(".test.ts"))
      saida.push(caminho);
  }
  return saida;
}

test("toda ação que o código grava tem nome em português", () => {
  /* É o teste que fecha a porta: hoje sete ações apareciam cruas na linha do
     tempo do fornecedor, porque foram criadas nas rotas e ninguém lembrou da
     tradução. Acrescentar uma oitava sem rótulo passa a quebrar aqui. */
  const raiz = pastaDoCodigo();
  /* As migrações entram na varredura porque nem toda ação nasce em TypeScript:
     `purchase.received` é escrita pela função `receber_compra_do_fornecedor`,
     dentro da transação do recebimento. Olhando só o TS, ela passava batido —
     foi assim que essa ação ficou sem rótulo e apareceu crua na tela. */
  const fontes = [join(raiz, "app", "api"), join(raiz, "lib", "fornecedores"), join(raiz, "db", "migrations")]
    .filter((p) => existsSync(p))
    .flatMap((p) => arquivosDe(p));

  assert.ok(fontes.length > 20, `esperava dezenas de arquivos para varrer, achei ${fontes.length}`);

  const escritas = new Set<string>();
  for (const arquivo of fontes) {
    const texto = readFileSync(arquivo, "utf8");
    /* Só vale o arquivo que realmente escreve no log. */
    if (!texto.includes("supplier_activity_log") && !texto.includes("insertActivity")) continue;
    /* Aspas duplas no TypeScript, aspas simples no SQL. */
    for (const achado of texto.matchAll(/["'](supplier|purchase|payable|import|material)\.[a-z_]+["']/g)) {
      escritas.add(achado[0].slice(1, -1));
    }
  }

  assert.ok(escritas.size >= 15, `esperava as ações do módulo, achei ${escritas.size}`);

  const semNome = [...escritas].filter((a) => !ROTULO_DA_ACAO[a]).sort();
  assert.deepEqual(semNome, [], `ações gravadas sem rótulo: ${semNome.join(", ")}`);
});

test("o recebimento aparece no histórico", () => {
  /* Era a ação que faltava: a compra virava estoque e custo sem deixar rastro
     na tela que promete a ordem cronológica. */
  assert.equal(rotuloDaAcao("purchase.received"), "Mercadoria recebida");
  assert.equal(familiaDoEvento("purchase.received", "purchase_order"), "compra");
});

test("ação desconhecida vira frase, nunca o identificador cru", () => {
  assert.equal(rotuloDaAcao("purchase.inventada"), "Movimentação na compra");
  assert.equal(rotuloDaAcao("payable.inventada"), "Movimentação na conta a pagar");
  assert.equal(rotuloDaAcao("coisa.nova"), "Movimentação registrada");
  /* E o que não pode voltar é o ponto no meio da palavra na cara do usuário. */
  for (const acao of ["purchase.x", "material.y", "import.z", "supplier.w", "outro.k"]) {
    assert.ok(!rotuloDaAcao(acao).includes("."), `${acao} vazou para a tela`);
  }
});

test("material do fornecedor não se disfarça de cadastro", () => {
  /* Os eventos de material são gravados com entity_type "supplier", porque
     material não é entidade própria do log. Lendo o tipo, os três apareciam
     etiquetados como "Fornecedor". */
  assert.equal(familiaDoEvento("material.created", "supplier"), "material");
  assert.equal(familiaDoEvento("material.updated", "supplier"), "material");
  assert.equal(familiaDoEvento("material.deleted", "supplier"), "material");
  /* E o cadastro continua sendo cadastro. */
  assert.equal(familiaDoEvento("supplier.updated", "supplier"), "cadastro");
  assert.equal(familiaDoEvento("payable.paid", "payable"), "conta");
});

test("os detalhes do evento não mostram identificador nem nome de campo do código", () => {
  /* A linha do tempo exibia `itemsCount: 2` e `materialId: 80c3ccf6-…` ao lado
     de frases em português. O identificador não diz nada a quem lê, e ainda
     tomava a vaga do detalhe que diria. */
  const detalhes = detalhesDoEvento({
    materialId: "80c3ccf6-f6ca-407a-a623-51e3515991ac",
    itemsCount: 2,
    totalAmount: 1775,
    description: "Reposição de canecas",
  });
  assert.deepEqual(detalhes, ["itens: 2", "valor: 1775", "descrição: Reposição de canecas"]);

  for (const chave of ["id", "purchaseOrderId", "supplier_id", "bankAccountUuid"]) {
    assert.deepEqual(detalhesDoEvento({ [chave]: "x" }), [], `${chave} vazou para a tela`);
  }
});

test("os detalhes do recebimento saem legíveis", () => {
  assert.deepEqual(
    detalhesDoEvento({ itens_recebidos: 2, entradas_no_estoque: 2, total_da_compra: 1775, observacao: "" }),
    ["itens recebidos: 2", "entradas no estoque: 2", "total da compra: 1775"]
  );
});
