/**
 * PGLite forward-reference bootstrap tests.
 *
 * Validates the contract of `PGLiteEngine#applyForwardReferenceBootstrap`:
 * given a brain that lacks the schema-blob's forward-referenced state, the
 * bootstrap adds enough state for PGLITE_SCHEMA_SQL to replay safely.
 *
 * The bootstrap covers the wedge incidents from issues
 * #239/#266/#357/#366/#374/#375/#378/#396 — every gbrain release that added
 * a column-with-index in the schema blob without a corresponding bootstrap
 * triggered the same wedge family.
 *
 * Honest limitation: test 4 simulates a v20 brain by dropping known forward
 * state from a fresh-LATEST instance. This is the same down-mutation pattern
 * codex flagged as "weak simulation" — it can't simulate every possible
 * historical state. Acceptable here because the bootstrap's contract is
 * narrow ("given a brain that lacks the specific forward-references,
 * initSchema produces a brain at LATEST"), and that contract is exactly
 * what this test exercises.
 */

import { describe, test, expect } from 'bun:test';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { LATEST_VERSION } from '../src/core/migrate.ts';

// Tier 3 opt-out: this file tests the cold init / bootstrap path explicitly.
// If GBRAIN_PGLITE_SNAPSHOT is set (ci:local sets it for unit shards), every
// PGlite would boot post-initSchema and these assertions ("0 tables on fresh
// install", "bootstrap converts pre-v0.18 brain to LATEST") would fail
// trivially. Unset for this file's process.
delete process.env.GBRAIN_PGLITE_SNAPSHOT;

describe('PGLiteEngine#applyForwardReferenceBootstrap', () => {
  test('no-op on fresh install (no pages or links table)', async () => {
    const engine = new PGLiteEngine();
    await engine.connect({});
    try {
      // Don't call initSchema — verify bootstrap alone does nothing on empty DB
      await (engine as any).applyForwardReferenceBootstrap();
      const { rows } = await (engine as any).db.query(`
        SELECT COUNT(*)::int AS c FROM information_schema.tables
        WHERE table_schema = 'public'
      `);
      expect(rows[0].c).toBe(0);
    } finally {
      await engine.disconnect();
    }
  }, 30000);

  test('idempotent: calling twice produces same result', async () => {
    const engine = new PGLiteEngine();
    await engine.connect({});
    try {
      await engine.initSchema();
      const db = (engine as any).db;

      // Mutate to pre-v0.18 shape: drop source_id and the sources FK target
      await db.exec(`
        ALTER TABLE pages DROP CONSTRAINT IF EXISTS pages_source_slug_key;
        ALTER TABLE pages ADD CONSTRAINT pages_slug_key UNIQUE (slug);
        DROP INDEX IF EXISTS idx_pages_source_id;
        ALTER TABLE pages DROP COLUMN IF EXISTS source_id;
        DROP TABLE IF EXISTS sources CASCADE;
      `);

      // First call: applies bootstrap
      await (engine as any).applyForwardReferenceBootstrap();
      // Second call: must not error, must not duplicate state
      await (engine as any).applyForwardReferenceBootstrap();

      const { rows: cols } = await db.query(`
        SELECT column_name FROM information_schema.columns
        WHERE table_name = 'pages' AND column_name = 'source_id'
      `);
      expect(cols).toHaveLength(1);

      const { rows: src } = await db.query(`SELECT COUNT(*)::int AS c FROM sources`);
      expect(src[0].c).toBe(1); // 'default' seed not duplicated
    } finally {
      await engine.disconnect();
    }
  }, 30000);

  test('no-op on modern brain (source_id and links provenance already present)', async () => {
    const engine = new PGLiteEngine();
    await engine.connect({});
    try {
      await engine.initSchema();
      const db = (engine as any).db;

      const before = await db.query(`SELECT COUNT(*)::int AS c FROM sources`);
      await (engine as any).applyForwardReferenceBootstrap();
      const after = await db.query(`SELECT COUNT(*)::int AS c FROM sources`);

      // Bootstrap probe should detect the brain is modern and skip the seed insert
      expect(after.rows[0].c).toBe(before.rows[0].c);
    } finally {
      await engine.disconnect();
    }
  }, 30000);

  test('full path: pre-v0.18 brain reaches LATEST_VERSION via initSchema', async () => {
    const engine = new PGLiteEngine();
    await engine.connect({});
    try {
      await engine.initSchema();
      const db = (engine as any).db;

      // Mutate to pre-v0.18 shape: strip the forward-referenced state.
      // Match the shape from #399's regression fixture; constraints first
      // (so dropping columns succeeds).
      await db.exec(`
        ALTER TABLE pages DROP CONSTRAINT IF EXISTS pages_source_slug_key;
        ALTER TABLE pages ADD CONSTRAINT pages_slug_key UNIQUE (slug);
        DROP INDEX IF EXISTS idx_pages_source_id;
        ALTER TABLE pages DROP COLUMN IF EXISTS source_id;
        DROP TABLE IF EXISTS sources CASCADE;
        ALTER TABLE links DROP CONSTRAINT IF EXISTS links_resolution_type_check;
        ALTER TABLE links DROP COLUMN IF EXISTS resolution_type;
      `);
      await engine.setConfig('version', '20');

      // Path under test: bootstrap → SCHEMA_SQL → runMigrations
      await engine.initSchema();

      expect(await engine.getConfig('version')).toBe(String(LATEST_VERSION));

      const { rows: srcCol } = await db.query(`
        SELECT column_name FROM information_schema.columns
        WHERE table_name = 'pages' AND column_name = 'source_id'
      `);
      expect(srcCol).toHaveLength(1);

      const { rows: defaultSrc } = await db.query(`SELECT id FROM sources WHERE id = 'default'`);
      expect(defaultSrc).toHaveLength(1);
    } finally {
      await engine.disconnect();
    }
  }, 30000);

  test('fresh install regression: initSchema on empty DB produces LATEST', async () => {
    // The bootstrap's table-existence probe must not mis-classify "no table"
    // as "pre-v0.18 brain." Without the table-existence guard, the bootstrap
    // would call runMigrations against an empty DB and crash on
    // `relation "config" does not exist`. Regression test for that path.
    const engine = new PGLiteEngine();
    await engine.connect({});
    try {
      await engine.initSchema();
      expect(await engine.getConfig('version')).toBe(String(LATEST_VERSION));

      const db = (engine as any).db;
      const pages = await db.query(`SELECT 1 FROM pages LIMIT 0`);
      const sources = await db.query(`SELECT 1 FROM sources LIMIT 0`);
      const config = await db.query(`SELECT 1 FROM config LIMIT 0`);
      expect(pages).toBeDefined();
      expect(sources).toBeDefined();
      expect(config).toBeDefined();
    } finally {
      await engine.disconnect();
    }
  }, 30000);

  test('pre-v0.13 links shape: bootstrap adds link_source + origin_page_id', async () => {
    // Issues #266 / #357 — pre-v0.13 brains had `links` without
    // `link_source` / `origin_page_id`. Schema blob's
    // `CREATE INDEX idx_links_source` would crash before v11 ran.
    const engine = new PGLiteEngine();
    await engine.connect({});
    try {
      await engine.initSchema();
      const db = (engine as any).db;

      await db.exec(`
        DROP INDEX IF EXISTS idx_links_source;
        DROP INDEX IF EXISTS idx_links_origin;
        ALTER TABLE links DROP CONSTRAINT IF EXISTS links_from_to_type_source_origin_unique;
        ALTER TABLE links DROP COLUMN IF EXISTS link_source;
        ALTER TABLE links DROP COLUMN IF EXISTS origin_page_id;
      `);

      await (engine as any).applyForwardReferenceBootstrap();

      const { rows: lsCol } = await db.query(`
        SELECT column_name FROM information_schema.columns
        WHERE table_name = 'links' AND column_name = 'link_source'
      `);
      expect(lsCol).toHaveLength(1);

      const { rows: opCol } = await db.query(`
        SELECT column_name FROM information_schema.columns
        WHERE table_name = 'links' AND column_name = 'origin_page_id'
      `);
      expect(opCol).toHaveLength(1);
    } finally {
      await engine.disconnect();
    }
  }, 30000);

  test('pre-v121 timeline shape reaches LATEST through full initSchema', async () => {
    const engine = new PGLiteEngine();
    await engine.connect({});
    try {
      await engine.initSchema();
      const db = (engine as any).db;
      await db.exec(`
        DROP INDEX IF EXISTS idx_timeline_event_dedup;
        DROP INDEX IF EXISTS idx_timeline_event_page;
        ALTER TABLE timeline_entries DROP CONSTRAINT IF EXISTS timeline_entries_event_page_id_fkey;
        ALTER TABLE timeline_entries DROP COLUMN IF EXISTS event_page_id;
      `);
      await engine.setConfig('version', '119');

      await engine.initSchema();
      await engine.initSchema();

      expect(await engine.getConfig('version')).toBe(String(LATEST_VERSION));
      const { rows } = await db.query(`
        SELECT column_name FROM information_schema.columns
        WHERE table_name = 'timeline_entries' AND column_name = 'event_page_id'
      `);
      expect(rows).toHaveLength(1);
    } finally {
      await engine.disconnect();
    }
  }, 30000);

  test('pre-v121 partial bootstrap resumes without skipping migration work', async () => {
    const engine = new PGLiteEngine();
    await engine.connect({});
    try {
      await engine.initSchema();
      const db = (engine as any).db;
      await db.exec(`
        DROP INDEX IF EXISTS idx_timeline_event_dedup;
        DROP INDEX IF EXISTS idx_timeline_event_page;
        ALTER TABLE timeline_entries DROP CONSTRAINT IF EXISTS timeline_entries_event_page_id_fkey;
      `);
      await engine.setConfig('version', '119');

      // Simulates interruption after bootstrap added the column but before
      // schema-blob replay and migration v121 completed the FK/index work.
      await engine.initSchema();

      expect(await engine.getConfig('version')).toBe(String(LATEST_VERSION));
      const { rows } = await db.query(`
        SELECT to_regclass('idx_timeline_event_page') AS lookup_idx,
               to_regclass('idx_timeline_event_dedup') AS dedup_idx
      `);
      expect(rows[0]?.lookup_idx).not.toBeNull();
      expect(rows[0]?.dedup_idx).not.toBeNull();
    } finally {
      await engine.disconnect();
    }
  }, 30000);
});
