#!/usr/bin/env node
"use strict";
/* eslint-disable @typescript-eslint/no-require-imports */
Object.defineProperty(exports, "__esModule", { value: true });
exports.runTestCli = runTestCli;
exports.resolveTestWorkerConcurrency = resolveTestWorkerConcurrency;
const node_child_process_1 = require("node:child_process");
const node_fs_1 = require("node:fs");
const node_os_1 = require("node:os");
const node_path_1 = require("node:path");
const promises_1 = require("node:timers/promises");
const app_1 = require("../app");
const env_1 = require("../env");
const common_1 = require("./common");
const typescript_output_1 = require("../typescript-output");
async function runTestCli(args = process.argv.slice(2)) {
    if (args.includes('--help') || args.includes('-h')) {
        printUsage();
        return 0;
    }
    env_1.Env.TZ = 'UTC';
    env_1.Env.APP_ENV = 'test';
    const distDir = (0, typescript_output_1.resolveTypeScriptOutDir)({ tsconfigPath: env_1.Env.TSF_TSCONFIG }) ?? (0, node_path_1.resolve)('dist');
    const teardown = await runGlobalSetup(distDir);
    let manager;
    try {
        const { nodeArgs, fileArgs } = splitArgs(args);
        const testFiles = collectTestFiles(distDir, fileArgs);
        if (testFiles.length === 0) {
            console.error(`No compiled test files found in ${distDir}.`);
            return 1;
        }
        const resolvedDatabaseEnv = resolveTestDatabaseEnv();
        const testRunTs = env_1.Env.TEST_RUN_TS ?? String(Math.floor(Date.now() / 1000));
        env_1.Env.TEST_RUN_TS = testRunTs;
        const mysqlSessionPoolSize = resolveTestWorkerConcurrency(nodeArgs);
        manager = await startMySQLSessionManagerIfNeeded(testRunTs, resolvedDatabaseEnv, mysqlSessionPoolSize);
        const result = (0, common_1.runNode)(['--enable-source-maps', ...nodeArgs, '--test', '--test-force-exit', '--test-timeout=180000', ...testFiles], process.cwd(), {
            ...resolvedDatabaseEnv,
            APP_ENV: 'test',
            TZ: 'UTC',
            TEST_RUN_TS: testRunTs,
            ...manager?.env
        });
        return result.status;
    }
    finally {
        await manager?.stop();
        if (teardown)
            await teardown();
    }
}
function printUsage() {
    console.log(`Usage: tsf-test [node-test-options] [test-files-or-dirs...]

Runs compiled node:test specs from the configured TypeScript outDir. Source paths
are mapped using rootDir and outDir from the effective tsconfig.`);
}
async function runGlobalSetup(distDir) {
    const setupPath = findGlobalSetupPath(distDir);
    if (!setupPath)
        return undefined;
    const beforeRequireEnv = snapshotProcessEnv();
    const globalSetup = require(setupPath);
    const requireEnvPatch = diffProcessEnv(beforeRequireEnv, snapshotProcessEnv());
    if (globalSetup.setup)
        await globalSetup.setup();
    applyProcessEnvPatch(requireEnvPatch);
    return globalSetup.teardown ? () => Promise.resolve(globalSetup.teardown()) : undefined;
}
function findGlobalSetupPath(distDir) {
    const candidates = [
        (0, typescript_output_1.resolveTypeScriptOutputPath)('tests/shared/globalSetup.ts', { tsconfigPath: env_1.Env.TSF_TSCONFIG }),
        (0, typescript_output_1.resolveTypeScriptOutputPath)('src/tests/shared/globalSetup.ts', { tsconfigPath: env_1.Env.TSF_TSCONFIG }),
        (0, node_path_1.join)(distDir, 'tests', 'shared', 'globalSetup.js')
    ].filter((path) => !!path && (0, node_fs_1.existsSync)(path));
    return [...new Set(candidates)][0];
}
function snapshotProcessEnv() {
    return { ...process.env };
}
function diffProcessEnv(before, after) {
    const patch = {};
    for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
        if (before[key] !== after[key])
            patch[key] = after[key];
    }
    return patch;
}
function applyProcessEnvPatch(patch) {
    for (const [key, value] of Object.entries(patch)) {
        if (value === undefined)
            delete process.env[key];
        else
            process.env[key] = value;
    }
}
async function startMySQLSessionManagerIfNeeded(testRunTs, databaseEnv, poolSize) {
    if (!shouldStartMySQLSessionManager(databaseEnv))
        return undefined;
    const child = (0, node_child_process_1.spawn)(process.execPath, [(0, node_path_1.join)(__dirname, '..', 'testing', 'mysql-session-manager-process.js')], {
        cwd: process.cwd(),
        env: {
            ...process.env,
            ...databaseEnv,
            APP_ENV: 'test',
            TZ: 'UTC',
            TEST_RUN_TS: testRunTs,
            TSF_TEST_MYSQL_SESSION_POOL_SIZE: String(poolSize)
        },
        stdio: ['ignore', 'pipe', 'inherit']
    });
    let ready;
    try {
        ready = await new Promise((resolve, reject) => {
            const timeout = setTimeout(() => reject(new Error('Timed out waiting for MySQL session manager to start')), 10000);
            let buffer = '';
            let settled = false;
            const settle = (worker) => {
                if (settled)
                    return;
                settled = true;
                clearTimeout(timeout);
                worker();
            };
            child.stdout?.on('data', data => {
                buffer += data.toString();
                while (true) {
                    const newline = buffer.indexOf('\n');
                    if (newline === -1)
                        return;
                    const line = buffer.slice(0, newline);
                    buffer = buffer.slice(newline + 1);
                    const prefix = 'TSF_MYSQL_SESSION_MANAGER_READY ';
                    if (line.startsWith(prefix)) {
                        settle(() => resolve(JSON.parse(line.slice(prefix.length))));
                    }
                    else if (line.trim()) {
                        process.stdout.write(`${line}\n`);
                    }
                }
            });
            child.once('error', error => settle(() => reject(error)));
            child.once('exit', code => {
                if (!settled)
                    settle(() => reject(new Error(`MySQL session manager exited before startup with code ${code}`)));
            });
        });
    }
    catch (error) {
        child.kill('SIGTERM');
        throw error;
    }
    console.log('tsf-test: shared MySQL session manager enabled');
    let closed = false;
    const closePromise = new Promise(resolve => {
        child.once('close', () => {
            closed = true;
            resolve();
        });
    });
    return {
        env: {
            TSF_TEST_MYSQL_SESSION_MANAGER: '1',
            TSF_TEST_MYSQL_SESSION_POOL_SIZE: String(poolSize),
            TSF_TEST_MYSQL_SESSION_MANAGER_PORT: String(ready.port),
            TSF_TEST_MYSQL_SESSION_MANAGER_TOKEN: ready.token
        },
        async stop() {
            if (closed)
                return;
            child.kill('SIGTERM');
            await Promise.race([
                closePromise,
                (0, promises_1.setTimeout)(3000).then(() => {
                    if (!closed)
                        child.kill('SIGKILL');
                })
            ]);
        }
    };
}
function shouldStartMySQLSessionManager(databaseEnv) {
    const flag = env_1.Env.TSF_TEST_MYSQL_SESSION_MANAGER;
    if (env_1.Env.TSF_TEST_ALLOW_SAVEPOINTS !== undefined && !isEnabledFlag(env_1.Env.TSF_TEST_ALLOW_SAVEPOINTS))
        return false;
    if (flag === '0' || flag === 'false')
        return false;
    if (env_1.Env.TSF_TEST_MYSQL_SESSION_MANAGER_PORT)
        return false;
    const dbAdapter = databaseEnv.DB_ADAPTER ?? env_1.Env.DB_ADAPTER;
    const mysqlHost = databaseEnv.MYSQL_HOST ?? env_1.Env.MYSQL_HOST;
    const mysqlUser = databaseEnv.MYSQL_USER ?? env_1.Env.MYSQL_USER;
    if (!mysqlHost || !mysqlUser)
        return flag === '1' || flag === 'true';
    if (dbAdapter && dbAdapter !== 'mysql')
        return flag === '1' || flag === 'true';
    return true;
}
function resolveTestWorkerConcurrency(nodeArgs) {
    let resolved;
    for (let index = 0; index < nodeArgs.length; index++) {
        const arg = nodeArgs[index];
        if (arg === '--test-concurrency') {
            resolved = parseTestConcurrency(nodeArgs[index + 1]);
            index++;
            continue;
        }
        if (arg.startsWith('--test-concurrency=')) {
            resolved = parseTestConcurrency(arg.slice('--test-concurrency='.length));
        }
    }
    return resolved ?? defaultTestWorkerConcurrency();
}
function parseTestConcurrency(value) {
    if (value === 'false')
        return 1;
    if (!value || value === 'true')
        return undefined;
    const parsed = Number(value);
    if (!Number.isFinite(parsed) || parsed < 1)
        return undefined;
    return Math.floor(parsed);
}
function defaultTestWorkerConcurrency() {
    return Math.max(1, (0, node_os_1.availableParallelism)() - 1);
}
function resolveTestDatabaseEnv() {
    try {
        const config = new app_1.ConfigLoader(app_1.BaseAppConfig).load();
        return compactEnv({
            DB_ADAPTER: config.DB_ADAPTER,
            MYSQL_HOST: config.MYSQL_HOST,
            MYSQL_PORT: config.MYSQL_PORT,
            MYSQL_USER: config.MYSQL_USER,
            MYSQL_PASSWORD_SECRET: config.MYSQL_PASSWORD_SECRET,
            MYSQL_DATABASE: config.MYSQL_DATABASE,
            MYSQL_CONNECTION_LIMIT: config.MYSQL_CONNECTION_LIMIT,
            MYSQL_MIN_IDLE_CONNECTIONS: config.MYSQL_MIN_IDLE_CONNECTIONS,
            MYSQL_IDLE_TIMEOUT_SECONDS: config.MYSQL_IDLE_TIMEOUT_SECONDS,
            PG_HOST: config.PG_HOST,
            PG_PORT: config.PG_PORT,
            PG_USER: config.PG_USER,
            PG_PASSWORD_SECRET: config.PG_PASSWORD_SECRET,
            PG_DATABASE: config.PG_DATABASE,
            PG_SCHEMA: config.PG_SCHEMA,
            PG_SSL: config.PG_SSL,
            PG_SSL_REJECT_UNAUTHORIZED: config.PG_SSL_REJECT_UNAUTHORIZED,
            PG_CONNECTION_LIMIT: config.PG_CONNECTION_LIMIT,
            PG_IDLE_TIMEOUT_SECONDS: config.PG_IDLE_TIMEOUT_SECONDS
        });
    }
    catch {
        return {};
    }
}
function compactEnv(values) {
    const env = {};
    for (const [key, value] of Object.entries(values)) {
        if (value !== undefined)
            env[key] = String(value);
    }
    return env;
}
function isEnabledFlag(value) {
    return value === '1' || value === 'true';
}
function splitArgs(args) {
    const nodeArgs = [];
    const fileArgs = [];
    let fileMode = false;
    for (let index = 0; index < args.length; index++) {
        const arg = args[index];
        if (fileMode) {
            fileArgs.push(arg);
            continue;
        }
        if (arg === '--') {
            fileMode = true;
            continue;
        }
        if (arg.startsWith('-')) {
            nodeArgs.push(arg);
            if (optionTakesValue(arg) && args[index + 1] && !args[index + 1].startsWith('-')) {
                nodeArgs.push(args[index + 1]);
                index++;
            }
        }
        else {
            fileArgs.push(arg);
        }
    }
    return { nodeArgs, fileArgs };
}
function collectTestFiles(distDir, fileArgs) {
    if (fileArgs.length === 0)
        return (0, node_fs_1.globSync)((0, node_path_1.join)(distDir, '**/*.spec.js'));
    const testFiles = [];
    for (const fileArg of fileArgs) {
        const distPath = toDistTestPath(fileArg, distDir);
        if (distPath.endsWith('/') || !distPath.split(/[\\/]/).pop()?.includes('.')) {
            testFiles.push(...(0, node_fs_1.globSync)((0, node_path_1.join)(distPath, '**/*.spec.js')));
        }
        else {
            testFiles.push(distPath);
        }
    }
    return testFiles;
}
function toDistTestPath(fileArg, distDir) {
    const absoluteInput = (0, node_path_1.isAbsolute)(fileArg) ? fileArg : (0, node_path_1.resolve)(fileArg);
    const relativeToDist = (0, node_path_1.relative)(distDir, absoluteInput);
    if (relativeToDist !== '..' && !relativeToDist.startsWith(`..${node_path_1.sep}`) && !(0, node_path_1.isAbsolute)(relativeToDist)) {
        return replaceTestExtension(absoluteInput);
    }
    const emitted = (0, typescript_output_1.resolveTypeScriptOutputPath)(absoluteInput, { tsconfigPath: env_1.Env.TSF_TSCONFIG });
    if (emitted)
        return emitted;
    let normalized = (0, node_path_1.isAbsolute)(fileArg) ? (0, node_path_1.relative)(process.cwd(), fileArg) : fileArg;
    normalized = normalized.replace(/\\/g, '/').replace(/^\.\//, '');
    return replaceTestExtension((0, node_path_1.resolve)(distDir, normalized));
}
function replaceTestExtension(path) {
    return path.replace(/\.(?:cts|mts|tsx?)$/, extension => (extension === '.mts' ? '.mjs' : extension === '.cts' ? '.cjs' : '.js'));
}
function optionTakesValue(arg) {
    if (arg.includes('='))
        return false;
    return OPTIONS_WITH_VALUES.has(arg);
}
const OPTIONS_WITH_VALUES = new Set([
    '--conditions',
    '--cpu-prof-dir',
    '--diagnostic-dir',
    '--experimental-loader',
    '--import',
    '--inspect-port',
    '--loader',
    '--require',
    '--test-concurrency',
    '--test-coverage-branches',
    '--test-coverage-exclude',
    '--test-coverage-functions',
    '--test-coverage-include',
    '--test-coverage-lines',
    '--test-name-pattern',
    '--test-reporter',
    '--test-reporter-destination',
    '--test-shard',
    '--test-timeout',
    '-C',
    '-r'
]);
if (require.main === module) {
    runTestCli()
        .then(code => process.exit(code))
        .catch(error => {
        console.error('Test runner error:', error);
        process.exit(1);
    });
}
//# sourceMappingURL=tsf-test.js.map