import { test } from "node:test";
import assert from "node:assert/strict";
import { canCompanyRole, companyPermissions, type CompanyRole } from "./company-context.ts";
import { ACAO_POR_CHAVE, CATALOGO, ORDEM_NIVEL, cadeiaDe, niveisDoModulo } from "./catalogo-permissoes.ts";

/**
 * O catálogo responde igual ao que valia antes.
 *
 * Estes testes existem para uma coisa só: garantir que a troca do motor de
 * permissões não mudou o acesso de ninguém. A tabela `REGRA_ANTIGA` abaixo é a
 * transcrição literal do `switch` que existia — se o catálogo divergir dela em
 * qualquer combinação, algum cliente perde ou ganha acesso sem ninguém ter
 * pedido, e o teste falha.
 *
 * Quando uma regra mudar de propósito (a etapa das permissões fantasma, por
 * exemplo), é aqui que a mudança fica registrada — de forma deliberada, não
 * por acidente.
 */

const PAPEIS: CompanyRole[] = [
  "owner",
  "admin",
  "manager",
  "financial",
  "production",
  "sales",
  "viewer",
];

const TODOS: CompanyRole[] = ["manager", "financial", "production", "sales", "viewer"];

/* Transcrição do comportamento anterior. Não simplificar: o valor deste bloco
   é ser uma cópia independente, escrita a partir do código antigo. */
const REGRA_ANTIGA: Record<string, CompanyRole[]> = {
  "users.manage": [],
  "subscription.manage": [],
  "company.settings": ["manager"],
  "dashboard.read": [],
  "pdv.read": [],
  "products.read": TODOS,
  "products.write": ["manager"],
  "categories.read": TODOS,
  "categories.write": ["manager"],
  "catalog.read": TODOS,
  "catalog.write": ["manager"],
  "clients.read": TODOS,
  "clients.write": ["manager", "sales"],
  "quotes.read": TODOS,
  "quotes.write": ["manager", "sales"],
  "orders.read": TODOS,
  "orders.write": ["manager", "sales", "production"],
  "orders.edit": [],
  "financial.read": ["financial"],
  "financial.write": ["financial"],
  "commissions.read": ["manager", "financial"],
  "commissions.write": ["manager"],
  "commissions.adjust": ["financial"],
  "commissions.close": ["financial"],
  "commissions.approve": ["financial"],
  "commissions.pay": ["financial"],
  "commissions.self": TODOS,
  "commissions.profile": ["manager", "financial", "sales"],
  "ia.read": TODOS,
  "ia.write": ["manager"],
  "agenda.read": TODOS,
  "agenda.write": ["manager", "sales", "production"],
  "pricing.read": TODOS,
  "pricing.write": ["manager", "financial"],
};

/* Chaves que ganharam padrão próprio de propósito, com data e motivo.
   Elas saem da comparação "nada mudou" porque a mudança FOI o pedido — o que
   as guarda é a tabela logo abaixo, que diz o novo acesso esperado.

   09/09/2026, Fornecedores: o módulo dependia inteiramente de `financial.*`,
   e em produção isso deixava 43 dos 44 funcionários sem conseguir abrir a
   tela. Gerente e comprador agora entram por papel próprio. A herança fica no
   catálogo para ninguém perder o acesso que já tinha. */
const MUDOU_DE_PROPOSITO: Record<string, CompanyRole[]> = {
  "suppliers.read": ["manager", "financial", "production"],
  "suppliers.write": ["manager", "financial"],
  /* 16/09/2026, Notas fiscais: módulo novo, sem chave anterior para herdar.
     Vendedor vê as notas; cancelar fica com gerente e financeiro; configurar
     só com dono e admin.
     18/09/2026: emitir gasta nota PAGA do pacote, então saiu do padrão do
     vendedor (auditoria das brechas). O admin libera na tela de permissões. */
  "fiscal.ver": ["manager", "financial", "sales"],
  "fiscal.emitir": ["manager", "financial"],
  "fiscal.cancelar": ["manager", "financial"],
  "fiscal.configurar": [],
  "fiscal.recebidas": ["manager", "financial"],
  /* 01/10/2026, Assistentes e apps: recurso novo, sem chave anterior para
     herdar. Só dono e admin por padrão; os demais, um a um, pela tela de
     permissões. */
  "company.assistants.manage": [],
  /* 09/10/2026, Produtos digitais: área nova, sem chave anterior para herdar.
     Só dono e admin por padrão; os demais pelo nível do módulo ou por
     "Liberar" (a área mostra e-mails de alunos e o que cada um comprou). */
  "digital.read": [],
  "digital.write": [],
};

/* Módulos que não tinham chave própria: a tela perguntava por uma emprestada.
   Enquanto ninguém configurar a chave nova, ela tem de responder exatamente o
   que a emprestada respondia — é o que mantém o acesso de quem já usa. */
const CHAVE_QUE_ERA_USADA: Record<string, string> = {
  "suppliers.read": "financial.read",
  "suppliers.write": "financial.write",
  "production.read": "products.read",
  "production.write": "products.write",
  "materials.read": "products.read",
  "materials.write": "products.write",
  "reports.read": "financial.read",
  "whatsapp.manage": "company.settings",
  "marketing.read": "catalog.read",
  "marketing.manage": "catalog.write",
  /* O Chats nasceu perguntando por Pedidos: quem via pedido via a conversa, e
     quem alterava pedido respondia por ela. */
  "chats.read": "orders.read",
  "chats.write": "orders.edit",
  "chats.settings": "orders.edit",
  /* Cada tela do Financeiro ganhou chave propria. Todas nascem repassando a
     pergunta para a porta do modulo, entao quem tinha o Financeiro continua
     com as onze telas e com os valores a vista — e so muda o acesso de quem o
     admin configurar tela a tela. */
  "financial.overview.read": "financial.read",
  "financial.payables.read": "financial.read",
  "financial.fixedcosts.read": "financial.read",
  "financial.receivables.read": "financial.read",
  "financial.payments.read": "financial.read",
  "financial.cashflow.read": "financial.read",
  "financial.bank.read": "financial.read",
  "financial.dre.read": "financial.read",
  "financial.links.read": "financial.read",
  "financial.amounts.see": "financial.read",
  "financial.payables.write": "financial.write",
  "financial.payables.settle": "financial.write",
  "financial.fixedcosts.write": "financial.write",
  "financial.receivables.write": "financial.write",
  "financial.receivables.settle": "financial.write",
  "financial.cashflow.write": "financial.write",
  "financial.bank.write": "financial.write",
  "financial.links.write": "financial.write",
  /* E as telas dos demais modulos, pela mesma regra. */
  "catalog.store.read": "catalog.read",
  "catalog.store.write": "catalog.write",
  "catalog.dashboard.read": "catalog.read",
  "catalog.products.read": "catalog.read",
  "catalog.products.write": "catalog.write",
  "catalog.orders.read": "catalog.read",
  "catalog.orders.write": "catalog.write",
  "catalog.banners.read": "catalog.read",
  "catalog.banners.write": "catalog.write",
  "catalog.stories.read": "catalog.read",
  "catalog.stories.write": "catalog.write",
  "catalog.reviews.read": "catalog.read",
  "catalog.reviews.write": "catalog.write",
  "pricing.config.read": "pricing.read",
  "pricing.config.write": "pricing.write",
  "pricing.wizard.read": "pricing.read",
  "pricing.health.read": "pricing.read",
  "pricing.taxes.read": "pricing.read",
  "pricing.taxes.write": "financial.write",
  "products.catalog.read": "products.read",
  "products.catalog.write": "products.write",
  "products.stock.read": "products.read",
  "products.stock.write": "products.write",
  "suppliers.list.read": "suppliers.read",
  "suppliers.list.write": "suppliers.write",
  "suppliers.xbz.read": "suppliers.read",
  "production.board.read": "production.read",
  "production.board.write": "production.write",
  "production.costs.read": "production.read",
  "production.costs.write": "production.write",
  "clients.list.read": "clients.read",
  "clients.list.write": "clients.write",
  "clients.ranking.read": "clients.read",
  "agenda.board.read": "agenda.read",
  "agenda.board.write": "agenda.write",
  "agenda.report.read": "agenda.read",
  "company.data.read": "company.settings",
  "company.data.write": "company.settings",
  "company.shipping.read": "company.settings",
  "company.shipping.write": "company.settings",
  "pricing.health.write": "pricing.write",
  "suppliers.xbz.write": "suppliers.write",
  "production.board.columns": "company.settings",
  "financial.bank.integrations": "company.settings",
  /* As telas dos produtos digitais respondem pela porta do módulo. */
  "digital.overview.read": "digital.read",
  "digital.products.read": "digital.read",
  "digital.products.write": "digital.write",
  "digital.students.read": "digital.read",
  "digital.students.write": "digital.write",
  "digital.comments.read": "digital.read",
  "digital.comments.write": "digital.write",
  "digital.deliveries.read": "digital.read",
  "digital.deliveries.write": "digital.write",
  "digital.files.read": "digital.read",
  "digital.files.write": "digital.write",
  "digital.settings.read": "digital.read",
  "digital.settings.write": "digital.write",
};

test("nenhum acesso mudou: catálogo × regra anterior, todos os papéis", () => {
  let comparados = 0;
  for (const permissao of companyPermissions) {
    /* Chave que mudou de propósito responde pela tabela nova; chave nova
       responde pela emprestada; chave antiga responde por si. */
    /* A herança tem mais de um elo desde que as telas ganharam chave própria:
       `suppliers.overview.read` herda de `suppliers.read`, que por sua vez
       herdava de `financial.read`. O passeio para no primeiro elo que tem
       resposta — seja uma regra antiga, seja uma mudança deliberada. */
    let referencia = permissao as string;
    let mudou = MUDOU_DE_PROPOSITO[referencia];
    for (let salto = 0; salto < 6 && !mudou; salto += 1) {
      if (REGRA_ANTIGA[referencia]) break;
      const proxima = CHAVE_QUE_ERA_USADA[referencia];
      if (!proxima) break;
      referencia = proxima;
      mudou = MUDOU_DE_PROPOSITO[referencia];
    }
    const esperadoPara = mudou ?? REGRA_ANTIGA[referencia];
    assert.ok(esperadoPara, `permissão ${referencia} não está na regra anterior`);

    for (const papel of PAPEIS) {
      const esperado = papel === "owner" || papel === "admin" || esperadoPara.includes(papel);
      const obtido = canCompanyRole(papel, {}, permissao);
      assert.equal(obtido, esperado, `${permissao} para ${papel}`);
      comparados += 1;
    }
  }
  assert.equal(comparados, companyPermissions.length * PAPEIS.length);
});

test("toda chave nova herda, e herda de uma chave que existe", () => {
  for (const [nova, antiga] of Object.entries(CHAVE_QUE_ERA_USADA)) {
    const acao = ACAO_POR_CHAVE.get(nova);
    assert.ok(acao, `${nova} falta no catálogo`);
    assert.equal(acao.herdaDe, antiga, `${nova} deveria herdar ${antiga}`);
    assert.ok(ACAO_POR_CHAVE.has(antiga), `${nova} herda de chave inexistente`);
    /* Padrão próprio junto com herança daria duas respostas para a mesma
       pergunta, e a herança nunca chegaria a valer — a não ser quando o padrão
       próprio é o pedido, e aí ele é o que responde primeiro. */
    if (MUDOU_DE_PROPOSITO[nova]) {
      assert.deepEqual(
        acao.papeisPadrao,
        MUDOU_DE_PROPOSITO[nova],
        `${nova} tem padrão próprio diferente do declarado em MUDOU_DE_PROPOSITO`
      );
    } else {
      assert.deepEqual(acao.papeisPadrao, [], `${nova} não pode ter padrão próprio ainda`);
    }
  }
});

test("o ajuste da chave nova vence o da emprestada", () => {
  /* O dia em que o admin marcar Fornecedores no modal, a herança sai de cena —
     inclusive para liberar quem não tem o financeiro. */
  assert.equal(canCompanyRole("sales", { "suppliers.read": true }, "suppliers.read"), true);
  assert.equal(canCompanyRole("financial", { "suppliers.read": false }, "suppliers.read"), false);
  /* E o financeiro segue respondendo enquanto Fornecedores não for tocado. */
  assert.equal(canCompanyRole("financial", {}, "suppliers.read"), true);
  assert.equal(canCompanyRole("sales", {}, "suppliers.read"), false);
});

test("negar a emprestada continua negando a nova, inclusive para admin", () => {
  /* Um admin com o financeiro cortado não entra em Fornecedores hoje. A chave
     nova não pode ser a porta dos fundos que ele não tinha. */
  assert.equal(canCompanyRole("admin", { "financial.read": false }, "suppliers.read"), false);
  assert.equal(canCompanyRole("financial", { "financial.read": false }, "suppliers.read"), false);
  /* Mas o ajuste específico continua sendo o mais forte dos dois. */
  assert.equal(
    canCompanyRole("admin", { "financial.read": false, "suppliers.read": true }, "suppliers.read"),
    true
  );
});

test("cadeia de herança termina, mesmo se alguém escrever um ciclo", () => {
  assert.deepEqual(cadeiaDe("suppliers.read"), ["suppliers.read", "financial.read"]);
  assert.deepEqual(cadeiaDe("financial.read"), ["financial.read"]);
  /* Chave fora do catálogo não quebra: vira cadeia de um elo e é negada. */
  assert.deepEqual(cadeiaDe("modulo.inexistente"), ["modulo.inexistente"]);
  for (const chave of companyPermissions) {
    const cadeia = cadeiaDe(chave);
    assert.equal(new Set(cadeia).size, cadeia.length, `${chave} repete um elo`);
    assert.ok(cadeia.length <= 6, `${chave} tem cadeia longa demais`);
  }
});

test("o catálogo cobre exatamente as permissões declaradas", () => {
  /* Chave no tipo e ausente do catálogo seria negada em silêncio; sobrando no
     catálogo, apareceria na tela sem existir no código. */
  const noCatalogo = new Set(ACAO_POR_CHAVE.keys());
  for (const p of companyPermissions) {
    assert.ok(noCatalogo.has(p), `${p} falta no catálogo`);
  }
  assert.equal(noCatalogo.size, companyPermissions.length);
});

test("ajuste da ação vence o padrão do papel, e também o dono", () => {
  assert.equal(canCompanyRole("viewer", { "financial.read": true }, "financial.read"), true);
  assert.equal(canCompanyRole("financial", { "financial.read": false }, "financial.read"), false);
  /* Há empresas que tiraram algo de um admin por aqui. */
  assert.equal(canCompanyRole("admin", { "users.manage": false }, "users.manage"), false);
  assert.equal(canCompanyRole("owner", { "users.manage": false }, "users.manage"), false);
});

test("nível do módulo concede o que está abaixo dele e nega o que está acima", () => {
  /* A camada nova. Ainda sem uso em produção — entra com o modal. */
  assert.equal(canCompanyRole("viewer", { orders: "ver" }, "orders.read"), true);
  assert.equal(canCompanyRole("viewer", { orders: "ver" }, "orders.write"), false);
  assert.equal(canCompanyRole("viewer", { orders: "operar" }, "orders.write"), true);
  assert.equal(canCompanyRole("viewer", { orders: "operar" }, "orders.edit"), false);
  assert.equal(canCompanyRole("viewer", { orders: "total" }, "orders.edit"), true);
});

test("nível restritivo vence o padrão do papel", () => {
  /* Sem isto, escolher "ver" não adiantaria: o papel reabriria na camada
     seguinte e a escolha do admin não valeria nada. */
  assert.equal(canCompanyRole("sales", {}, "orders.write"), true);
  assert.equal(canCompanyRole("sales", { orders: "ver" }, "orders.write"), false);
});

test("a ação marcada vence o nível do módulo", () => {
  assert.equal(canCompanyRole("viewer", { orders: "ver", "orders.edit": true }, "orders.edit"), true);
  assert.equal(canCompanyRole("viewer", { orders: "total", "orders.edit": false }, "orders.edit"), false);
});

test("nível inválido é ignorado, não vira liberação", () => {
  for (const lixo of ["tudo", "", "sim", "1"]) {
    assert.equal(
      canCompanyRole("viewer", { orders: lixo }, "orders.write"),
      false,
      `nível "${lixo}" não podia decidir nada`
    );
  }
});

test("chave fora do catálogo é negada", () => {
  assert.equal(
    canCompanyRole("manager", {}, "modulo.inexistente" as never),
    false
  );
});

test("cada módulo tem rótulo em português e nenhuma ação órfã", () => {
  for (const modulo of CATALOGO) {
    assert.ok(modulo.rotulo.length > 2, `módulo ${modulo.chave} sem rótulo`);
    assert.ok(modulo.acoes.length > 0, `módulo ${modulo.chave} sem ações`);
    for (const acao of modulo.acoes) {
      assert.equal(acao.modulo, modulo.chave, `${acao.chave} está no módulo errado`);
      assert.ok(acao.rotulo.length > 2, `${acao.chave} sem rótulo legível`);
      /* O modal antigo mostrava a chave crua para o admin adivinhar. */
      assert.ok(!acao.rotulo.includes("."), `${acao.chave} tem rótulo com cara de chave`);
    }
  }
});

test("nível \"nenhum\" fecha o módulo inteiro, inclusive contra o padrão do papel", () => {
  /* Vendedor recebe pedidos por padrão; o admin fecha o módulo e a escolha
     dele vale. */
  assert.equal(canCompanyRole("sales", {}, "orders.read"), true);
  assert.equal(canCompanyRole("sales", { orders: "nenhum" }, "orders.read"), false);
  assert.equal(canCompanyRole("sales", { orders: "nenhum" }, "orders.write"), false);
  assert.equal(canCompanyRole("sales", { orders: "nenhum" }, "orders.edit"), false);
});

test("fechar o módulo não fecha os outros", () => {
  const so_pedidos = { orders: "nenhum" };
  assert.equal(canCompanyRole("sales", so_pedidos, "orders.read"), false);
  assert.equal(canCompanyRole("sales", so_pedidos, "quotes.read"), true);
  assert.equal(canCompanyRole("sales", so_pedidos, "clients.write"), true);
});

test("a ação marcada ainda vence o módulo fechado", () => {
  /* É como o admin abre uma exceção: fecha Pedidos, mas deixa a pessoa ver. */
  assert.equal(
    canCompanyRole("sales", { orders: "nenhum", "orders.read": true }, "orders.read"),
    true
  );
  assert.equal(
    canCompanyRole("sales", { orders: "nenhum", "orders.read": true }, "orders.write"),
    false
  );
});

test("a escada oferecida por módulo não promete degrau que não existe", () => {
  const porChave = new Map(CATALOGO.map((m) => [m.chave, m]));

  /* Funcionários só tem uma ação, de nível total: oferecer "Só ver" seria uma
     escolha que não mudaria nada na tela. */
  assert.deepEqual(niveisDoModulo(porChave.get("users")!), ["nenhum", "total"]);
  assert.deepEqual(niveisDoModulo(porChave.get("orders")!), ["nenhum", "ver", "operar", "total"]);
  assert.deepEqual(niveisDoModulo(porChave.get("products")!), ["nenhum", "ver", "operar"]);

  for (const modulo of CATALOGO) {
    const niveis = niveisDoModulo(modulo);
    assert.ok(niveis.includes("nenhum"), `${modulo.chave} precisa poder ser fechado`);
    /* Todo degrau oferecido tem de corresponder a alguma ação real. */
    for (const nivel of niveis) {
      if (nivel === "nenhum") continue;
      assert.ok(
        modulo.acoes.some((a) => a.nivelMinimo === nivel),
        `${modulo.chave} oferece "${nivel}" sem ação nesse degrau`
      );
    }
  }
});

test("cada degrau da escada concede exatamente o que está nele e abaixo", () => {
  const pedidos = CATALOGO.find((m) => m.chave === "orders")!;
  for (const nivel of niveisDoModulo(pedidos)) {
    for (const acao of pedidos.acoes) {
      const esperado = ORDEM_NIVEL[nivel] >= ORDEM_NIVEL[acao.nivelMinimo!];
      assert.equal(
        canCompanyRole("viewer", { orders: nivel }, acao.chave),
        esperado,
        `${nivel} x ${acao.chave}`
      );
    }
  }
});

test("Fornecedores tem porta propria, e ninguem perdeu a antiga", () => {
  /* O pedido de 09/09/2026: gerente e comprador entram sem depender do
     Financeiro. */
  assert.equal(canCompanyRole("manager", {}, "suppliers.read"), true);
  assert.equal(canCompanyRole("manager", {}, "suppliers.write"), true);
  assert.equal(canCompanyRole("production", {}, "suppliers.read"), true);
  /* Producao ve de quem se compra, mas nao cadastra nem lanca compra. */
  assert.equal(canCompanyRole("production", {}, "suppliers.write"), false);
  /* Vendas e visualizador continuam de fora por padrao. */
  assert.equal(canCompanyRole("sales", {}, "suppliers.read"), false);
  assert.equal(canCompanyRole("viewer", {}, "suppliers.read"), false);

  /* E quem entrava pelo financeiro continua entrando: e a heranca que segura
     isso, e ela nao pode ter sido perdida na virada. */
  assert.equal(canCompanyRole("financial", {}, "suppliers.read"), true);
  assert.equal(canCompanyRole("sales", { "financial.read": true }, "suppliers.read"), true);
  assert.equal(canCompanyRole("viewer", { "financial.write": true }, "suppliers.write"), true);

  /* O ajuste especifico continua vencendo os dois lados. */
  assert.equal(canCompanyRole("manager", { "suppliers.read": false }, "suppliers.read"), false);
});

test("chave nova responde igual à antiga enquanto ninguém configurar nada", () => {
  /* A garantia central da separação por tela, escrita uma vez e valendo para
     todas as chaves de uma vez.

     Toda chave criada nas migrações 126, 127 e 128 nasce sem papel padrão e
     herdando a chave que respondia por aquilo antes. Enquanto o dono não marcar
     nada, a resposta das duas tem de ser IDÊNTICA, papel a papel. É isso que
     permitiu apontar as rotas de gravação para a chave da tela sem tirar acesso
     de ninguém em produção.

     O dia em que alguém der papel padrão a uma chave nova, ou apontar a herança
     para outro lugar, este teste cai — e tem de cair, porque nesse dia alguém
     ganha ou perde acesso. */
  const novas = CATALOGO.flatMap((m) => m.acoes).filter(
    (a) => a.herdaDe && a.papeisPadrao.length === 0
  );

  /* Se este número cair para perto de zero, o teste virou enfeite. */
  assert.ok(novas.length > 50, `esperava dezenas de chaves herdeiras, achei ${novas.length}`);

  for (const acao of novas) {
    for (const papel of PAPEIS) {
      assert.equal(
        canCompanyRole(papel, {}, acao.chave),
        canCompanyRole(papel, {}, acao.herdaDe!),
        `${acao.chave} responde diferente de ${acao.herdaDe} para ${papel}`
      );
    }
  }
});

test("toda tela declarada tem ação, e toda ação de tela tem tela declarada", () => {
  /* Tela sem ação vira um bloco vazio no painel; ação apontando para uma tela
     que não existe some do painel e nunca mais é configurada. Os dois casos já
     aconteceram: a página "Relatórios financeiros" ficou órfã quando a chave
     dela saiu do catálogo. */
  for (const modulo of CATALOGO) {
    const declaradas = new Set((modulo.paginas ?? []).map((p) => p.chave));

    for (const pagina of modulo.paginas ?? []) {
      const acoes = modulo.acoes.filter((a) => a.pagina === pagina.chave);
      assert.ok(
        acoes.length > 0,
        `a tela "${pagina.rotulo}" de ${modulo.chave} não tem ação nenhuma`
      );
    }

    for (const acao of modulo.acoes) {
      if (!acao.pagina) continue;
      assert.ok(
        declaradas.has(acao.pagina),
        `${acao.chave} aponta para a tela "${acao.pagina}", que ${modulo.chave} não declara`
      );
    }
  }
});
