import * as fs from "fs";
import * as path from "path";
import { listTsFiles, scanCounts } from "./scan-tenant-unaware-access";

function loadJson<T>(file: string): T {
  return JSON.parse(fs.readFileSync(path.join(__dirname, file), "utf8"));
}

const offenseBaseline = loadJson<{ counts: Record<string, number> }>(
  "tenant-aware-baseline.json",
);
const approvedBypasses = loadJson<{
  approved: Record<string, { count: number; justificacion: string }>;
}>("tenant-aware-approved-bypasses.json");

function findRegressions(
  actual: Record<string, number>,
  baseline: Record<string, number>,
): Array<{ file: string; baseline: number; actual: number }> {
  const regressions: Array<{ file: string; baseline: number; actual: number }> =
    [];
  for (const [file, count] of Object.entries(actual)) {
    const baselineCount = baseline[file] ?? 0;
    if (count > baselineCount) {
      regressions.push({ file, baseline: baselineCount, actual: count });
    }
  }
  return regressions;
}

// Verificacion mecanica del 4(d): dos categorias separadas, a proposito.
//   OFENSAS  — inyeccion de repositorio/DataSource fuera del contexto de
//              tenant. Trinquete: solo falla si sube o aparece una nueva.
//   BYPASSES — withTenantRepositoryExplicit()/withTenantDataSourceExplicit(), contra una lista blanca
//              APROBADA (archivo + cantidad exacta + justificacion). Un
//              bypass fuera de la lista, o con mas usos de los aprobados,
//              rompe la prueba igual que un ofensor nuevo.
// Mezclar las dos categorias en una sola lista (como en la ronda anterior)
// bloqueaba la definicion de "listo" para siempre: auth necesita el bypass
// de forma permanente, no como deuda, asi que las ofensas SI pueden llegar
// a cero mientras el bypass aprobado sigue ahi.
describe("acceso a datos pasa por el contexto de tenant (4d)", () => {
  const srcDir = path.join(__dirname, "..");

  it("trinquete de OFENSAS: ninguna sube, ninguna nueva aparece", () => {
    const { offenses } = scanCounts(srcDir);
    const regressions = findRegressions(offenses, offenseBaseline.counts);

    const total = Object.values(offenses).reduce((a, b) => a + b, 0);
    // eslint-disable-next-line no-console
    console.log(
      `[tenant-aware-access] OFENSAS: ${total} reales hoy en ${Object.keys(offenses).length} archivo(s).`,
    );

    expect(regressions).toEqual([]);
  });

  it("trinquete de BYPASSES: solo los aprobados, nunca por encima de lo aprobado", () => {
    const { bypasses } = scanCounts(srcDir);
    const approvedCounts = Object.fromEntries(
      Object.entries(approvedBypasses.approved).map(([file, v]) => [
        file,
        v.count,
      ]),
    );
    const regressions = findRegressions(bypasses, approvedCounts);

    const total = Object.values(bypasses).reduce((a, b) => a + b, 0);
    // eslint-disable-next-line no-console
    console.log(
      `[tenant-aware-access] BYPASSES: ${total} reales hoy (aprobados: ${Object.values(approvedCounts).reduce((a, b) => a + b, 0)}).`,
    );

    expect(regressions).toEqual([]);
  });

  it("LISTO = ofensas en 0 Y bypasses exactamente iguales a lo aprobado (siempre corre, no hay que reactivar nada)", () => {
    const { offenses, bypasses } = scanCounts(srcDir);
    const totalOffenses = Object.values(offenses).reduce((a, b) => a + b, 0);

    if (totalOffenses > 0) {
      // Todavia hay conversion pendiente — esta prueba no aplica todavia.
      // (No usamos it.skip: un test salteado no lo reactiva nadie. Esta
      // prueba SIEMPRE corre; simplemente no exige nada mientras falte
      // conversion, y empieza a exigir en el momento exacto en que deja de
      // faltar, sin que nadie tenga que acordarse de tocar el archivo.)
      return;
    }

    const approvedCounts = Object.fromEntries(
      Object.entries(approvedBypasses.approved).map(([file, v]) => [
        file,
        v.count,
      ]),
    );
    expect(bypasses).toEqual(approvedCounts);
  });

  // Regla 7(a): mientras falte conversion, ninguna ruta de switch de tenant
  // puede existir. Se evalua sobre OFENSAS, nunca sobre bypasses — un bypass
  // aprobado (auth resolviendo su propio tenant) no es "falta de conversion".
  it("regla 7(a): ninguna ruta de switch de tenant existe mientras falten ofensas por convertir", () => {
    const { offenses } = scanCounts(srcDir);
    const totalOffenses = Object.values(offenses).reduce((a, b) => a + b, 0);

    if (totalOffenses === 0) {
      return;
    }

    const controllerFiles = listTsFiles(srcDir, srcDir).filter((f: string) =>
      f.endsWith(".controller.ts"),
    );

    const SWITCH_MARKERS = [/switch-tenant/i, /switchTenant/, /["']switch["']/];
    const offendingControllers: string[] = [];

    for (const relFile of controllerFiles) {
      const content = fs.readFileSync(path.join(srcDir, relFile), "utf8");
      if (SWITCH_MARKERS.some((m) => m.test(content))) {
        offendingControllers.push(relFile);
      }
    }

    expect(offendingControllers).toEqual([]);
  });
});
