import * as path from "path";
import { DataSource } from "typeorm";
import { getTallerSchemas, loadRootEnv } from "./schema-snapshot";

// Verifica la propiedad que el futuro runner del panel necesita: un
// reintento (revertir una migracion y volver a aplicarla) tiene que
// producir EXACTAMENTE el mismo esquema que un alta limpia que nunca se
// revirtio. Nace de un bug real, encontrado corriendo esto a mano (no
// razonando sobre el codigo): ReconcileOtCosasTrae1770000000000 dejaba un
// indice de soporte (IDX_ot_cosas_trae_ot_id, creado por su propio down()
// para poder borrar uk_ot_cosa sin romper el FK de ot_id) que su up() no
// sabia limpiar — un tenant que pasara por un reintento terminaba con un
// esquema DISTINTO al de un alta limpia. Corregido en ese archivo; esta
// prueba automatiza la verificacion para que no dependa de que alguien la
// repita a mano cada vez que se agregue o toque una reconciliacion.
//
// Alcance: las 8 migraciones de reconciliacion (timestamp >= 1770000000000).
// NO las 14+1 originales — esas nunca se editan (regla del proyecto) y
// revertirlas no esta probado como seguro; el bug que motiva esta prueba
// vive especificamente en la capa de reconciliacion que este trabajo
// escribio, no en el codigo heredado.
//
// Corre contra test_default_sentinel (ver test/e2e-helpers.ts,
// ALLOWED_ISOLATION_TEST_DATABASES) — nunca la demo real ni las bases con
// datos centinela de aislamiento (test_tenant_a/b). Verificado antes de
// escribir esto: las tablas que tocan estas 8 migraciones tienen 0 filas en
// test_default_sentinel, asi que ningun down() aborta por datos reales.
const ROUND_TRIP_DATABASE = "test_default_sentinel";
const RECONCILIATION_TIMESTAMP_FLOOR = 1770000000000;

function listReconciliationClassNames(): string[] {
  const migrationsDir = path.join(__dirname, "..");
  const fs = require("fs");
  return fs
    .readdirSync(migrationsDir)
    .filter((f: string) => f.endsWith(".ts"))
    .map((f: string) => {
      const match = f.match(/^(\d+)-(.+)\.ts$/);
      if (!match) throw new Error(`Nombre de archivo de migracion inesperado: ${f}`);
      return { timestamp: Number(match[1]), className: `${match[2]}${match[1]}` };
    })
    .filter((m: { timestamp: number }) => m.timestamp >= RECONCILIATION_TIMESTAMP_FLOOR)
    .sort((a: { timestamp: number }, b: { timestamp: number }) => a.timestamp - b.timestamp)
    .map((m: { className: string }) => m.className);
}

function buildDataSource(): DataSource {
  return new DataSource({
    type: "mysql",
    host: process.env.TENANT_CLI_DB_HOST,
    port: parseInt(process.env.TENANT_CLI_DB_PORT || "3306", 10),
    username: process.env.TENANT_CLI_DB_USERNAME,
    password: process.env.TENANT_CLI_DB_PASSWORD,
    database: ROUND_TRIP_DATABASE,
    entities: [path.join(__dirname, "..", "..", "!(control-plane)", "**", "*.entity{.ts,.js}")],
    migrations: [path.join(__dirname, "..", "*{.ts,.js}")],
    synchronize: false,
  });
}

describe("round-trip de las migraciones de reconciliacion (revertir + reaplicar == alta limpia)", () => {
  let dataSource: DataSource;
  let reconciliationClassNames: string[];

  beforeAll(async () => {
    loadRootEnv();
    reconciliationClassNames = listReconciliationClassNames();
    dataSource = buildDataSource();
    await dataSource.initialize();
  }, 30000);

  afterAll(async () => {
    try {
      // Red de seguridad: sin importar como termino la prueba (verde, roja,
      // o con una excepcion a mitad de camino), test_default_sentinel tiene
      // que quedar con las 8 reconciliaciones aplicadas — otras pruebas
      // (schema-divergence.spec.ts, el e2e de aislamiento) asumen esa base
      // migrada al dia. runMigrations() es un no-op si ya estan todas.
      if (dataSource?.isInitialized) {
        await dataSource.runMigrations();
        await dataSource.destroy();
      }
    } catch (e) {
      // eslint-disable-next-line no-console
      console.error(
        "ALERTA: no se pudo restaurar test_default_sentinel a full-migrated en afterAll:",
        e,
      );
      throw e;
    }
  }, 30000);

  it("revertir las 8 y reaplicarlas reproduce byte-a-byte el esquema de antes", async () => {
    const before = await getTallerSchemas(ROUND_TRIP_DATABASE);

    for (let i = 0; i < reconciliationClassNames.length; i++) {
      await dataSource.undoLastMigration({ transaction: "each" });
    }

    const appliedAfterRevert: Array<{ name: string }> = await dataSource.query(
      "SELECT name FROM migrations ORDER BY name",
    );
    for (const className of reconciliationClassNames) {
      expect(appliedAfterRevert.map((r) => r.name)).not.toContain(className);
    }

    await dataSource.runMigrations();

    const appliedAfterReapply: Array<{ name: string }> = await dataSource.query(
      "SELECT name FROM migrations ORDER BY name",
    );
    for (const className of reconciliationClassNames) {
      expect(appliedAfterReapply.map((r) => r.name)).toContain(className);
    }

    const after = await getTallerSchemas(ROUND_TRIP_DATABASE);

    const tables = new Set([...Object.keys(before), ...Object.keys(after)]);
    const diffs: string[] = [];
    for (const table of tables) {
      if (before[table] !== after[table]) {
        diffs.push(
          `${table}:\n  ANTES:    ${before[table] ?? "(no existia)"}\n  DESPUES:  ${after[table] ?? "(no existe)"}`,
        );
      }
    }

    if (diffs.length > 0) {
      throw new Error(
        `${diffs.length} tabla(s) quedaron con un esquema distinto despues del round-trip ` +
          "(revertir + reaplicar deberia ser un no-op sobre el esquema):\n\n" +
          diffs.join("\n\n"),
      );
    }
  }, 60000);
});
