import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import path from "node:path";
import pg from "pg";

const run = promisify(execFile);

/**
 * Migrations : rejouabilité et concurrence.
 *
 * `migrate.js` s'exécute au démarrage de CHAQUE conteneur API. C'est donc du
 * code de production critique — un échec ici empêche l'API de démarrer — et il
 * n'était couvert par aucun test.
 */
const BASE = process.env.INTEGRATION_DB_URL || "postgres://bm:bm@127.0.0.1:55432/bm_test";
const DB = "bm_migrate_test";
const DB_URL = BASE.replace(/\/[^/]+$/, `/${DB}`);
const REPO = path.resolve(import.meta.dirname, "../../../..");

const migrate = () =>
  run(process.execPath, ["apps/api/src/migrate.js"], {
    cwd: REPO,
    env: { ...process.env, DATABASE_URL: DB_URL },
  });

let admin, client;

beforeAll(async () => {
  admin = new pg.Client({ connectionString: BASE, connectionTimeoutMillis: 4000 });
  await admin.connect();
  // Base dédiée : les autres tests d'intégration ne doivent pas être perturbés.
  await admin.query(`DROP DATABASE IF EXISTS ${DB}`);
  await admin.query(`CREATE DATABASE ${DB}`);
  client = new pg.Client({ connectionString: DB_URL });
  await client.connect();
  await client.query("CREATE EXTENSION IF NOT EXISTS postgis");
}, 120000);

afterAll(async () => {
  await client?.end().catch(() => {});
  await admin?.query(`DROP DATABASE IF EXISTS ${DB}`).catch(() => {});
  await admin?.end().catch(() => {});
});

describe("migrate.js", () => {
  it("applique toutes les migrations sur une base vierge", async () => {
    const { stdout } = await migrate();
    expect(stdout).toMatch(/applying 001_initial_schema\.sql/);
    expect(stdout).toMatch(/applying 015_service_health\.sql/);

    const r = await client.query("SELECT count(*)::int AS n FROM schema_migrations");
    expect(r.rows[0].n).toBeGreaterThanOrEqual(15);
  }, 60000);

  it("est rejouable : un second passage n'applique rien", async () => {
    const { stdout } = await migrate();
    expect(stdout).toMatch(/nothing to apply/);
    expect(stdout).not.toMatch(/applying/);
  }, 60000);

  it("enregistre chaque fichier une seule fois", async () => {
    const r = await client.query(`
      SELECT version, count(*)::int AS n FROM schema_migrations
      GROUP BY version HAVING count(*) > 1
    `);
    expect(r.rows).toEqual([]);
  });

  it("applique les fichiers dans l'ordre lexicographique", async () => {
    const r = await client.query("SELECT version FROM schema_migrations ORDER BY applied_at, version");
    const versions = r.rows.map((x) => x.version);
    expect(versions).toEqual([...versions].sort());
  });

  /**
   * Le cas qui motivait le verrou consultatif : deux conteneurs API qui
   * démarrent en même temps lisaient tous deux schema_migrations vide et
   * appliquaient les mêmes fichiers en parallèle.
   */
  it("deux migrations simultanées sur une base vierge n'entrent pas en conflit", async () => {
    await admin.query(`DROP DATABASE IF EXISTS ${DB}_par`);
    await admin.query(`CREATE DATABASE ${DB}_par`);
    const parUrl = `${DB_URL}_par`;
    const c = new pg.Client({ connectionString: parUrl });
    await c.connect();
    await c.query("CREATE EXTENSION IF NOT EXISTS postgis");

    const runPar = () =>
      run(process.execPath, ["apps/api/src/migrate.js"], {
        cwd: REPO, env: { ...process.env, DATABASE_URL: parUrl },
      });

    // Les deux doivent réussir : le verrou sérialise, le second constate que
    // tout est déjà appliqué.
    const [a, b] = await Promise.all([runPar(), runPar()]);
    const outputs = [a.stdout, b.stdout];

    expect(outputs.some((o) => /applying 001_initial_schema/.test(o))).toBe(true);
    expect(outputs.some((o) => /nothing to apply/.test(o))).toBe(true);

    const r = await c.query(`
      SELECT version, count(*)::int AS n FROM schema_migrations
      GROUP BY version HAVING count(*) > 1
    `);
    expect(r.rows).toEqual([]);

    await c.end();
    await admin.query(`DROP DATABASE IF EXISTS ${DB}_par`);
  }, 120000);

  it("le verrou est relâché à la fin (une migration ultérieure n'attend pas)", async () => {
    const held = await client.query(
      "SELECT count(*)::int AS n FROM pg_locks WHERE locktype = 'advisory'"
    );
    expect(held.rows[0].n).toBe(0);
  });

  /**
   * Une migration invalide doit faire échouer le démarrage bruyamment, avec un
   * code de sortie non nul : Docker/Coolify s'en servent pour redémarrer, et un
   * schéma à moitié appliqué est pire qu'un conteneur qui refuse de démarrer.
   */
  it("échoue avec un code non nul si une migration est invalide", async () => {
    const fs = await import("node:fs");
    const bad = path.join(REPO, "apps/api/migrations/999_temoin_invalide.sql");
    fs.writeFileSync(bad, "CECI N'EST PAS DU SQL;\n");
    try {
      await expect(migrate()).rejects.toMatchObject({ code: 1 });
      // La migration fautive ne doit pas être enregistrée comme appliquée.
      const r = await client.query(
        "SELECT 1 FROM schema_migrations WHERE version = '999_temoin_invalide.sql'"
      );
      expect(r.rows).toEqual([]);
    } finally {
      fs.unlinkSync(bad);
    }
  }, 60000);
});
