import './assert_dev_env.js';
/**
 * Test database fixtures for parameterized testing.
 *
 * Provides factory builders for creating test databases with pglite (in-memory)
 * and pg (PostgreSQL) drivers. Consumer projects provide their own schema
 * initialization via `run_migrations` and compose factories into test suites.
 *
 * @example
 * ```ts
 * import {create_pglite_factory, create_pg_factory} from '@fuzdev/fuz_app/testing/db.js';
 * import {run_migrations} from '@fuzdev/fuz_app/db/migrate.js';
 * import {auth_migration_ns} from '@fuzdev/fuz_app/auth/migrations.js';
 *
 * const init_schema = async (db) => {
 *   await run_migrations(db, [auth_migration_ns]);
 * };
 * const pglite_factory = create_pglite_factory(init_schema);
 * const pg_factory = create_pg_factory(init_schema, process.env.TEST_DATABASE_URL);
 * export const db_factories = [pglite_factory, pg_factory];
 * ```
 *
 * @module
 */
import { describe, beforeAll, beforeEach, afterAll } from 'vitest';
import { create_pglite_db } from '../db/db_pglite.js';
import { assert_valid_sql_identifier } from '../db/sql_identifier.js';
import { create_pg_db } from '../db/db_pg.js';
/**
 * CI detection — `CI=true` is set automatically by GitHub Actions, GitLab CI, etc.
 */
export const IS_CI = process.env.CI === 'true';
/**
 * Reset a PGlite database to a clean state by dropping and recreating the public schema.
 *
 * Removes all tables, sequences, indexes, types, and functions.
 * The database instance remains usable after reset.
 *
 * @mutates db - drops the `public` schema and recreates it; all rows in all
 *   tables are gone after this returns.
 */
export const reset_pglite = async (db) => {
    await db.query('DROP SCHEMA public CASCADE');
    await db.query('CREATE SCHEMA public');
};
// Module-level PGlite cache — shared across all factories in the same vitest worker (one test file).
// Each vitest file runs in its own worker thread, so no cross-file contamination.
let module_db = null;
/**
 * Create a pglite (in-memory) database factory for tests.
 *
 * Always enabled — no external dependencies required.
 * Shares a single PGlite WASM instance across all factories in the same
 * vitest worker thread (one test file). Subsequent `create()` calls reset
 * the schema via `DROP SCHEMA public CASCADE` instead of paying the WASM
 * cold-start cost again.
 *
 * @param init_schema - callback to initialize the database schema
 */
export const create_pglite_factory = (init_schema) => ({
    name: 'pglite',
    skip: false,
    async create() {
        if (module_db) {
            await reset_pglite(module_db);
        }
        else {
            const { PGlite } = await import('@electric-sql/pglite');
            const pglite = new PGlite();
            module_db = create_pglite_db(pglite).db;
        }
        await init_schema(module_db);
        return module_db;
    },
    async close() {
        // No-op: shared instance lives for the worker thread's lifetime.
        // PGlite is cleaned up when the vitest worker exits.
    },
});
/**
 * Create a pg (PostgreSQL) database factory for tests.
 *
 * Skipped when `test_url` is not provided.
 * Drops `schema_version` before running `init_schema`, forcing migrations
 * to re-evaluate against the actual tables. Prevents stale tracker rows
 * from skipping migrations when DDL changes between test sessions.
 *
 * For full clean-slate behavior (recommended), call `drop_auth_schema(db)`
 * at the start of `init_schema` before running migrations. This handles
 * upstream schema changes that go beyond adding new tables/columns.
 *
 * @param init_schema - callback to initialize the database schema
 * @param test_url - PostgreSQL connection URL (e.g. from `TEST_DATABASE_URL`)
 * @returns a factory that creates pg databases. The returned `create()`
 *   throws when `test_url` is unset (despite the `skip: true` flag — defense
 *   against direct invocation), and rewrites Postgres "database does not
 *   exist" errors into a `createdb` hint message.
 */
export const create_pg_factory = (init_schema, test_url) => {
    const should_skip = !test_url;
    const skip_reason = !test_url ? 'TEST_DATABASE_URL not set' : undefined;
    let pool_ref = null;
    return {
        name: 'pg',
        skip: should_skip,
        skip_reason,
        async create() {
            if (!test_url) {
                throw new Error('TEST_DATABASE_URL required for pg tests.');
            }
            // Close any previous pool to prevent leaks on repeated create() calls.
            // With isolate: false, the pool may have been ended by a previous file's close().
            if (pool_ref) {
                try {
                    await pool_ref.end();
                }
                catch {
                    // pool already ended — safe to ignore
                }
                pool_ref = null;
            }
            const { Pool } = await import('pg');
            const pool = new Pool({ connectionString: test_url });
            pool_ref = pool;
            const { db } = create_pg_db(pool);
            try {
                // Drop schema_version so migrations re-evaluate against the actual
                // tables. Prevents stale tracker rows from skipping migrations
                // when DDL changes between test sessions. Migrations use
                // IF NOT EXISTS guards, so re-running is safe.
                await db.query('DROP TABLE IF EXISTS schema_version');
                await init_schema(db);
            }
            catch (error) {
                await pool.end();
                const msg = error instanceof Error ? error.message : String(error);
                if (msg.includes('does not exist')) {
                    const db_name = test_url.split('/').pop()?.split('?')[0] ?? 'test_db';
                    throw new Error(`Database "${db_name}" does not exist. Create it with: createdb ${db_name}`);
                }
                throw error;
            }
            return db;
        },
        async close() {
            if (pool_ref) {
                try {
                    await pool_ref.end();
                }
                catch {
                    // pool already ended — safe to ignore
                }
                pool_ref = null;
            }
        },
    };
};
/**
 * Auth table names in truncation order (children first for FK safety).
 *
 * Consumer projects can spread this into their own list and append app-specific tables.
 */
export const auth_truncate_tables = [
    'invite',
    'api_token',
    'auth_session',
    'role_grant',
    'role_grant_offer',
    'actor',
    'account',
];
/**
 * Auth tables including `audit_log` — for integration tests that exercise
 * the full middleware stack (login, admin, rate limiting).
 *
 * Separate from `auth_truncate_tables` because unit-level DB tests that don't
 * touch audit logging don't need to truncate it.
 */
export const auth_integration_truncate_tables = [...auth_truncate_tables, 'audit_log'];
/**
 * All auth tables in drop order (children first for FK safety).
 *
 * The full set created by `auth_migrations` — use for clean-slate
 * test DB initialization. `auth_truncate_tables` is the subset for
 * between-test data cleanup (excludes `audit_log`).
 *
 * When adding tables to `auth_migrations`, add them here too.
 */
export const auth_drop_tables = [
    'app_settings',
    'invite',
    'audit_log',
    'api_token',
    'auth_session',
    'role_grant',
    'role_grant_offer',
    'actor',
    'account',
    'bootstrap_lock',
];
/**
 * Drop all auth tables and schema version tracking for a clean slate.
 *
 * Recommended at the start of `init_schema` callbacks for `create_pg_factory`.
 * Persistent test databases can accumulate stale schema from previous fuz_app
 * versions — this ensures migrations run against a truly empty database.
 * Safe on fresh databases (`IF EXISTS` on all statements). No-op effect for
 * PGlite (already fresh), but harmless to call unconditionally.
 *
 * @mutates db - drops every table in `auth_drop_tables` plus `schema_version`.
 */
export const drop_auth_schema = async (db) => {
    for (const table of auth_drop_tables) {
        await db.query(`DROP TABLE IF EXISTS ${assert_valid_sql_identifier(table)} CASCADE`);
    }
    await db.query('DROP TABLE IF EXISTS schema_version CASCADE');
};
/**
 * Create a `describe_db` function bound to specific factories and truncate tables.
 *
 * Returns a 2-arg `(name, fn)` function that runs the test suite against each
 * factory. Each factory gets its own `describe` block with a shared database
 * instance, automatic `beforeEach` truncation, and `afterAll` cleanup.
 * Skipped factories use `describe.skip`.
 *
 * @param factories - one or more database factories to run suites against
 * @param truncate_tables - tables to truncate between tests (children first for FK safety)
 * @mutates the underlying database between tests — `beforeEach` issues
 *   `TRUNCATE <truncate_tables> CASCADE` against the shared instance.
 */
export const create_describe_db = (factories, truncate_tables) => {
    const factory_list = Array.isArray(factories) ? factories : [factories];
    return (name, fn) => {
        for (const factory of factory_list) {
            const describe_fn = factory.skip ? describe.skip : describe;
            describe_fn(`${name} (${factory.name})`, () => {
                let db;
                beforeAll(async () => {
                    db = await factory.create();
                });
                beforeEach(async () => {
                    if (db && truncate_tables.length > 0) {
                        await db.query(`TRUNCATE ${truncate_tables.map(assert_valid_sql_identifier).join(', ')} CASCADE`);
                    }
                });
                afterAll(async () => {
                    if (db)
                        await factory.close(db);
                });
                fn(() => db);
            });
        }
    };
};
/**
 * Log factory status to console.
 */
export const log_db_factory_status = (factories) => {
    const enabled = factories.filter((f) => !f.skip).map((f) => f.name);
    const skipped = factories.filter((f) => f.skip).map((f) => `${f.name} (${f.skip_reason})`);
    console.log(`[db tests] drivers: ${enabled.join(', ')}${skipped.length ? ` | skipped: ${skipped.join(', ')}` : ''}`);
};
