import './assert_dev_env.js';
import { z } from 'zod';
import { Logger } from '@fuzdev/fuz_util/log.js';
import { ROLE_KEEPER } from '../auth/role_schema.js';
import { create_validated_keyring } from '../auth/keyring.js';
import { generate_api_token } from '../auth/api_token.js';
import { query_create_account_with_actor } from '../auth/account_queries.js';
import { query_create_role_grant } from '../auth/role_grant_queries.js';
import { generate_session_token, hash_session_token, AUTH_SESSION_LIFETIME_MS, query_create_session, } from '../auth/session_queries.js';
import { query_create_api_token } from '../auth/api_token_queries.js';
import { create_session_cookie_value } from '../auth/session_cookie.js';
import { run_migrations } from '../db/migrate.js';
import { auth_migration_ns } from '../auth/migrations.js';
import { default_audit_factory } from '../server/app_backend.js';
import { create_app_server, } from '../server/app_server.js';
import { generate_daemon_token, DAEMON_TOKEN_HEADER, } from '../auth/daemon_token.js';
import { create_pglite_factory } from './db.js';
/**
 * Fast password stub for tests that don't exercise login/password flows.
 *
 * Hashes are deterministic (`stub_hash_<password>`) and verify correctly,
 * so auth bootstrap and session creation work without Argon2 overhead.
 */
export const stub_password_deps = {
    hash_password: async (p) => `stub_hash_${p}`,
    verify_password: async (p, h) => h === `stub_hash_${p}`,
    verify_dummy: async () => false,
};
/** 64-hex-char test cookie secret — deterministic, never used in production. */
export const TEST_COOKIE_SECRET = 'a'.repeat(64);
/**
 * Default password for bootstrapped test accounts. Shared between the
 * in-process keeper bootstrap (`bootstrap_test_keeper`,
 * `create_test_account_with_credentials`, `create_test_app_server`,
 * `TestApp.create_account`) and the cross-process bootstrap
 * (`cross_backend/setup.ts`). The two paths MUST agree — when they
 * diverged during the 3d cross-process lift, ~20 login tests 401'd
 * silently against the cross-process backend because the per-test
 * fixture minted accounts under a different default than the
 * integration suite's hardcoded login bodies expected. Consumers
 * hardcoding the literal string in test bodies should import this
 * constant instead so a future divergence becomes a typecheck miss
 * rather than a runtime password mismatch.
 */
export const DEFAULT_TEST_PASSWORD = 'test-password-123';
// Module-level PGlite factory for create_test_app_server when no db is provided.
// Shares the WASM instance cache from test_db.ts, avoiding redundant cold starts
// within the same vitest worker thread. Schema is reset on each create() call.
const fallback_pglite_factory = create_pglite_factory(async (db) => {
    await run_migrations(db, [auth_migration_ns]);
});
// Auto-created PGlite factories keyed by extra-namespace identity. Suites
// whose backend needs tables beyond auth (e.g. the cell layer) share one
// factory per distinct namespace set — and the worker-cached WASM instance —
// rather than each hand-building its own. The cell parity suite is the first
// user.
const fallback_factories_by_namespaces = new Map();
/**
 * Resolve the no-`db` PGlite factory for the requested extra migration
 * namespaces. Empty / omitted → the shared auth-only `fallback_pglite_factory`;
 * otherwise a memoized factory whose `init` runs
 * `[auth_migration_ns, ...migration_namespaces]` on each reset. The
 * reset-on-`create` gives the same fresh-db-per-test isolation as the
 * auth-only default. Mirrors `create_app_backend`'s `migration_namespaces`
 * seam at the test layer.
 */
const resolve_fallback_factory = (migration_namespaces) => {
    if (!migration_namespaces || migration_namespaces.length === 0) {
        return fallback_pglite_factory;
    }
    const key = migration_namespaces.map((ns) => ns.namespace).join(',');
    let factory = fallback_factories_by_namespaces.get(key);
    if (!factory) {
        factory = create_pglite_factory(async (db) => {
            await run_migrations(db, [auth_migration_ns, ...migration_namespaces]);
        });
        fallback_factories_by_namespaces.set(key, factory);
    }
    return factory;
};
/**
 * Create a test account with credentials. Use for additional accounts
 * minted alongside the keeper (e.g. `TestApp.create_account` for
 * cross-account / multi-user tests). Does NOT flip `bootstrap_lock` —
 * non-keeper accounts should not appear to the system as bootstrap
 * having happened.
 *
 * Creates an account with actor, grants roles, creates an API token,
 * creates a session, and signs a session cookie.
 *
 * @mutates the underlying `options.db` — inserts rows into `account`, `actor`,
 *   `role_grant` (one per role), `api_token`, and `auth_session`.
 */
export const create_test_account_with_credentials = async (options) => {
    const { db, keyring, session_options, password, username = 'keeper', password_value = DEFAULT_TEST_PASSWORD, roles = [], } = options;
    const deps = { db };
    const password_hash = await password.hash_password(password_value);
    const { account, actor } = await query_create_account_with_actor(deps, {
        username,
        password_hash,
    });
    // Grant roles
    for (const role of roles) {
        await query_create_role_grant(deps, { actor_id: actor.id, role, granted_by: null });
    }
    // Create API token (account-scoped — acting actor is per-request)
    const { token: api_token, id: token_id, token_hash } = generate_api_token();
    await query_create_api_token(deps, token_id, account.id, 'test-cli', token_hash);
    // Create session (account-scoped — acting actor is per-request).
    // Shares the mint primitive with `mint_test_session` / the
    // `_testing_mint_session` action; here with the standard 30-day lifetime.
    const { session_cookie } = await mint_test_session({
        db,
        keyring,
        session_options,
        account_id: account.id,
        expires_in_seconds: AUTH_SESSION_LIFETIME_MS / 1000,
    });
    return {
        account: { id: account.id, username: account.username },
        actor: { id: actor.id },
        api_token,
        session_cookie,
    };
};
/**
 * Mint a real `auth_session` row for an existing account and return a
 * validly-signed session cookie value referencing it. Test-only — the
 * forge behind the cross-backend expiry conformance cases (the
 * `expired_session` principal): pass a negative `expires_in_seconds` to
 * produce an *expired server-side session* whose signed cookie envelope is
 * still well-formed. Both the TS `_testing_mint_session` action and the
 * in-process `fixture.mint_expired_session()` seam call this so the write
 * semantics match across transports.
 *
 * @mutates `options.db` — inserts one `auth_session` row.
 */
export const mint_test_session = async (options) => {
    const { db, keyring, session_options, account_id, expires_in_seconds } = options;
    const session_token = generate_session_token();
    const session_hash = hash_session_token(session_token);
    const expires_at = new Date(Date.now() + expires_in_seconds * 1000);
    await query_create_session({ db }, session_hash, account_id, expires_at);
    const session_cookie = await create_session_cookie_value(keyring, session_token, session_options);
    return { session_cookie };
};
/**
 * Bootstrap the test-DB keeper. Direct-query shortcut for the default
 * `create_test_app` path — bootstrap is not what most tests exercise, so
 * we skip the real `bootstrap_account` flow (no audit row, no
 * `on_bootstrap` callback). Tests that need the full success-path flow
 * use `create_test_app_for_bootstrap` instead.
 *
 * Flips `bootstrap_lock.bootstrapped = true` so the post-insert DB state
 * matches a real bootstrap completion — production code can trust the
 * lock as the single signal without a belt-and-suspenders
 * `query_account_has_any` defense.
 *
 * @mutates the underlying `options.db` — inserts the account/actor/roles/
 *   API token/session_cookie rows AND flips `bootstrap_lock.bootstrapped`.
 */
export const bootstrap_test_keeper = async (options) => {
    const result = await create_test_account_with_credentials(options);
    // Lock flip — mirrors production `bootstrap_account` so test/prod write
    // semantics stay in parity.
    await options.db.query('UPDATE bootstrap_lock SET bootstrapped = true WHERE id = 1 AND bootstrapped = false');
    return result;
};
/** Silent logger for tests — suppresses all output. */
const test_log = new Logger('test', { level: 'off' });
const default_test_fs_stubs = {
    stat: async () => null,
    read_text_file: async () => '',
    delete_file: async () => { },
};
/**
 * Shared backend-assembly path for `create_test_app_server` and
 * `create_test_app_for_bootstrap`. Returns the raw `AppBackend` + the
 * keyring used to sign session cookies; callers wrap with their own
 * concerns (keeper pre-creation vs. pre-bootstrap state).
 *
 * Resets `app_settings` singleton row for caller-supplied DBs so prior
 * tests don't leak `open_signup`. Does NOT reset `bootstrap_lock` —
 * callers own that policy (`create_test_app_server` lets
 * `bootstrap_test_keeper` flip it; `create_test_app_for_bootstrap`
 * resets it to false before this runs).
 */
const _build_test_backend = async (options) => {
    const { db: existing_db, db_type = 'pglite-memory', password = stub_password_deps, audit_factory = default_audit_factory, fs_stubs = default_test_fs_stubs, migration_namespaces, } = options;
    if (existing_db && migration_namespaces && migration_namespaces.length > 0) {
        throw new Error('test app setup: pass either `db` (caller owns migrations) or `migration_namespaces` ' +
            '(harness migrates), not both');
    }
    const keyring_result = create_validated_keyring(TEST_COOKIE_SECRET);
    if (!keyring_result.ok) {
        throw new Error(`Test keyring failed: ${keyring_result.errors.join(', ')}`);
    }
    let backend;
    if (existing_db) {
        // Reset singleton config row from a previous test (harmless on fresh pglite).
        await existing_db.query('UPDATE app_settings SET open_signup = false, updated_at = NULL, updated_by = NULL WHERE open_signup = true OR updated_at IS NOT NULL');
        const audit = audit_factory({ db: existing_db, log: test_log });
        backend = {
            db_type,
            db_name: 'test',
            migration_results: [], // migrations ran in the factory's init_schema
            close: async () => { },
            deps: {
                keyring: keyring_result.keyring,
                password,
                db: existing_db,
                log: test_log,
                audit,
                ...fs_stubs,
            },
        };
    }
    else {
        // In-memory PGlite via cached factory — reuses the WASM instance from test_db.ts
        // instead of creating a new PGlite each time. Schema is reset and migrations re-run
        // on each call, but the expensive WASM cold start only happens once per worker thread.
        // `migration_namespaces` selects an auth+extras factory; auth-only is the default.
        const db = await resolve_fallback_factory(migration_namespaces).create();
        const audit = audit_factory({ db, log: test_log });
        backend = {
            db_type: 'pglite-memory',
            db_name: '(memory)',
            migration_results: [],
            close: async () => { },
            deps: {
                keyring: keyring_result.keyring,
                password,
                db,
                log: test_log,
                audit,
                ...fs_stubs,
            },
        };
    }
    return { backend, keyring: keyring_result.keyring };
};
export const create_test_app_server = async (options) => {
    const { session_options, password = stub_password_deps, username = 'keeper', password_value = DEFAULT_TEST_PASSWORD, roles = [ROLE_KEEPER], } = options;
    const { backend, keyring } = await _build_test_backend(options);
    const bootstrapped = await bootstrap_test_keeper({
        db: backend.deps.db,
        keyring,
        session_options,
        password,
        username,
        password_value,
        roles,
    });
    return {
        ...backend,
        ...bootstrapped,
        keyring,
        cleanup: () => backend.close(),
    };
};
/**
 * Create a fully assembled test app with a Hono server, middleware, and routes.
 *
 * Combines `create_test_app_server` + `create_app_server` into a single call.
 * Disables rate limiters and logging by default (test-friendly).
 *
 * A fresh Hono app is created each call — middleware closures bind to the
 * server's deps (db, keyring), so reuse across servers is unsafe.
 * The expensive resource (PGlite WASM) is cached separately in `test_db.ts`.
 *
 * @param options - test app configuration
 * @returns a `TestApp` ready for HTTP testing
 */
export const create_test_app = async (options) => {
    const test_server = await create_test_app_server(options);
    // Daemon token state for keeper auth in tests.
    // Uses a static token (no rotation) — sufficient for request-level testing.
    const test_daemon_token = generate_daemon_token();
    const daemon_token_state = {
        current_token: test_daemon_token,
        previous_token: null,
        rotated_at: new Date(),
        keeper_account_id: test_server.account.id,
    };
    const result = await create_app_server({
        backend: test_server,
        session_options: options.session_options,
        allowed_origins: [/^http:\/\/localhost/],
        proxy: { trusted_proxies: ['127.0.0.1'], get_connection_ip: () => '127.0.0.1' },
        env_schema: z.object({}),
        ip_rate_limiter: null,
        login_account_rate_limiter: null,
        signup_account_rate_limiter: null,
        bearer_ip_rate_limiter: null,
        await_pending_effects: true,
        daemon_token_state,
        rpc_endpoints: options.rpc_endpoints,
        bootstrap: options.bootstrap,
        ...options.app_options,
        create_route_specs: options.create_route_specs,
    });
    const { app, surface_spec } = result;
    const { cookie_name } = options.session_options;
    const { password = stub_password_deps } = options;
    const create_session_headers = (extra) => ({
        host: 'localhost',
        origin: 'http://localhost:5173',
        cookie: `${cookie_name}=${test_server.session_cookie}`,
        ...extra,
    });
    const create_bearer_headers = (extra) => ({
        host: 'localhost',
        authorization: `Bearer ${test_server.api_token}`,
        ...extra,
    });
    const create_daemon_token_headers = (extra) => ({
        host: 'localhost',
        [DAEMON_TOKEN_HEADER]: test_daemon_token,
        ...extra,
    });
    let account_counter = 0;
    const create_account = async (account_options) => {
        account_counter++;
        const bootstrapped = await create_test_account_with_credentials({
            db: test_server.deps.db,
            keyring: test_server.keyring,
            session_options: options.session_options,
            password,
            username: account_options?.username ?? `test_user_${account_counter}`,
            password_value: account_options?.password_value ?? DEFAULT_TEST_PASSWORD,
            roles: account_options?.roles ?? [],
        });
        return {
            ...bootstrapped,
            create_session_headers: (extra) => ({
                host: 'localhost',
                origin: 'http://localhost:5173',
                cookie: `${cookie_name}=${bootstrapped.session_cookie}`,
                ...extra,
            }),
            create_bearer_headers: (extra) => ({
                host: 'localhost',
                authorization: `Bearer ${bootstrapped.api_token}`,
                ...extra,
            }),
        };
    };
    return {
        app,
        backend: test_server,
        surface_spec,
        surface: surface_spec.surface,
        route_specs: surface_spec.route_specs,
        create_session_headers,
        create_bearer_headers,
        create_daemon_token_headers,
        create_account,
        cleanup: () => test_server.cleanup(),
    };
};
/**
 * Create a test app in the pre-bootstrap state for exercising the
 * bootstrap success path end-to-end.
 *
 * Skips the keeper pre-creation `create_test_app` does by default —
 * `bootstrap_lock.bootstrapped` stays at `false` and the DB has no
 * accounts. The fs stubs return `options.bootstrap_token` when the
 * bootstrap handler reads `bootstrap.token_path`, so a `POST /bootstrap`
 * with `{token: bootstrap_token, username, password}` reaches the
 * success branch.
 *
 * Pair with `describe_bootstrap_success_tests` for the consumer-runnable
 * suite that drives the full happy path + adjacent assertions on
 * observable state (account exists, audit row emitted, on_bootstrap
 * callback fired).
 *
 * @param options - bootstrap config + factory inputs
 * @returns a `TestAppForBootstrap` ready for the test to drive bootstrap
 */
export const create_test_app_for_bootstrap = async (options) => {
    const { session_options, bootstrap, bootstrap_token } = options;
    // Caller-supplied DB may carry lock state from a prior test — reset to false
    // before `_build_test_backend` runs (which doesn't touch the lock itself).
    // Fresh pglite already starts at false (factory init).
    if (options.db) {
        await options.db.query('UPDATE bootstrap_lock SET bootstrapped = false WHERE bootstrapped = true');
    }
    // Token-aware fs stubs: the bootstrap route's filesystem operations resolve
    // against the configured token_path; everything else returns no-op defaults.
    let token_file_deleted = false;
    const fs_stubs = {
        stat: async (path) => path === bootstrap.token_path && !token_file_deleted
            ? { is_file: true, is_directory: false }
            : null,
        read_text_file: async (path) => path === bootstrap.token_path && !token_file_deleted ? bootstrap_token : '',
        delete_file: async (path) => {
            if (path === bootstrap.token_path)
                token_file_deleted = true;
        },
    };
    const { backend } = await _build_test_backend({ ...options, fs_stubs });
    // Daemon token state isn't reachable pre-bootstrap (no keeper account)
    // but the field is required by AppServerOptions; pass a placeholder.
    const daemon_token_state = {
        current_token: generate_daemon_token(),
        previous_token: null,
        rotated_at: new Date(),
        keeper_account_id: null,
    };
    const result = await create_app_server({
        backend,
        session_options,
        allowed_origins: [/^http:\/\/localhost/],
        proxy: { trusted_proxies: ['127.0.0.1'], get_connection_ip: () => '127.0.0.1' },
        env_schema: z.object({}),
        ip_rate_limiter: null,
        login_account_rate_limiter: null,
        signup_account_rate_limiter: null,
        bearer_ip_rate_limiter: null,
        await_pending_effects: true,
        daemon_token_state,
        rpc_endpoints: options.rpc_endpoints,
        bootstrap,
        ...options.app_options,
        create_route_specs: options.create_route_specs,
    });
    const create_request_headers = (extra) => ({
        host: 'localhost',
        origin: 'http://localhost:5173',
        ...extra,
    });
    return {
        app: result.app,
        backend,
        surface_spec: result.surface_spec,
        surface: result.surface_spec.surface,
        route_specs: result.surface_spec.route_specs,
        create_request_headers,
        cleanup: () => backend.close(),
    };
};
