// ---------------------------------------------------------------------------
// integration-runner.ts -- 5-phase browser-based integration test runner
//
// Phases: Preflight -> Connect -> Execute -> Verify -> Cleanup
// ---------------------------------------------------------------------------
import { randomUUID } from "node:crypto";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { createRequire } from "node:module";
import { resolveAllCredentials } from "./credential-resolver.js";
import { resolveProfile } from "./chrome-profile.js";
import { PlatformRateLimiter } from "./rate-limiter.js";
import { judgeAssertion } from "./judge.js";
import { createLlmClient } from "./llm.js";
// ---------------------------------------------------------------------------
// SIGINT cleanup state
// ---------------------------------------------------------------------------
let cleanupRegistered = false;
let cleanupFn = null;
let cleanupDone = false;
function registerSigintHandler(fn) {
    cleanupFn = fn;
    cleanupDone = false;
    if (!cleanupRegistered) {
        process.on("SIGINT", sigintHandler);
        cleanupRegistered = true;
    }
}
function deregisterSigintHandler() {
    process.removeListener("SIGINT", sigintHandler);
    cleanupRegistered = false;
    cleanupFn = null;
}
async function sigintHandler() {
    if (cleanupDone)
        return;
    cleanupDone = true;
    console.log("\nSIGINT received — running cleanup...");
    if (cleanupFn) {
        try {
            await cleanupFn();
            console.log("Cleanup complete, exiting.");
        }
        catch (err) {
            console.error("Cleanup failed:", err.message);
        }
    }
    process.exit(0);
}
// ---------------------------------------------------------------------------
// Playwright lazy check
// ---------------------------------------------------------------------------
export function checkPlaywright() {
    try {
        const require = createRequire(import.meta.url);
        require.resolve("playwright");
    }
    catch {
        throw new Error("Playwright is required for integration tests. Install it with:\n" +
            "  npm install --save-dev playwright && npx playwright install chromium");
    }
}
// ---------------------------------------------------------------------------
// Main runner
// ---------------------------------------------------------------------------
export async function runIntegrationCase(evalCase, opts) {
    const runId = opts.runId ?? randomUUID().slice(0, 8).toUpperCase();
    const testPrefix = `[VSKILL-TEST-${runId}]`;
    const phases = [];
    const testArtifactIds = [runId];
    let browser = null;
    let context = null;
    // Register cleanup for SIGINT
    registerSigintHandler(async () => {
        await runCleanup(evalCase, browser, testArtifactIds);
    });
    try {
        // -----------------------------------------------------------------------
        // Phase 1: PREFLIGHT
        // -----------------------------------------------------------------------
        const preflightResult = await runPhase("preflight", async () => {
            // Check credentials
            if (evalCase.requiredCredentials?.length) {
                const statuses = resolveAllCredentials(evalCase.requiredCredentials, opts.skillDir);
                const missing = statuses.filter((s) => s.status === "missing");
                if (missing.length > 0) {
                    throw new Error(`Missing credentials: ${missing.map((m) => m.name).join(", ")}. ` +
                        `Set them with: vskill credentials set <KEY>`);
                }
            }
            // Check Chrome profile
            const profileName = evalCase.requirements?.chromeProfile;
            const profilePath = evalCase.requirements?.chromeProfilePath;
            if (profileName && !profilePath) {
                resolveProfile(profileName);
            }
            // Check Playwright
            if (!opts.dryRun) {
                checkPlaywright();
            }
        });
        phases.push(preflightResult);
        if (preflightResult.status === "fail") {
            // Abort remaining phases
            for (const p of ["connect", "execute", "verify", "cleanup"]) {
                phases.push({ phase: p, status: "skipped" });
            }
            return buildResult(evalCase, runId, phases, testArtifactIds, !!opts.dryRun);
        }
        // -----------------------------------------------------------------------
        // Phase 2: CONNECT
        // -----------------------------------------------------------------------
        const connectResult = await runPhase("connect", async () => {
            if (opts.dryRun) {
                console.log(`[DRY RUN] Would launch browser with profile: ${evalCase.requirements?.chromeProfile ?? "default"}`);
                return;
            }
            const profileName = evalCase.requirements?.chromeProfile;
            const profilePath = evalCase.requirements?.chromeProfilePath ?? (profileName ? resolveProfile(profileName) : undefined);
            const pw = await import("playwright");
            if (profilePath) {
                context = await pw.chromium.launchPersistentContext(profilePath, {
                    headless: false,
                    args: ["--disable-blink-features=AutomationControlled"],
                });
                browser = null; // persistent context manages its own browser
            }
            else {
                browser = await pw.chromium.launch({ headless: false });
                context = await browser.newContext();
            }
        });
        phases.push(connectResult);
        if (connectResult.status === "fail") {
            phases.push({ phase: "execute", status: "skipped" });
            phases.push({ phase: "verify", status: "skipped" });
            phases.push(await runPhase("cleanup", () => runCleanup(evalCase, browser ?? context, testArtifactIds)));
            return buildResult(evalCase, runId, phases, testArtifactIds, !!opts.dryRun);
        }
        // -----------------------------------------------------------------------
        // Phase 3: EXECUTE
        // -----------------------------------------------------------------------
        let generatedOutput = "";
        const executeResult = await runPhase("execute", async () => {
            const platform = evalCase.requirements?.platform;
            // Rate limiting
            if (platform && !opts.dryRun) {
                const rateLimiter = new PlatformRateLimiter(evalCase.requirements?.rateLimit
                    ? { [platform]: evalCase.requirements.rateLimit }
                    : undefined);
                await rateLimiter.acquire(platform);
            }
            // Build prompt with test prefix
            const promptWithPrefix = `${testPrefix}\n\nIMPORTANT: All content you create or post MUST include the prefix "${testPrefix}" for identification and cleanup.\n\n${evalCase.prompt}`;
            if (opts.dryRun) {
                console.log(`[DRY RUN] Would execute LLM with prompt:\n${promptWithPrefix.slice(0, 200)}...`);
                generatedOutput = `[DRY RUN] Simulated output for: ${evalCase.name}`;
                return;
            }
            // Create LLM client and generate
            const client = createLlmClient();
            const skillMdPath = join(opts.skillDir, "SKILL.md");
            let systemPrompt = "You are executing an integration test. Follow the instructions precisely.";
            if (existsSync(skillMdPath)) {
                systemPrompt = readFileSync(skillMdPath, "utf-8");
            }
            const result = await client.generate(systemPrompt, promptWithPrefix);
            generatedOutput = result.text;
        });
        phases.push(executeResult);
        if (executeResult.status === "fail") {
            phases.push({ phase: "verify", status: "skipped" });
            phases.push(await runPhase("cleanup", () => runCleanup(evalCase, browser ?? context, testArtifactIds)));
            return buildResult(evalCase, runId, phases, testArtifactIds, !!opts.dryRun);
        }
        // -----------------------------------------------------------------------
        // Phase 4: VERIFY
        // -----------------------------------------------------------------------
        const verifyResult = await runPhase("verify", async () => {
            if (!evalCase.assertions?.length)
                return;
            const client = createLlmClient();
            const results = await Promise.all(evalCase.assertions.map((assertion) => judgeAssertion(generatedOutput, assertion, client)));
            const failed = results.filter((r) => !r.pass);
            if (failed.length > 0) {
                throw new Error(`${failed.length} assertion(s) failed:\n` +
                    failed.map((f) => `  - ${f.text}: ${f.reasoning}`).join("\n"));
            }
        });
        phases.push(verifyResult);
        // -----------------------------------------------------------------------
        // Phase 5: CLEANUP
        // -----------------------------------------------------------------------
        const cleanupResult = await runPhase("cleanup", () => runCleanup(evalCase, browser ?? context, testArtifactIds));
        phases.push(cleanupResult);
        return buildResult(evalCase, runId, phases, testArtifactIds, !!opts.dryRun);
    }
    finally {
        deregisterSigintHandler();
    }
}
// ---------------------------------------------------------------------------
// Phase executor
// ---------------------------------------------------------------------------
async function runPhase(phase, fn) {
    const start = Date.now();
    try {
        await fn();
        return { phase, status: "pass", durationMs: Date.now() - start };
    }
    catch (err) {
        return {
            phase,
            status: "fail",
            durationMs: Date.now() - start,
            errorMessage: err.message,
        };
    }
}
// ---------------------------------------------------------------------------
// Cleanup
// ---------------------------------------------------------------------------
async function runCleanup(evalCase, browserOrContext, _testArtifactIds) {
    // Run cleanup actions defined in the eval case
    if (evalCase.cleanup?.length) {
        for (const action of evalCase.cleanup) {
            try {
                if (action.execute) {
                    await action.execute();
                }
            }
            catch (err) {
                // Log but do not throw — test result stands independently
                console.error(`Cleanup action "${action.description}" failed:`, err.message);
            }
        }
    }
    // Close browser
    if (browserOrContext) {
        try {
            await browserOrContext.close();
        }
        catch {
            // Browser may already be closed
        }
    }
}
// ---------------------------------------------------------------------------
// Confirmation prompt
// ---------------------------------------------------------------------------
export async function promptConfirmation(platform, actions) {
    // Skip in CI
    if (process.env.CI === "true")
        return true;
    const { createInterface } = await import("node:readline");
    const rl = createInterface({ input: process.stdin, output: process.stdout });
    const actionList = actions.map((a) => `  - ${a}`).join("\n");
    const question = `\nThis will perform the following actions on ${platform}:\n${actionList}\n\nProceed? (y/N) `;
    return new Promise((resolve) => {
        rl.question(question, (answer) => {
            rl.close();
            resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
        });
    });
}
/**
 * Check if this is the first run against a platform (no history file).
 */
export function isFirstRun(skillDir) {
    return !existsSync(join(skillDir, "evals", ".integration-history.json"));
}
/**
 * Record a run in the integration history file.
 */
export function recordRun(skillDir, result) {
    const historyPath = join(skillDir, "evals", ".integration-history.json");
    let history = [];
    if (existsSync(historyPath)) {
        try {
            history = JSON.parse(readFileSync(historyPath, "utf-8"));
        }
        catch {
            history = [];
        }
    }
    history.push(result);
    writeFileSync(historyPath, JSON.stringify(history, null, 2), "utf-8");
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function buildResult(evalCase, runId, phases, testArtifactIds, dryRun) {
    const overallPass = phases.every((p) => p.status === "pass" || p.status === "skipped");
    return {
        evalId: String(evalCase.id),
        runId,
        phases,
        overallPass,
        testArtifactIds,
        dryRun,
    };
}
//# sourceMappingURL=integration-runner.js.map