// engine/validation-loop.ts — SPEC-369: Test Automation Loop
import { readdir, readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { extractCriteriaLines } from './readiness-checker.js';
import { stripFrontmatter } from './frontmatter-parser.js';
const POINTS_SPEC_COMPLETE = 20;
const POINTS_TESTS_EXIST = 20;
const POINTS_TESTS_PASS = 20;
const POINTS_COVERAGE_OK = 20;
const POINTS_NO_DRIFT = 20;
/**
 * Read a spec markdown file and extract description + criteria.
 * Falls back to in-memory fields for backward compatibility (tests).
 */
export async function loadSpecContent(spec) {
    const specData = spec;
    // Backward compat: check in-memory fields first (used by tests)
    const memCriteria = Array.isArray(specData.acceptanceCriteria)
        ? specData.acceptanceCriteria.filter((c) => typeof c === 'string')
        : Array.isArray(specData.criteria)
            ? specData.criteria.filter((c) => typeof c === 'string')
            : [];
    const memDescription = typeof specData.description === 'string' ? specData.description : '';
    if (memCriteria.length > 0 || memDescription.trim().length > 0) {
        return { description: memDescription, criteria: memCriteria };
    }
    // Read from disk
    if (!spec.specPath) {
        return { description: '', criteria: [] };
    }
    try {
        const raw = await readFile(spec.specPath, 'utf-8');
        const body = stripFrontmatter(raw);
        // Extract criteria from body
        const bodyCriteria = extractCriteriaLines(body);
        // If no body criteria, try frontmatter (lean format stores them there)
        let frontmatterCriteria = [];
        if (bodyCriteria.length === 0) {
            const frontmatterMatch = /^---\n([\s\S]*?)\n---\n/.exec(raw);
            if (frontmatterMatch?.[1]) {
                const fm = frontmatterMatch[1];
                // Checkbox format: "  - text: ..."
                const textMatches = fm.match(/^\s+-\s+text:\s*(.+)$/gm);
                if (textMatches) {
                    frontmatterCriteria = textMatches.map((m) => m
                        .replace(/^\s+-\s+text:\s*/, '')
                        .trim()
                        .replace(/^"(.*)"$/, '$1')
                        .replace(/\\"/g, '"'));
                }
                else {
                    // BDD format: "  - title: ..."
                    const titleMatches = fm.match(/^\s+-\s+title:\s*(.+)$/gm);
                    if (titleMatches) {
                        frontmatterCriteria = titleMatches.map((m) => m
                            .replace(/^\s+-\s+title:\s*/, '')
                            .trim()
                            .replace(/^"(.*)"$/, '$1')
                            .replace(/\\"/g, '"'));
                    }
                }
            }
        }
        const criteria = bodyCriteria.length > 0 ? bodyCriteria : frontmatterCriteria;
        // Extract description from body (first non-empty paragraph before any ## section)
        const lines = body.split('\n');
        let description = '';
        let collecting = false;
        for (const line of lines) {
            const trimmed = line.trim();
            if (trimmed.startsWith('## ')) {
                break;
            }
            if (trimmed.length === 0) {
                if (collecting) {
                    break;
                }
                continue;
            }
            description += (description ? ' ' : '') + trimmed;
            collecting = true;
        }
        return { description, criteria };
    }
    catch {
        return { description: '', criteria: [] };
    }
}
/**
 * Check if a tests/ directory contains files mentioning the given spec or its tools.
 */
async function findTestsForSpec(projectPath, specId, cachedTestFiles) {
    const specLower = specId.toLowerCase();
    if (cachedTestFiles !== undefined) {
        return cachedTestFiles.some((f) => f.toLowerCase().includes(specLower));
    }
    const testsDir = join(projectPath, 'tests');
    try {
        const files = await readdir(testsDir, { recursive: true });
        return files.some((f) => {
            const name = typeof f === 'string' ? f : '';
            return name.toLowerCase().includes(specLower);
        });
    }
    catch {
        return false;
    }
}
export async function computeSpecHealth(projectPath, spec, cachedTestFiles) {
    const specId = spec?.id ?? 'unknown';
    const breakdown = {
        specComplete: 0,
        testsExist: 0,
        testsPass: 0,
        coverageOk: 0,
        noDrift: 0,
    };
    const details = {};
    if (!spec) {
        return { specId, total: 0, breakdown, details: { error: `Spec ${specId} not found` } };
    }
    // specComplete: has title + description + non-empty acceptanceCriteria
    const { description, criteria } = await loadSpecContent(spec);
    const hasTitle = typeof spec.title === 'string' && spec.title.trim().length > 0;
    const hasDescription = description.trim().length > 0;
    const hasCriteria = criteria.length > 0;
    if (hasTitle && hasDescription && hasCriteria) {
        breakdown.specComplete = POINTS_SPEC_COMPLETE;
        details.specComplete = 'OK — title, description, and criteria present';
    }
    else {
        const missing = [];
        if (!hasTitle) {
            missing.push('title');
        }
        if (!hasDescription) {
            missing.push('description');
        }
        if (!hasCriteria) {
            missing.push('acceptanceCriteria');
        }
        details.specComplete = `Missing: ${missing.join(', ')}`;
    }
    // testsExist: check tests/ dir for files mentioning specId
    const testsFound = await findTestsForSpec(projectPath, specId, cachedTestFiles);
    if (testsFound) {
        breakdown.testsExist = POINTS_TESTS_EXIST;
        details.testsExist = `OK — test file found referencing ${specId}`;
    }
    else {
        details.testsExist = `No test file found for ${specId}`;
    }
    // testsPass: 20 if last_validation was successful (not computable in real-time)
    const specData = spec;
    const lastValidation = specData.lastValidation;
    if (lastValidation?.passed === true) {
        breakdown.testsPass = POINTS_TESTS_PASS;
        details.testsPass = 'OK — last validation passed';
    }
    else {
        details.testsPass = 'No passing validation recorded';
    }
    // coverageOk: 20 if spec is done
    if (spec.status === 'done') {
        breakdown.coverageOk = POINTS_COVERAGE_OK;
        details.coverageOk = 'OK — spec is done';
    }
    else {
        details.coverageOk = `Spec status is '${spec.status}', not done`;
    }
    // noDrift: 20 if status is done or implementing (no drift registered)
    const safeDrift = specData.driftScore;
    const hasDrift = typeof safeDrift === 'number' && safeDrift > 0;
    if (!hasDrift && (spec.status === 'done' || spec.status === 'implementing')) {
        breakdown.noDrift = POINTS_NO_DRIFT;
        details.noDrift = 'OK — no drift detected';
    }
    else {
        details.noDrift = hasDrift ? `Drift score: ${safeDrift}` : `Status: ${spec.status}`;
    }
    const total = breakdown.specComplete +
        breakdown.testsExist +
        breakdown.testsPass +
        breakdown.coverageOk +
        breakdown.noDrift;
    return { specId, total, breakdown, details };
}
export function buildValidationPlan(specIds, maxIterations, testCommand, autoFix) {
    const plan = specIds.map((specId) => ({
        specId,
        steps: [
            `Run: planu validate --specId ${specId}`,
            `Run: ${testCommand} -- ${specId}`,
            autoFix
                ? `If failing: apply suggested fixes from validate output`
                : `Review failures manually`,
            `Re-run validate to confirm`,
        ],
        expectedOutcome: 'All acceptance criteria verified, tests green',
    }));
    const summary = [
        `Validation loop plan for ${specIds.length} spec(s), max ${maxIterations} iteration(s).`,
        `Test command: ${testCommand}`,
        `Auto-fix: ${autoFix ? 'enabled' : 'disabled'}`,
        ``,
        `NOTE: Planu cannot execute shell commands directly. `,
        `Copy the steps below and run them in your terminal.`,
    ].join('\n');
    return {
        iterations: maxIterations,
        finalStatus: 'partial',
        summary,
        plan,
    };
}
//# sourceMappingURL=validation-loop.js.map