/*
 * Copyright OpenSearch Contributors
 * SPDX-License-Identifier: Apache-2.0
 */
import { createHash } from 'crypto';
import { readFileSync } from 'fs';
import { resolve, dirname } from 'path';
import { pathToFileURL } from 'url';
import { createRequire, Module as NodeModule } from 'module';
import { setActiveFile, getRegisteredTests, getRegisteredHooks, clearRegistry, } from './define.js';
import { getAuthoringSurface } from './authoringSurface.js';
const CODE_EXTENSIONS = ['.ts', '.js', '.mjs'];
export function isCodeFile(filename) {
    const lower = filename.toLowerCase();
    return CODE_EXTENSIONS.some(ext => lower.endsWith(ext));
}
export function computeTestCaseHash(tc) {
    const content = JSON.stringify({
        name: tc.name,
        prompt: tc.options.prompt,
        context: tc.options.context,
        labels: tc.options.labels,
        description: tc.options.description,
        // Include the new expected* fields so editing them invalidates the
        // sourceHash and the upsert path picks up the change. Without this,
        // a user adding `expectedOutcomes: [...]` to an existing test would
        // see the test case stay on its old version and the new outcomes
        // would never reach storage.
        expectedOutcomes: tc.options.expectedOutcomes,
        expectedTrajectory: tc.options.expectedTrajectory,
    });
    return createHash('sha256').update(content).digest('hex');
}
export async function loadTestCasesFromModule(filePath) {
    const absPath = resolve(filePath);
    // Clear any prior registration for this file and set it as active
    clearRegistry(absPath);
    setActiveFile(absPath);
    let module;
    if (absPath.endsWith('.js')) {
        // For CJS .js files, execute in a fresh context with our test() function
        // injected. This avoids module caching issues across multiple loads.
        const code = readFileSync(absPath, 'utf-8');
        const fileDir = dirname(absPath);
        // NOTE: do not call require('module') here — this file is bundled as ESM
        // by esbuild for the server, and Dynamic require of built-ins is unsupported
        // in ESM output. Use the statically-imported NodeModule instead.
        const Module = NodeModule;
        const m = new Module(absPath);
        m.filename = absPath;
        m.paths = Module._nodeModulePaths(fileDir);
        // Provide a require function scoped to the file's directory, but override
        // any require of the define module to return our own instance
        const fileRequire = createRequire(absPath);
        // Match if `id` looks like our own define module before doing any
        // filesystem resolution. Required because:
        //   1) the loader is bundled as ESM by esbuild, so synthetic CJS
        //      `require.resolve` is unavailable here, and
        //   2) `lib/testCases/define.ts` is not compiled to a .js sibling, so
        //      `fileRequire.resolve('../../lib/testCases/define')` from a
        //      fixture would throw MODULE_NOT_FOUND.
        const isDefineId = (id) => {
            const normalized = id.replace(/\\/g, '/').replace(/\.js$/, '');
            return (normalized === 'lib/testCases/define' ||
                normalized.endsWith('/lib/testCases/define'));
        };
        // Intercept the published package name as well — fixtures generally
        // do `require('@opensearch-project/agent-health')` and Node's resolver
        // would otherwise hit the package's exports map (which only exposes
        // 'import' for ESM consumers).
        const isPackageName = (id) => id === '@opensearch-project/agent-health' ||
            id === '@opensearch/agent-health' ||
            id === 'agent-health';
        // The object handed back when a CJS eval file requires the SDK. Single
        // source of truth shared with the package exports (see authoringSurface)
        // so `.js` and `.ts`/`.mjs` files see the SAME surface — no drift (#232).
        const sdkExports = getAuthoringSurface();
        const wrappedRequire = (id) => {
            if (isDefineId(id) || isPackageName(id)) {
                return sdkExports;
            }
            // Fall back to a resolution-based check so that fixtures requiring the
            // SDK by absolute path or via the published package name still hand back
            // our test() registrar (and therefore land in our file-scoped registry).
            try {
                const resolved = fileRequire.resolve(id);
                const normalized = resolved.replace(/\\/g, '/');
                if (normalized.endsWith('/lib/testCases/define.js') ||
                    normalized.endsWith('/lib/testCases/define')) {
                    return sdkExports;
                }
            }
            catch {
                // ignore unresolvable IDs — fileRequire below will surface the error
            }
            return fileRequire(id);
        };
        wrappedRequire.resolve = fileRequire.resolve;
        const wrapper = `(function(exports, require, module, __filename, __dirname) { ${code}\n});`;
        const compiledFn = eval(wrapper);
        compiledFn(m.exports, wrappedRequire, m, absPath, fileDir);
        module = m.exports;
    }
    else {
        // For .ts and .mjs files, use dynamic import
        try {
            const fileUrl = pathToFileURL(absPath).href;
            module = await import(fileUrl);
        }
        catch (err) {
            if (err.code === 'ERR_UNKNOWN_FILE_EXTENSION' && absPath.endsWith('.ts')) {
                throw new Error(`Cannot import TypeScript file: ${filePath}\n` +
                    'Install tsx as a dependency: npm install tsx\n' +
                    'Or pre-compile .eval.ts to .eval.js before running.');
            }
            throw new Error(`Failed to import module: ${filePath}\n${err.message}`);
        }
    }
    // Tests come exclusively from test() / describe() registration calls
    // (Playwright-style). Legacy default-export array form was removed when
    // we dropped defineTestCases() — the SDK is experimental and the API
    // shape is intentionally narrow.
    const testCases = getRegisteredTests(absPath);
    if (testCases.length === 0) {
        throw new Error(`Module ${filePath} has no test cases. Register tests with test(name, options?, body) inside the file.`);
    }
    // Compute per-test-case hash
    const loaded = testCases.map(tc => ({
        ...tc,
        hash: computeTestCaseHash(tc),
    }));
    // Derive describe-group → test names mapping. Tests outside any
    // describe() have benchmarkPath=undefined and are excluded; the CLI/
    // server adds them to the file-default benchmark separately.
    const benchmarks = new Map();
    for (const tc of loaded) {
        if (tc.benchmarkPath) {
            const list = benchmarks.get(tc.benchmarkPath) ?? [];
            list.push(tc.name);
            benchmarks.set(tc.benchmarkPath, list);
        }
    }
    // Collect every hook the file registered. Empty when no
    // beforeEach/afterEach/beforeAll/afterAll were called — the orchestrator
    // is a no-op in that case so existing tests are unaffected.
    const hooks = getRegisteredHooks(absPath);
    return { testCases: loaded, filePath: absPath, benchmarks, hooks };
}
//# sourceMappingURL=loader.js.map