import { writeFileSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { Pool } from "pg";
import { beforeAll, afterAll, beforeEach } from "vitest";
import type { Logger } from "../logger.js";

// Test database configuration
const TEST_DATABASE_URL =
  process.env.TEST_DATABASE_URL || "postgresql://test:test@localhost:5433/llm_gateway_test";
const PG_TESTS_ENABLED = process.env.PG_TESTS === "1";
const MIGRATION_LOCK_KEY = 88421173;
const CLEANUP_LOCK_KEY = 88421174;

// Test isolation: route the gateway's async job persistence to an in-process
// MemoryJobStore so tests don't touch ~/.llm-cli-gateway/logs.db. Async tools
// stay fully registered (unlike the old LLM_GATEWAY_LOGS_DB=none, which would
// now disable them entirely under the new structural-invariant model).
if (process.env.LLM_GATEWAY_CONFIG === undefined) {
  const testConfigPath = join(tmpdir(), `llm-cli-gateway-test-config-${process.pid}.toml`);
  writeFileSync(
    testConfigPath,
    ["[persistence]", 'backend = "memory"', "acknowledgeEphemeral = true", ""].join("\n")
  );
  process.env.LLM_GATEWAY_CONFIG = testConfigPath;
}
// Clear the legacy env vars so they don't override the test config.
delete process.env.LLM_GATEWAY_LOGS_DB;
delete process.env.LLM_GATEWAY_JOBS_DB;

let testPool: Pool | null = null;

/**
 * Mock logger for tests
 */
export const mockLogger: Logger = {
  info: () => {},
  error: () => {},
  debug: () => {},
};

/**
 * Setup test database connections
 */
export async function setupTestDatabase(): Promise<{ pool: Pool }> {
  if (!testPool) {
    testPool = new Pool({ connectionString: TEST_DATABASE_URL });

    const client = await testPool.connect();
    try {
      // Serialize schema bootstrap across parallel Vitest workers/processes.
      await client.query("SELECT pg_advisory_lock($1)", [MIGRATION_LOCK_KEY]);
      try {
        const migrationSql = `
          -- Create sessions table
          CREATE TABLE IF NOT EXISTS sessions (
            id TEXT PRIMARY KEY,
            cli VARCHAR(32) NOT NULL CHECK (cli IN ('claude', 'codex', 'gemini', 'grok', 'mistral', 'grok-api')),
            description TEXT,
            metadata JSONB DEFAULT '{}'::JSONB,
            created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
            last_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
          );

          -- Create active_sessions table
          CREATE TABLE IF NOT EXISTS active_sessions (
            cli VARCHAR(32) PRIMARY KEY CHECK (cli IN ('claude', 'codex', 'gemini', 'grok', 'mistral', 'grok-api')),
            session_id TEXT REFERENCES sessions(id) ON DELETE CASCADE,
            updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
          );

          -- Indexes
          CREATE INDEX IF NOT EXISTS idx_sessions_cli ON sessions(cli);
          CREATE INDEX IF NOT EXISTS idx_sessions_last_used_at ON sessions(last_used_at DESC);
          CREATE INDEX IF NOT EXISTS idx_sessions_metadata ON sessions USING GIN(metadata);
          CREATE INDEX IF NOT EXISTS idx_sessions_cli_last_used ON sessions(cli, last_used_at DESC);

          -- Normalize legacy UUID schemas to opaque string IDs for compatibility.
          DO $$
          BEGIN
            IF EXISTS (
              SELECT 1
              FROM information_schema.columns
              WHERE table_schema = 'public'
                AND table_name = 'sessions'
                AND column_name = 'id'
                AND udt_name = 'uuid'
            ) THEN
              ALTER TABLE active_sessions DROP CONSTRAINT IF EXISTS active_sessions_session_id_fkey;
              ALTER TABLE sessions ALTER COLUMN id TYPE TEXT USING id::text;
              ALTER TABLE active_sessions ALTER COLUMN session_id TYPE TEXT USING session_id::text;
              ALTER TABLE active_sessions
                ADD CONSTRAINT active_sessions_session_id_fkey
                FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE;
            END IF;
          END;
          $$ LANGUAGE plpgsql;

          -- Normalize legacy provider constraints to the current provider set.
          ALTER TABLE sessions DROP CONSTRAINT IF EXISTS sessions_cli_check;
          ALTER TABLE sessions ALTER COLUMN cli TYPE VARCHAR(32);
          ALTER TABLE sessions
            ADD CONSTRAINT sessions_cli_check
            CHECK (cli IN ('claude', 'codex', 'gemini', 'grok', 'mistral', 'grok-api'));

          ALTER TABLE active_sessions DROP CONSTRAINT IF EXISTS active_sessions_cli_check;
          ALTER TABLE active_sessions ALTER COLUMN cli TYPE VARCHAR(32);
          ALTER TABLE active_sessions
            ADD CONSTRAINT active_sessions_cli_check
            CHECK (cli IN ('claude', 'codex', 'gemini', 'grok', 'mistral', 'grok-api'));
        `;

        await client.query(migrationSql);
      } finally {
        await client.query("SELECT pg_advisory_unlock($1)", [MIGRATION_LOCK_KEY]);
      }
    } finally {
      client.release();
    }
  }

  return { pool: testPool };
}

/**
 * Clean test database
 */
export async function cleanTestDatabase(): Promise<void> {
  if (testPool) {
    const client = await testPool.connect();
    try {
      await client.query("BEGIN");
      // xact lock is released automatically on COMMIT/ROLLBACK.
      await client.query("SELECT pg_advisory_xact_lock($1)", [CLEANUP_LOCK_KEY]);
      // Row deletes avoid TRUNCATE lock amplification across parallel workers.
      await client.query("DELETE FROM active_sessions");
      await client.query("DELETE FROM sessions");
      await client.query("COMMIT");
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  }
}

/**
 * Teardown test database connections
 */
export async function teardownTestDatabase(): Promise<void> {
  if (testPool) {
    await testPool.end();
    testPool = null;
  }
}

if (PG_TESTS_ENABLED) {
  // Global setup/teardown for PostgreSQL-backed test suites.
  beforeAll(async () => {
    await setupTestDatabase();
  });

  afterAll(async () => {
    await teardownTestDatabase();
  });

  // Clean database before each test.
  beforeEach(async () => {
    await cleanTestDatabase();
  });
}
