// Planu — Testing Analyzer (SPEC-483)
// Rule-based analyzer: missing test files, coverage gaps, test smells.
import { readFile, readdir, access } from 'node:fs/promises';
import { join, relative, extname, basename } from 'node:path';
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/** Source directories that MUST have corresponding tests. */
const TESTED_DIRS = ['engine', 'tools', 'storage'];
/** Test smells: duplicated setup blocks (>3 beforeEach in the same file). */
const BEFORE_EACH_THRESHOLD = 3;
/** Magic numbers in test assertions (anything except 0, 1, -1, 2, 100, 200, 404, 500). */
const MAGIC_NUMBER_PATTERN = /(?:toBe|toEqual|toStrictEqual)\s*\(\s*(\d{3,})\s*\)/;
const ALLOWED_MAGIC = new Set([0, 1, 2, 100, 200, 201, 204, 400, 401, 403, 404, 500]);
// ---------------------------------------------------------------------------
// File discovery
// ---------------------------------------------------------------------------
async function walkDir(dir, root, includeTests = false) {
    const results = [];
    let entries;
    try {
        entries = await readdir(dir, { withFileTypes: true, encoding: 'utf-8' });
    }
    catch {
        return results;
    }
    for (const entry of entries) {
        if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'dist') {
            continue;
        }
        const full = join(dir, entry.name);
        if (entry.isDirectory()) {
            const sub = await walkDir(full, root, includeTests);
            results.push(...sub);
        }
        else if (includeTests ? isTestFile(entry.name) : isSourceFile(entry.name)) {
            results.push(full);
        }
    }
    return results;
}
function isSourceFile(name) {
    const ext = extname(name);
    return (['.ts', '.js'].includes(ext) &&
        !name.endsWith('.d.ts') &&
        !name.endsWith('.test.ts') &&
        !name.endsWith('.test.js') &&
        !name.endsWith('.spec.ts') &&
        !name.endsWith('.spec.js'));
}
function isTestFile(name) {
    return (name.endsWith('.test.ts') ||
        name.endsWith('.test.js') ||
        name.endsWith('.spec.ts') ||
        name.endsWith('.spec.js'));
}
async function collectSourceFiles(root, filterFiles) {
    if (filterFiles !== undefined && filterFiles.length > 0) {
        return filterFiles.map((f) => join(root, f)).filter((f) => isSourceFile(basename(f)));
    }
    const all = [];
    for (const dir of TESTED_DIRS) {
        const full = join(root, 'src', dir);
        const files = await walkDir(full, root);
        all.push(...files);
    }
    return all;
}
// ---------------------------------------------------------------------------
// Coverage gap: check if a corresponding test file exists
// ---------------------------------------------------------------------------
async function testFileExists(sourceFile, root) {
    const rel = relative(join(root, 'src'), sourceFile);
    // src/engine/foo.ts → tests/engine/foo.test.ts
    const testPath = join(root, 'tests', rel.replace(/\.(ts|js)$/, '.test.$1'));
    try {
        await access(testPath);
        return true;
    }
    catch {
        return false;
    }
}
// ---------------------------------------------------------------------------
// Test smell detection
// ---------------------------------------------------------------------------
async function analyzeTestFile(filePath, root) {
    if (!isTestFile(basename(filePath))) {
        return [];
    }
    let content;
    try {
        content = await readFile(filePath, 'utf-8');
    }
    catch {
        return [];
    }
    const rel = relative(root, filePath);
    const lines = content.split('\n');
    const findings = [];
    // Duplicated setup: too many beforeEach blocks
    const beforeEachCount = lines.filter((l) => /\bbeforeEach\s*\(/.test(l)).length;
    if (beforeEachCount > BEFORE_EACH_THRESHOLD) {
        findings.push({
            severity: 'low',
            file: rel,
            line: 0,
            ruleId: 'testing/duplicated-setup',
            message: `File has ${beforeEachCount} beforeEach blocks (>${BEFORE_EACH_THRESHOLD}). Consider shared test factories.`,
            fixHint: 'Extract shared setup into a factory function or a single beforeAll.',
        });
    }
    // Magic numbers in assertions
    lines.forEach((line, idx) => {
        const match = MAGIC_NUMBER_PATTERN.exec(line);
        if (match !== null) {
            const num = Number(match[1]);
            if (!ALLOWED_MAGIC.has(num)) {
                findings.push({
                    severity: 'nit',
                    file: rel,
                    line: idx + 1,
                    ruleId: 'testing/magic-number',
                    message: `Magic number ${String(num)} in test assertion. Name it as a constant.`,
                    fixHint: `const EXPECTED_${String(num).toUpperCase()} = ${String(num)};`,
                });
            }
        }
    });
    // Missing edge cases: check if null/undefined/empty cases are absent
    const hasNullCheck = content.includes('null') || content.includes('undefined');
    const hasEmptyCheck = content.includes("''") || content.includes('[]') || content.includes('{}');
    if (!hasNullCheck) {
        findings.push({
            severity: 'medium',
            file: rel,
            line: 0,
            ruleId: 'testing/missing-null-edge-case',
            message: 'No null/undefined edge case detected in test file.',
            fixHint: 'Add at least one test case covering null or undefined input.',
        });
    }
    if (!hasEmptyCheck) {
        findings.push({
            severity: 'low',
            file: rel,
            line: 0,
            ruleId: 'testing/missing-empty-edge-case',
            message: 'No empty string/array/object edge case detected in test file.',
            fixHint: "Add at least one test case covering empty input (e.g., '' or []).",
        });
    }
    return findings;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
export async function runTestingAnalyzer(projectPath, files) {
    const sourceFiles = await collectSourceFiles(projectPath, files);
    const allFindings = [];
    // Coverage gap check: missing test files
    for (const sf of sourceFiles) {
        const hasTest = await testFileExists(sf, projectPath);
        if (!hasTest) {
            const rel = relative(projectPath, sf);
            allFindings.push({
                severity: 'medium',
                file: rel,
                line: 0,
                ruleId: 'testing/missing-test-file',
                message: `No corresponding test file found for ${rel}.`,
                fixHint: `Create tests/${relative(join(projectPath, 'src'), sf).replace(/\.(ts|js)$/, '.test.$1')}`,
            });
        }
    }
    // Test smell detection: walk tests/ directory (includeTests=true to pick up .test.ts files)
    const testsDir = join(projectPath, 'tests');
    const testFiles = await walkDir(testsDir, projectPath, true);
    for (const tf of testFiles) {
        const smells = await analyzeTestFile(tf, projectPath);
        allFindings.push(...smells);
    }
    return {
        domain: 'testing',
        findings: allFindings,
        analyzedAt: new Date().toISOString(),
    };
}
//# sourceMappingURL=testing-analyzer.js.map