/**
 * Fuzz Runner
 *
 * Core executor for fuzz tests with timeout and memory monitoring.
 */
import { appendFileSync, writeFileSync } from "node:fs";
import { Bash } from "../../../Bash.js";
import { DEFAULT_FUZZ_CONFIG } from "../config.js";
import { FeatureCoverage, } from "../coverage/feature-coverage.js";
/**
 * Fuzz runner that executes bash scripts with timeout and memory monitoring.
 */
export class FuzzRunner {
    config;
    scriptCount = 0;
    constructor(config) {
        this.config = { ...DEFAULT_FUZZ_CONFIG, ...config };
        // Clear the script log file at start if configured
        if (this.config.scriptLogFile) {
            try {
                writeFileSync(this.config.scriptLogFile, `# Fuzz test scripts - ${new Date().toISOString()}\n# Config: numRuns=${this.config.numRuns}, timeout=${this.config.timeoutMs}ms\n\n`);
            }
            catch {
                // Ignore errors if file can't be written
            }
        }
        // Note: failure log is append-only, never cleared
    }
    /**
     * Log script at start of execution (in case test times out).
     */
    logScriptStart(script) {
        if (!this.config.scriptLogFile)
            return;
        this.scriptCount++;
        try {
            appendFileSync(this.config.scriptLogFile, `# [${this.scriptCount}] RUNNING...\n${script}\n`);
        }
        catch {
            // Ignore errors if file can't be written
        }
    }
    /**
     * Log script completion status.
     */
    logScriptEnd(result) {
        if (!this.config.scriptLogFile)
            return;
        const status = result.timedOut
            ? "TIMEOUT"
            : result.hitLimit
                ? "LIMIT"
                : result.error
                    ? "ERROR"
                    : "OK";
        try {
            appendFileSync(this.config.scriptLogFile, `# -> ${status} (${result.durationMs}ms)\n\n`);
        }
        catch {
            // Ignore errors if file can't be written
        }
    }
    /**
     * Run a fuzz test with the given script.
     */
    async run(script) {
        // Log script at start (in case of vitest-level timeout)
        this.logScriptStart(script);
        const violations = [];
        const startTime = Date.now();
        const startMemory = process.memoryUsage().heapUsed;
        // Create coverage collector if enabled
        const coverageCollector = this.config.enableCoverage
            ? new FeatureCoverage()
            : undefined;
        // Create bash instance with fuzzing config
        const bash = new Bash({
            executionLimits: this.config.executionLimits,
            security: {
                defenseInDepth: this.config.defenseInDepth
                    ? {
                        enabled: true,
                        auditMode: false,
                        onViolation: (v) => violations.push(v),
                    }
                    : false,
            },
            debug: {
                coverage: coverageCollector,
            },
        });
        const result = {
            script,
            completed: false,
            timedOut: false,
            durationMs: 0,
            memoryDeltaBytes: 0,
            violations,
            hitLimit: false,
        };
        // Use AbortController to cancel exec on timeout
        const controller = new AbortController();
        let timerId;
        try {
            const execPromise = bash.exec(script, {
                signal: controller.signal,
            });
            const timeoutPromise = new Promise((resolve) => {
                timerId = setTimeout(() => {
                    controller.abort();
                    resolve("timeout");
                }, this.config.timeoutMs);
            });
            const raceResult = await Promise.race([execPromise, timeoutPromise]);
            const endTime = Date.now();
            const endMemory = process.memoryUsage().heapUsed;
            result.durationMs = endTime - startTime;
            result.memoryDeltaBytes = endMemory - startMemory;
            if (raceResult === "timeout") {
                result.timedOut = true;
                result.completed = false;
            }
            else {
                result.completed = true;
                result.bashResult = raceResult;
                result.exitCode = raceResult.exitCode;
                result.stderr = raceResult.stderr;
                result.stdout = raceResult.stdout;
                // Check if execution hit a limit gracefully
                result.hitLimit =
                    raceResult.exitCode === 126 ||
                        raceResult.stderr.includes("maximum") ||
                        raceResult.stderr.includes("limit") ||
                        raceResult.stderr.includes("too many") ||
                        raceResult.stderr.includes("exceeded");
            }
        }
        catch (error) {
            const endTime = Date.now();
            const endMemory = process.memoryUsage().heapUsed;
            result.durationMs = endTime - startTime;
            result.memoryDeltaBytes = endMemory - startMemory;
            result.error = error instanceof Error ? error : new Error(String(error));
            result.completed = true; // Error is a form of completion
            // Check if error indicates a limit was hit
            const errorMsg = result.error.message.toLowerCase();
            result.hitLimit =
                errorMsg.includes("limit") ||
                    errorMsg.includes("maximum") ||
                    errorMsg.includes("exceeded");
        }
        finally {
            if (timerId !== undefined) {
                clearTimeout(timerId);
            }
        }
        // Capture coverage snapshot if enabled
        if (coverageCollector) {
            result.coverage = coverageCollector.snapshot();
        }
        // Log script completion
        this.logScriptEnd(result);
        return result;
    }
    /**
     * Run multiple scripts and collect results.
     */
    async runBatch(scripts) {
        const results = [];
        for (const script of scripts) {
            results.push(await this.run(script));
        }
        return results;
    }
    /**
     * Get the current configuration.
     */
    getConfig() {
        return { ...this.config };
    }
    /**
     * Log a test failure to the failure log file.
     * Call this when an assertion fails to record the failing script.
     */
    logFailure(result, reason) {
        if (!this.config.failureLogFile)
            return;
        try {
            const entry = [
                `# ===== FAILURE =====`,
                `# Reason: ${reason}`,
                `# Time: ${new Date().toISOString()}`,
                `# Duration: ${result.durationMs}ms`,
                `# Completed: ${result.completed}`,
                `# Timed out: ${result.timedOut}`,
                `# Hit limit: ${result.hitLimit}`,
                `# Exit code: ${result.exitCode}`,
                result.error ? `# Error: ${result.error.message}` : "",
                `# Script:`,
                result.script,
                `# Stdout:`,
                result.stdout || "(empty)",
                `# Stderr:`,
                result.stderr || "(empty)",
                `# ====================\n\n`,
            ]
                .filter(Boolean)
                .join("\n");
            appendFileSync(this.config.failureLogFile, entry);
        }
        catch {
            // Ignore errors if file can't be written
        }
    }
}
