import * as fs from "fs";
import * as mysql from "mysql2/promise";
import * as path from "path";
import { listSourceMigrationClassNames } from "./migration-source-list";
import { getTallerSchemas, loadRootEnv } from "./schema-snapshot";

// Compara el esquema que producen las migraciones (contra una base
// genuinamente nueva) con el esquema real de la demo (mbinvautocentrorodriguez)
// — y falla si aparece CUALQUIER diferencia que no este en
// tenant-schema-divergences-accepted.json. Mismo principio que el trinquete
// de ofensas (tenancy/tenant-aware-access.spec.ts): agregar una entrada
// nueva, o cambiar el string guardado de una existente, requiere aprobacion
// explicita del usuario en el diff — nunca un ajuste silencioso para que la
// prueba pase.
//
// Corre contra TENANT_CLI_DB_* (la demo real) y test_default_sentinel (una
// de las tres bases dedicadas de aislamiento — ver test/e2e-helpers.ts,
// ALLOWED_ISOLATION_TEST_DATABASES). Si esa base no existe o no tiene las
// migraciones aplicadas, esta prueba falla con un error de conexion — eso es
// correcto, no un falso negativo: sin una base fresca migrada no hay nada
// que comparar.
const FRESH_TENANT_DATABASE = "test_default_sentinel";

const accepted: {
  droppedInFresh: Record<string, { motivo: string }>;
  newInFresh: Record<string, { motivo: string }>;
  columnDivergences: Record<string, string>;
} = JSON.parse(
  fs.readFileSync(
    path.join(__dirname, "tenant-schema-divergences-accepted.json"),
    "utf8",
  ),
);

// Las entradas "_nota_*" y "_motivo_general_*" en columnDivergences son
// documentacion, no tablas — se descartan antes de comparar.
const acceptedColumnDivergences = Object.fromEntries(
  Object.entries(accepted.columnDivergences).filter(
    ([key]) => !key.startsWith("_"),
  ),
);

async function getAppliedMigrationNames(database: string): Promise<string[]> {
  const conn = await mysql.createConnection({
    host: process.env.TENANT_CLI_DB_HOST,
    port: Number(process.env.TENANT_CLI_DB_PORT),
    user: process.env.TENANT_CLI_DB_USERNAME,
    password: process.env.TENANT_CLI_DB_PASSWORD,
    database,
  });
  try {
    const [rows] = await conn.query<mysql.RowDataPacket[]>(
      "SELECT name FROM migrations ORDER BY name",
    );
    return rows.map((r) => (r as any).name);
  } finally {
    await conn.end();
  }
}

// Logica PURA (sin I/O), separada a proposito de assertBaselineUpToDate()
// para poder probarla con datos sinteticos — igual que "el control positivo
// detecta un instrumento roto" en tenant-isolation-required.e2e-spec.ts, sin
// tocar ninguna base real.
function checkBaselineSync(
  sourceNames: string[],
  appliedNames: string[],
): { missing: string[]; orphaned: string[] } {
  const sourceSet = new Set(sourceNames);
  const appliedSet = new Set(appliedNames);
  return {
    missing: [...sourceSet].filter((n) => !appliedSet.has(n)).sort(),
    orphaned: [...appliedSet].filter((n) => !sourceSet.has(n)).sort(),
  };
}

function buildBaselineErrorMessage(
  database: string,
  missing: string[],
  orphaned: string[],
): string {
  const parts: string[] = [];
  if (missing.length > 0) {
    parts.push(
      `Faltan aplicar en ${database}: ${missing.join(", ")}. ` +
        `Corre 'TENANT_CLI_DB_DATABASE=${database} npm run migration:run' antes de comparar.`,
    );
  }
  if (orphaned.length > 0) {
    parts.push(
      `${database} tiene aplicadas migraciones que ya no existen en el codigo fuente: ` +
        `${orphaned.join(", ")}. La base esta desincronizada del historial — revisar a mano.`,
    );
  }
  return (
    `La baseline (${database}) esta desactualizada respecto del codigo fuente — no se compara nada. ` +
    parts.join(" ")
  );
}

// Sondeo OBLIGATORIO antes de comparar nada mas — mismo motivo que el
// control positivo: sin esto, alguien agrega una migracion nueva, se olvida
// de re-migrar test_default_sentinel, y esta prueba compara contra una foto
// VIEJA de esa base y da verde igual. Un verde que no significa lo que
// parece es peor que un rojo.
async function assertBaselineUpToDate(database: string): Promise<void> {
  const sourceNames = listSourceMigrationClassNames();
  const appliedNames = await getAppliedMigrationNames(database);
  const { missing, orphaned } = checkBaselineSync(sourceNames, appliedNames);

  if (missing.length > 0 || orphaned.length > 0) {
    throw new Error(buildBaselineErrorMessage(database, missing, orphaned));
  }
}

describe("esquema de un tenant nuevo vs la demo real (divergencias aceptadas)", () => {
  let demoSchema: Record<string, string>;
  let freshSchema: Record<string, string>;

  beforeAll(async () => {
    loadRootEnv();
    await assertBaselineUpToDate(FRESH_TENANT_DATABASE);
    demoSchema = await getTallerSchemas(process.env.TENANT_CLI_DB_DATABASE!);
    freshSchema = await getTallerSchemas(FRESH_TENANT_DATABASE);
  }, 30000);

  it("las tablas que faltan en el tenant nuevo son exactamente las de droppedInFresh", () => {
    const demoTables = new Set(Object.keys(demoSchema));
    const freshTables = new Set(Object.keys(freshSchema));
    const actualDropped = [...demoTables].filter((t) => !freshTables.has(t)).sort();
    const acceptedDropped = Object.keys(accepted.droppedInFresh).sort();

    expect(actualDropped).toEqual(acceptedDropped);
  });

  it("las tablas de mas en el tenant nuevo son exactamente las de newInFresh", () => {
    const demoTables = new Set(Object.keys(demoSchema));
    const freshTables = new Set(Object.keys(freshSchema));
    const actualNew = [...freshTables].filter((t) => !demoTables.has(t)).sort();
    const acceptedNew = Object.keys(accepted.newInFresh).sort();

    expect(actualNew).toEqual(acceptedNew);
  });

  it("toda tabla que difiere de la demo esta en columnDivergences, con el string EXACTO aprobado", () => {
    const problems: string[] = [];

    for (const table of Object.keys(demoSchema)) {
      if (!(table in freshSchema)) continue; // cubierto por droppedInFresh

      if (demoSchema[table] === freshSchema[table]) {
        // Coincide con la demo — si estuviera en la lista aprobada, ya no
        // hace falta (informativo, no falla la prueba: una divergencia que
        // se resuelve del todo es una buena noticia).
        continue;
      }

      const approved = acceptedColumnDivergences[table];
      if (!approved) {
        problems.push(
          `${table}: difiere de la demo y NO esta en tenant-schema-divergences-accepted.json.\n` +
            `  DEMO:  ${demoSchema[table]}\n` +
            `  FRESH: ${freshSchema[table]}`,
        );
        continue;
      }

      if (approved !== freshSchema[table]) {
        problems.push(
          `${table}: el esquema real de un tenant nuevo YA NO coincide con el string aprobado ` +
            "en tenant-schema-divergences-accepted.json — algo cambio sin actualizar la lista.\n" +
            `  APROBADO: ${approved}\n` +
            `  ACTUAL:   ${freshSchema[table]}`,
        );
      }
    }

    if (problems.length > 0) {
      throw new Error(
        `${problems.length} divergencia(s) sin aprobar o desactualizada(s):\n\n` +
          problems.join("\n\n") +
          "\n\nSi es una divergencia nueva y legitima, agregala a " +
          "tenant-schema-divergences-accepted.json con su motivo — nunca en silencio.",
      );
    }
  });

  it("ninguna tabla lista en columnDivergences deberia coincidir YA con la demo (lista viva, no acumula entradas muertas)", () => {
    const staleEntries = Object.keys(acceptedColumnDivergences).filter(
      (table) => demoSchema[table] && demoSchema[table] === freshSchema[table],
    );
    if (staleEntries.length > 0) {
      throw new Error(
        `Estas tablas ya NO divergen de la demo pero siguen en la lista aprobada: ${staleEntries.join(", ")}. ` +
          "Sacarlas de tenant-schema-divergences-accepted.json (buena noticia: se resolvio del todo).",
      );
    }
  });
});

// Prueba de la prueba: si test_default_sentinel se queda atras (alguien
// agrega una migracion nueva y no la corre ahi), el guard tiene que
// bloquear la comparacion — no compararla contra una foto vieja y dar verde
// igual. Sintetico, sin tocar ninguna base real, mismo patron que "el
// control positivo detecta un instrumento roto".
describe("assertBaselineUpToDate detecta una baseline atrasada (sintetico)", () => {
  it("con una migracion en el codigo que falta aplicar, reporta 'Faltan aplicar' con el nombre exacto", () => {
    const { missing, orphaned } = checkBaselineSync(
      ["CreateTallerTables1738852000000", "ReconcileOtCosasTrae1770000000000"],
      ["CreateTallerTables1738852000000"],
    );
    expect(missing).toEqual(["ReconcileOtCosasTrae1770000000000"]);
    expect(orphaned).toEqual([]);

    const message = buildBaselineErrorMessage("test_default_sentinel", missing, orphaned);
    expect(message).toContain("desactualizada");
    expect(message).toContain("Faltan aplicar en test_default_sentinel");
    expect(message).toContain("ReconcileOtCosasTrae1770000000000");
    expect(message).toContain("migration:run");
  });

  it("con una migracion aplicada que ya no existe en el codigo, reporta 'desincronizada' con el nombre exacto", () => {
    const { missing, orphaned } = checkBaselineSync(
      ["CreateTallerTables1738852000000"],
      ["CreateTallerTables1738852000000", "UnaMigracionBorradaDelCodigo1234567890000"],
    );
    expect(missing).toEqual([]);
    expect(orphaned).toEqual(["UnaMigracionBorradaDelCodigo1234567890000"]);

    const message = buildBaselineErrorMessage("test_default_sentinel", missing, orphaned);
    expect(message).toContain("desincronizada");
    expect(message).toContain("UnaMigracionBorradaDelCodigo1234567890000");
  });

  it("con las mismas migraciones en ambos lados, no detecta nada", () => {
    const names = ["CreateTallerTables1738852000000", "ReconcileOtCosasTrae1770000000000"];
    const { missing, orphaned } = checkBaselineSync(names, [...names]);
    expect(missing).toEqual([]);
    expect(orphaned).toEqual([]);
  });

  it("listSourceMigrationClassNames() lee los archivos reales y produce nombres con el patron {ClassName}{timestamp}", () => {
    const names = listSourceMigrationClassNames();
    expect(names.length).toBeGreaterThan(0);
    expect(names).toContain("CreateTallerTables1738852000000");
    expect(names).toContain("CreateTallerUsuarios1740102000000");
    expect(names).toContain("DropUnusedSyncEvents1770700000000");
    // Nada de la carpeta verification/ (este archivo, el JSON) se coló:
    // fs.readdirSync no entra a subcarpetas, pero se confirma que el
    // resultado sigue teniendo forma de nombre de clase de migracion.
    for (const name of names) {
      expect(name).toMatch(/^[A-Za-z]+\d+$/);
    }
  });
});
