import { Database } from 'bun:sqlite';
import fs from 'fs';
import path from 'path';

import { afterEach, describe, expect, it } from 'vitest';

import { applyBaseSchema } from './base-schema.js';
import { initializeDatabaseSchema } from './bootstrap.js';
import { applyVersionedSchemaMigrations } from './migrations/index.js';
import { applyLegacySchemaMigrations } from './schema.js';

function tableColumns(database: Database, table: string): string[] {
  return (
    database.prepare(`PRAGMA table_info(${table})`).all() as Array<{
      name: string;
    }>
  ).map((column) => column.name);
}

function getAppliedSchemaMigrations(
  database: Database,
): Array<{ version: number; name: string }> {
  return database
    .prepare(
      `SELECT version, name
         FROM schema_migrations
        ORDER BY version ASC`,
    )
    .all() as Array<{ version: number; name: string }>;
}

function getExpectedSchemaMigrations(): Array<{
  version: number;
  name: string;
}> {
  return [
    { version: 1, name: 'legacy_schema_bundle' },
    { version: 2, name: 'scheduled_task_columns' },
    { version: 3, name: 'room_registration_canonical_columns' },
    { version: 4, name: 'message_seq_and_work_item_indexes' },
    { version: 5, name: 'sessions_composite_key' },
    { version: 6, name: 'chat_channel_metadata' },
    { version: 7, name: 'runtime_service_metadata' },
    { version: 8, name: 'paired_task_schema_cleanup' },
    { version: 9, name: 'paired_workspace_project_schema_cleanup' },
    { version: 10, name: 'paired_turn_provenance_upgrade' },
    { version: 11, name: 'owner_failure_count' },
    { version: 12, name: 'paired_verdict_and_step_telemetry' },
    { version: 13, name: 'message_source_kind' },
    { version: 14, name: 'work_item_attachments' },
    { version: 15, name: 'turn_progress_text' },
    { version: 16, name: 'room_skill_overrides' },
    { version: 17, name: 'scheduled_task_room_role' },
    { version: 18, name: 'paired_turn_output_attachments' },
    { version: 19, name: 'turn_progress_text_recovery' },
    { version: 20, name: 'arbiter_intervention_count' },
    { version: 21, name: 'reviewer_failure_count' },
    { version: 22, name: 'turn_progress_text_compat' },
  ];
}

describe('initializeDatabaseSchema', () => {
  const tempDirs: string[] = [];

  afterEach(() => {
    for (const tempDir of tempDirs.splice(0)) {
      fs.rmSync(tempDir, { recursive: true, force: true });
    }
  });

  it('records the legacy schema bundle as version 1 on a fresh database', () => {
    const database = new Database(':memory:');

    try {
      initializeDatabaseSchema(database);

      expect(getAppliedSchemaMigrations(database)).toEqual(
        getExpectedSchemaMigrations(),
      );
    } finally {
      database.close();
    }
  });

  it('does not duplicate schema migration rows on repeated initialization', () => {
    const database = new Database(':memory:');

    try {
      initializeDatabaseSchema(database);
      initializeDatabaseSchema(database);

      expect(getAppliedSchemaMigrations(database)).toEqual(
        getExpectedSchemaMigrations(),
      );
    } finally {
      database.close();
    }
  });

  it('backfills schema migration tracking for an existing pre-versioned database', () => {
    const tempDir = fs.mkdtempSync(
      path.join('/tmp', 'ejclaw-schema-migrations-'),
    );
    tempDirs.push(tempDir);
    const dbPath = path.join(tempDir, 'messages.db');
    const legacyDb = new Database(dbPath);

    try {
      applyBaseSchema(legacyDb);
      applyLegacySchemaMigrations(legacyDb, {
        assistantName: 'Andy',
      });
    } finally {
      legacyDb.close();
    }

    const reopened = new Database(dbPath);

    try {
      initializeDatabaseSchema(reopened);

      expect(getAppliedSchemaMigrations(reopened)).toEqual(
        getExpectedSchemaMigrations(),
      );
    } finally {
      reopened.close();
    }
  });

  it('backfills turn_progress_text columns when version 15 was recorded under a different name', () => {
    const database = new Database(':memory:');

    try {
      applyBaseSchema(database);

      // Reproduce a deployment that first shipped reviewer_failure_count as a
      // local migration numbered 15, before turn_progress_text claimed that
      // version upstream. The runner skips by version number, so migration 015
      // (turn_progress_text) would otherwise never run on this database.
      database.exec(`
        CREATE TABLE schema_migrations (
          version INTEGER PRIMARY KEY,
          name TEXT NOT NULL,
          applied_at TEXT NOT NULL DEFAULT (datetime('now'))
        )
      `);
      const collidedNames: Record<number, string> = {
        15: 'reviewer_failure_count',
      };
      for (let version = 1; version <= 15; version += 1) {
        database
          .prepare(
            'INSERT INTO schema_migrations (version, name) VALUES (?, ?)',
          )
          .run(version, collidedNames[version] ?? `legacy_${version}`);
      }
      // Precondition: the collided database is missing the progress columns.
      expect(tableColumns(database, 'paired_turns')).not.toContain(
        'progress_text',
      );

      applyVersionedSchemaMigrations(database, { assistantName: 'Andy' });

      const columns = tableColumns(database, 'paired_turns');
      expect(columns).toContain('progress_text');
      expect(columns).toContain('progress_updated_at');

      // The pre-existing version-15 row is left untouched; the compat migration
      // (version 22 after merge-renumbering) is what reconciles the schema.
      const version15 = database
        .prepare('SELECT name FROM schema_migrations WHERE version = 15')
        .get() as { name: string };
      expect(version15.name).toBe('reviewer_failure_count');
      const version22 = database
        .prepare('SELECT name FROM schema_migrations WHERE version = 22')
        .get() as { name: string } | null;
      expect(version22?.name).toBe('turn_progress_text_compat');
    } finally {
      database.close();
    }
  });
});
