// engine/spec-coverage/test-finder.ts — Finds test files in a project using
// multi-ecosystem naming conventions. Pure I/O, no analysis logic.
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { glob } from 'glob';
// ---------------------------------------------------------------------------
// Ecosystem glob patterns
// ---------------------------------------------------------------------------
const ECOSYSTEM_PATTERNS = [
    {
        ecosystem: 'typescript',
        patterns: ['**/*.test.ts', '**/*.spec.ts', '**/*.test.js', '**/*.spec.js'],
    },
    { ecosystem: 'python', patterns: ['**/*_test.py', '**/test_*.py'] },
    { ecosystem: 'go', patterns: ['**/*_test.go'] },
    {
        ecosystem: 'java',
        patterns: ['**/*Test.java', '**/*Tests.java'],
    },
    {
        ecosystem: 'kotlin',
        patterns: ['**/*Test.kt', '**/*Tests.kt', '**/*Spec.kt'],
    },
    { ecosystem: 'swift', patterns: ['**/*Tests.swift', '**/*Spec.swift'] },
    { ecosystem: 'csharp', patterns: ['**/*Tests.cs', '**/*Test.cs', '**/*Spec.cs'] },
    { ecosystem: 'php', patterns: ['**/*Test.php'] },
    { ecosystem: 'ruby', patterns: ['**/*_spec.rb', '**/*_test.rb'] },
];
/** Directories to ignore when scanning (node_modules, build artifacts, etc.) */
const IGNORE_DIRS = [
    '**/node_modules/**',
    '**/dist/**',
    '**/build/**',
    '**/.git/**',
    '**/vendor/**',
    '**/.cache/**',
    '**/__pycache__/**',
    '**/target/**',
];
// ---------------------------------------------------------------------------
// Superficial test detection patterns
// ---------------------------------------------------------------------------
/** Regex patterns that indicate a test body has no real assertions. */
const SUPERFICIAL_PATTERNS = [
    /\btodo!\s*\(/, // Rust: todo!()
    /^\s*pass\s*$/m, // Python: pass as only statement
    /\bpending\s*\(/, // JS/TS: pending()
    /\bxit\s*\(/, // JS/TS: xit(
    /\bxdescribe\s*\(/, // JS/TS: xdescribe(
    /test\.skip\s*\(/, // JS/TS: test.skip(
    /it\.skip\s*\(/, // JS/TS: it.skip(
    /describe\.skip\s*\(/, // JS/TS: describe.skip(
    /@Ignore\b/, // Java: @Ignore
    /\bt\.Skip\s*\(/, // Go: t.Skip(
    /\bskip\s+"[^"]*"/, // Ruby: skip "reason"
    /\bXCTSkip\b/, // Swift: XCTSkip
    /\[Ignore\]/, // C#: [Ignore]
];
// ---------------------------------------------------------------------------
// Runner detection config
// ---------------------------------------------------------------------------
const RUNNER_DETECTORS = [
    {
        name: 'vitest',
        ecosystem: 'typescript',
        configFiles: ['vitest.config.ts', 'vitest.config.js', 'vitest.config.mts'],
    },
    {
        name: 'jest',
        ecosystem: 'typescript',
        configFiles: ['jest.config.ts', 'jest.config.js', 'jest.config.cjs'],
    },
    {
        name: 'pytest',
        ecosystem: 'python',
        configFiles: ['pytest.ini', 'pyproject.toml', 'setup.cfg'],
    },
    { name: 'go test', ecosystem: 'go', configFiles: ['go.mod'] },
    { name: 'cargo test', ecosystem: 'rust', configFiles: ['Cargo.toml'] },
    {
        name: 'junit',
        ecosystem: 'java',
        configFiles: ['pom.xml', 'build.gradle', 'build.gradle.kts'],
    },
    { name: 'xctest', ecosystem: 'swift', configFiles: ['Package.swift'] },
    {
        name: 'phpunit',
        ecosystem: 'php',
        configFiles: ['phpunit.xml', 'phpunit.xml.dist', 'composer.json'],
    },
    { name: 'rspec', ecosystem: 'ruby', configFiles: ['.rspec', 'Gemfile'] },
    { name: 'xunit', ecosystem: 'csharp', configFiles: ['*.csproj', '*.sln'] },
];
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Finds all test files in the given project root using multi-ecosystem
 * naming conventions. Returns an array of TestFile descriptors.
 */
export async function findTestFiles(projectPath) {
    const results = [];
    const seen = new Set();
    for (const { ecosystem, patterns } of ECOSYSTEM_PATTERNS) {
        for (const pattern of patterns) {
            let matches;
            try {
                matches = await glob(pattern, {
                    cwd: projectPath,
                    ignore: IGNORE_DIRS,
                    absolute: false,
                    maxDepth: 12,
                    nodir: true,
                });
                /* v8 ignore start */
            }
            catch {
                continue;
            }
            /* v8 ignore stop */
            for (const relFile of matches) {
                if (seen.has(relFile)) {
                    /* v8 ignore next 1 */
                    continue;
                }
                seen.add(relFile);
                const absFile = join(projectPath, relFile);
                const isSuperficial = await detectSuperficialTest(absFile, ecosystem);
                results.push({ file: relFile, ecosystem, isSuperficial });
            }
        }
    }
    // Special-case: Rust files with #[cfg(test)] block
    const rustFiles = await findRustTestFiles(projectPath, seen);
    results.push(...rustFiles);
    return results;
}
/**
 * Detects test runners present in the project by checking for config files.
 * Returns only runners whose config files actually exist.
 */
export async function detectTestRunners(projectPath) {
    const runners = [];
    for (const { name, ecosystem, configFiles } of RUNNER_DETECTORS) {
        for (const configFile of configFiles) {
            // Support glob patterns in configFiles (e.g., "*.csproj")
            if (configFile.includes('*')) {
                const matches = await glob(configFile, {
                    cwd: projectPath,
                    nodir: true,
                    maxDepth: 2,
                }).catch(/* v8 ignore next 1 */ () => []);
                if (matches.length > 0) {
                    runners.push({ name, ecosystem, configFile: matches[0] ?? configFile });
                    break;
                }
            }
            else {
                let exists = false;
                try {
                    await readFile(join(projectPath, configFile), 'utf-8');
                    exists = true;
                }
                catch {
                    // File not found — try next config file
                }
                if (exists) {
                    runners.push({ name, ecosystem, configFile });
                    break;
                }
            }
        }
    }
    return runners;
}
/**
 * Reads a test file and returns its full content as a string.
 * Returns empty string if the file cannot be read.
 */
export async function readTestFileContent(projectPath, relFile) {
    try {
        return await readFile(join(projectPath, relFile), 'utf-8');
    }
    catch {
        return '';
    }
}
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
/** Returns true if the test file appears to contain only superficial tests. */
async function detectSuperficialTest(absFile, _ecosystem) {
    let content;
    try {
        content = await readFile(absFile, 'utf-8');
        /* v8 ignore start */
    }
    catch {
        return false;
    }
    /* v8 ignore stop */
    for (const pattern of SUPERFICIAL_PATTERNS) {
        if (pattern.test(content)) {
            return true;
        }
    }
    return false;
}
/** Finds Rust source files that contain a #[cfg(test)] block. */
async function findRustTestFiles(projectPath, seen) {
    const results = [];
    let rustFiles;
    try {
        rustFiles = await glob('**/*.rs', {
            cwd: projectPath,
            ignore: IGNORE_DIRS,
            maxDepth: 12,
            nodir: true,
        });
        /* v8 ignore start */
    }
    catch {
        return results;
    }
    /* v8 ignore stop */
    for (const relFile of rustFiles) {
        /* v8 ignore start */
        if (seen.has(relFile)) {
            continue;
        }
        /* v8 ignore stop */
        const absFile = join(projectPath, relFile);
        let content;
        try {
            content = await readFile(absFile, 'utf-8');
            /* v8 ignore start */
        }
        catch {
            continue;
        }
        /* v8 ignore stop */
        if (content.includes('#[cfg(test)]')) {
            seen.add(relFile);
            const isSuperficial = SUPERFICIAL_PATTERNS.some((p) => p.test(content));
            results.push({ file: relFile, ecosystem: 'rust', isSuperficial });
        }
    }
    return results;
}
//# sourceMappingURL=test-finder.js.map