import Docker from "dockerode";
import path from "path";
import fs from "fs";
import os from "os";
import { Writable } from "stream";
import { stripVTControlCharacters } from "util";
import { logger } from "../utils/logger.js";
import { buildContainerEnv } from "./containerEnv.js";
import { SKYRAMP_IMAGE_VERSION } from "../utils/versions.js";
const DEFAULT_TIMEOUT = 300000; // 5 minutes
const MAX_CONCURRENT_EXECUTIONS = 5;
export const EXECUTOR_DOCKER_IMAGE = `skyramp/executor:${SKYRAMP_IMAGE_VERSION}`;
const DOCKER_PLATFORM = "linux/amd64";
const EXECUTION_PROGRESS_INTERVAL = 10000; // 10 seconds between progress updates during execution
// Temp file with valid empty JSON — used instead of /dev/null for .json config files
// so Node.js doesn't throw ERR_INVALID_PACKAGE_CONFIG when reading them.
const EMPTY_JSON_PATH = path.join(os.tmpdir(), "skyramp-empty.json");
fs.writeFileSync(EMPTY_JSON_PATH, "{}");
// Minimal Playwright config — shadows repo configs that may import dotenv or other
// dependencies not available in the executor container.
const MINIMAL_PLAYWRIGHT_CONFIG_PATH = path.join(os.tmpdir(), "skyramp-playwright.config.ts");
fs.writeFileSync(MINIMAL_PLAYWRIGHT_CONFIG_PATH, `import { defineConfig } from "@playwright/test";
export default defineConfig({ testDir: ".", timeout: 30000, retries: 0 });
`);
// Playwright config files to shadow with minimal config (prevents dotenv import errors)
export const PLAYWRIGHT_CONFIG_FILES = [
    "playwright.config.ts",
    "playwright.config.js",
    "playwright.config.mjs",
];
// Directories to skip mounting entirely (cannot bind-mount /dev/null to a directory)
export const EXCLUDED_MOUNT_ITEMS = [
    "node_modules",
];
// Files to shadow with /dev/null recursively so the container ignores them
export const MOUNT_NULL_ITEMS = [
    "package-lock.json",
    "package.json",
    "pnpm-lock.yaml",
    "pnpm-workspace.yaml",
    "pytest.toml",
    "pyproject.toml",
    "tox.ini",
    "setup.cfg",
    "pytest.ini",
    "setup.py",
    "__init__.py",
    "conftest.py",
];
/**
 * Find the start index of a comment in a line, ignoring comment delimiters inside strings
 * Returns -1 if no comment is found outside of strings
 */
function findCommentStart(line) {
    let inSingleQuote = false;
    let inDoubleQuote = false;
    let escaped = false;
    for (let i = 0; i < line.length; i++) {
        const char = line[i];
        const nextChar = i + 1 < line.length ? line[i + 1] : "";
        // Handle escape sequences
        if (escaped) {
            escaped = false;
            continue;
        }
        if (char === "\\") {
            escaped = true;
            continue;
        }
        // Track string boundaries
        if (char === "'" && !inDoubleQuote) {
            inSingleQuote = !inSingleQuote;
            continue;
        }
        if (char === '"' && !inSingleQuote) {
            inDoubleQuote = !inDoubleQuote;
            continue;
        }
        // Check for comments only if not in a string
        if (!inSingleQuote && !inDoubleQuote) {
            if (char === "/" && nextChar === "/") {
                return i;
            }
            if (char === "#") {
                return i;
            }
        }
    }
    return -1; // No comment found
}
/**
 * Filter out comments from code lines
 * Returns array of non-comment lines with inline comments removed
 */
function filterComments(lines) {
    const nonCommentLines = [];
    let inMultiLineComment = false;
    let inPythonMultiLineComment = false;
    for (let line of lines) {
        const trimmed = line.trim();
        // Check for Python multi-line string comments (""" or ''')
        // Count occurrences to handle single-line strings like '''comment'''
        const tripleDoubleCount = (trimmed.match(/"""/g) || []).length;
        const tripleSingleCount = (trimmed.match(/'''/g) || []).length;
        if (tripleDoubleCount > 0) {
            // Odd number: opening or closing a multi-line comment (toggle state)
            // Even number: complete string on same line (e.g., """comment""" - don't toggle)
            if (tripleDoubleCount % 2 === 1) {
                inPythonMultiLineComment = !inPythonMultiLineComment;
            }
            continue; // Skip any line with triple quotes
        }
        if (tripleSingleCount > 0) {
            if (tripleSingleCount % 2 === 1) {
                inPythonMultiLineComment = !inPythonMultiLineComment;
            }
            continue;
        }
        if (inPythonMultiLineComment) {
            continue;
        }
        // Check for multi-line comment start/end (/* */)
        if (trimmed.includes("/*")) {
            if (trimmed.includes("*/")) {
                // Single-line multi-line comment (e.g., /* comment */)
                // Don't change state, just skip the line
                continue;
            }
            else {
                // Opening a multi-line comment
                inMultiLineComment = true;
                continue;
            }
        }
        if (inMultiLineComment) {
            if (trimmed.includes("*/")) {
                inMultiLineComment = false;
            }
            continue;
        }
        // Skip single-line comments
        if (trimmed.startsWith("//") || trimmed.startsWith("#")) {
            continue;
        }
        // Remove inline comments from the line before processing
        // Use helper function to avoid removing comment delimiters inside strings
        const commentIndex = findCommentStart(line);
        if (commentIndex >= 0) {
            line = line.substring(0, commentIndex);
        }
        nonCommentLines.push(line);
    }
    return nonCommentLines;
}
/**
 * Detect session file paths referenced in test files
 * Looks for storageState patterns in TypeScript/JavaScript/Python/Java/C# test files
 * Excludes matches found in comments
 *
 * Also handles the codegen pattern `path.join(__dirname, '<filename>')` (TS/JS) —
 * the filename is resolved relative to the test file's directory on the host so
 * the existing absolute-path mount branch makes it visible at the same path
 * inside the container (Playwright's TS loader resolves __dirname to the host
 * workspace path at runtime).
 */
export function detectSessionFiles(testFilePath) {
    try {
        const content = fs.readFileSync(testFilePath, "utf-8");
        const lines = content.split("\n");
        const sessionFiles = [];
        // Pattern for TypeScript/JavaScript: storageState: '/path/to/file' or storageState: "/path/to/file"
        const tsJsPattern = /storageState:\s*['"]([^'"]+)['"]/g;
        // Pattern for TypeScript/JavaScript with path.join(__dirname, 'filename') — covers
        // both the inline form (`storageState: path.join(__dirname, '...')`) and the
        // variable-assignment form (`const X = path.join(__dirname, '...')` then
        // `storageState: X`) the skyramp codegen emits.
        const tsJsPathJoinPattern = /path\.join\s*\(\s*__dirname\s*,\s*['"]([^'"]+)['"]\s*\)/g;
        // Pattern for Python: storage_state='/path/to/file' or storage_state="/path/to/file"
        const pythonPattern = /storage_state\s*=\s*['"]([^'"]+)['"]/g;
        // Pattern for Java: setStorageState(Paths.get("path")) or setStorageState("path")
        // Enforces proper parenthesis matching: first ) is required, second ) is required when using Paths.get
        const javaPattern = /setStorageState(?:Path)?\s*\(\s*(?:Paths\.get\s*\(\s*)?['"]([^'"]+)['"]\s*\)(?:\s*\))?/g;
        // Pattern for C#: StorageStatePath = "path" or StorageState = "path"
        const csharpPattern = /StorageState(?:Path)?\s*=\s*['"]([^'"]+)['"]/g;
        // Filter out comments
        const codeLines = filterComments(lines);
        const testFileDir = path.dirname(testFilePath);
        // Process each non-comment line
        for (const line of codeLines) {
            // Try all patterns on this line
            let match;
            tsJsPattern.lastIndex = 0;
            while ((match = tsJsPattern.exec(line)) !== null) {
                sessionFiles.push(match[1]);
            }
            tsJsPathJoinPattern.lastIndex = 0;
            while ((match = tsJsPathJoinPattern.exec(line)) !== null) {
                // Resolve relative to the test file's host directory so the absolute-
                // path branch below mounts it at the same path inside the container.
                sessionFiles.push(path.resolve(testFileDir, match[1]));
            }
            pythonPattern.lastIndex = 0;
            while ((match = pythonPattern.exec(line)) !== null) {
                sessionFiles.push(match[1]);
            }
            javaPattern.lastIndex = 0;
            while ((match = javaPattern.exec(line)) !== null) {
                sessionFiles.push(match[1]);
            }
            csharpPattern.lastIndex = 0;
            while ((match = csharpPattern.exec(line)) !== null) {
                sessionFiles.push(match[1]);
            }
        }
        return sessionFiles;
    }
    catch (error) {
        logger.error(`Failed to detect session files in ${testFilePath}`, {
            error,
        });
        return [];
    }
}
/**
 * Recursively find all files/directories matching names in excludedItems within a directory.
 * Skips recursing into directories that are themselves excluded.
 */
function findExcludedPaths(dir, excludedItems) {
    const results = [];
    let entries;
    try {
        entries = fs.readdirSync(dir, { withFileTypes: true });
    }
    catch {
        return results;
    }
    for (const entry of entries) {
        const fullPath = path.join(dir, entry.name);
        // Only shadow files — mounting /dev/null to a directory target causes Docker errors
        if (entry.isFile() && excludedItems.includes(entry.name)) {
            results.push(fullPath);
        }
        if (entry.isDirectory() && !excludedItems.includes(entry.name) && !EXCLUDED_MOUNT_ITEMS.includes(entry.name)) {
            results.push(...findExcludedPaths(fullPath, excludedItems));
        }
    }
    return results;
}
export class TestExecutionService {
    docker;
    imageReady = null;
    constructor() {
        this.docker = new Docker();
    }
    /**
     * Execute multiple tests in parallel batches
     */
    async executeBatch(testOptions) {
        logger.info(`Starting batch execution of ${testOptions.length} tests`);
        const startTime = Date.now();
        const results = [];
        // Execute tests in batches to control concurrency
        for (let i = 0; i < testOptions.length; i += MAX_CONCURRENT_EXECUTIONS) {
            const batch = testOptions.slice(i, i + MAX_CONCURRENT_EXECUTIONS);
            logger.debug(`Executing batch ${Math.floor(i / MAX_CONCURRENT_EXECUTIONS) + 1} (${batch.length} tests)`);
            const batchPromises = batch.map((options) => this.executeTest(options).catch((error) => {
                // If execution fails completely, return error result
                return {
                    testFile: options.testFile,
                    passed: false,
                    executedAt: new Date().toISOString(),
                    duration: 0,
                    errors: [error.message],
                    warnings: [],
                    crashed: true,
                };
            }));
            const batchResults = await Promise.all(batchPromises);
            results.push(...batchResults);
        }
        const totalDuration = Date.now() - startTime;
        const passed = results.filter((r) => r.passed).length;
        const failed = results.filter((r) => !r.passed && !r.crashed).length;
        const crashed = results.filter((r) => r.crashed).length;
        logger.info(`Batch execution complete: ${passed} passed, ${failed} failed, ${crashed} crashed`);
        return {
            totalTests: testOptions.length,
            passed,
            failed,
            crashed,
            totalDuration,
            results,
        };
    }
    /**
     * Execute a single test
     * @param options Test execution options
     * @param onProgress Optional callback for progress updates
     */
    async executeTest(options, onProgress) {
        const startTime = Date.now();
        const executedAt = new Date().toISOString();
        logger.debug(`Executing test: ${options.testFile}`);
        // Check and ensure Docker image is ready with progress reporting
        await onProgress?.({
            phase: "docker-check",
            message: "Checking Docker image availability...",
            percent: 5,
        });
        const imageResult = await this.ensureDockerImage(onProgress);
        if (imageResult.cached) {
            await onProgress?.({
                phase: "docker-check",
                message: "Using cached Docker image",
                percent: 20,
                details: { cached: true },
            });
        }
        else {
            await onProgress?.({
                phase: "docker-pull",
                message: "Docker image pull completed",
                percent: 20,
                details: { cached: false },
            });
        }
        // Report preparing phase
        await onProgress?.({
            phase: "preparing",
            message: "Validating workspace and test file...",
            percent: 25,
        });
        // Validate workspace path - use accessSync for better validation
        const workspacePath = path.resolve(options.workspacePath);
        try {
            fs.accessSync(workspacePath, fs.constants.R_OK);
        }
        catch (err) {
            throw new Error(`Workspace path does not exist or is not readable: ${workspacePath}`);
        }
        // Validate test file - use basename for safer filename extraction
        const filename = path.basename(options.testFile);
        if (!filename) {
            throw new Error("Invalid test file path: could not extract filename");
        }
        if (!fs.existsSync(options.testFile)) {
            throw new Error(`Test file does not exist: ${options.testFile}`);
        }
        await onProgress?.({
            phase: "preparing",
            message: "Preparing Docker container configuration...",
            percent: 30,
        });
        const containerMountPath = "/home/user";
        const dockerSocketPath = "/var/run/docker.sock";
        // Calculate relative test file path
        let testFilePath = path.relative(workspacePath, options.testFile);
        testFilePath = path.resolve(containerMountPath, testFilePath);
        // Prepare Docker command
        const command = [
            "/root/runner.sh",
            options.language,
            testFilePath,
            options.testType,
        ];
        // Prepare host config with mounts
        const hostConfig = {
            ExtraHosts: ["host.docker.internal:host-gateway"],
            ...(options.useHostNetwork && process.platform === "linux" ? { NetworkMode: "host" } : {}),
            Mounts: [
                {
                    Type: "bind",
                    Target: dockerSocketPath,
                    Source: dockerSocketPath,
                },
            ],
        };
        // Mount workspace files, skipping EXCLUDED_MOUNT_ITEMS completely.
        //
        // Each workspace entry is bind-mounted at BOTH the canonical /home/user
        // path AND its host-absolute path. The dual mount lets the test resolve
        // any absolute reference the codegen happens to embed (storageState,
        // fixture paths, snapshots) — including the host workspace path that
        // Playwright's TypeScript loader sometimes produces from `__dirname` —
        // without needing source-code detection. EXCLUDED_MOUNT_ITEMS
        // (node_modules) stays excluded at both targets; MOUNT_NULL_ITEMS
        // shadows (package.json → empty JSON, etc.) and PLAYWRIGHT_CONFIG_FILES
        // shadows (minimal config) are applied at both targets too so the
        // protections survive regardless of which path the test resolves to.
        const workspaceFiles = fs.readdirSync(workspacePath);
        const filesToMount = workspaceFiles.filter((file) => !EXCLUDED_MOUNT_ITEMS.includes(file) && !MOUNT_NULL_ITEMS.includes(file));
        // Single Set tracks every mount target we've added so far. Used to dedupe
        // both the workspace-mirror push (when workspacePath happens to equal
        // containerMountPath) and the session-file push below.
        const mountedPaths = new Set();
        const pushMount = (mount) => {
            if (mountedPaths.has(mount.Target))
                return;
            mountedPaths.add(mount.Target);
            hostConfig.Mounts.push(mount);
        };
        const mirrorAtHostPath = workspacePath !== containerMountPath;
        for (const file of filesToMount) {
            const source = path.join(workspacePath, file);
            pushMount({
                Type: "bind",
                Target: path.join(containerMountPath, file),
                Source: source,
            });
            if (mirrorAtHostPath) {
                pushMount({ Type: "bind", Target: source, Source: source });
            }
        }
        // Mount MOUNT_NULL_ITEMS (found recursively) to /dev/null (or empty JSON for .json files)
        const nullPaths = findExcludedPaths(workspacePath, MOUNT_NULL_ITEMS);
        for (const absolutePath of nullPaths) {
            const rel = path.relative(workspacePath, absolutePath);
            const source = absolutePath.endsWith(".json") ? EMPTY_JSON_PATH : "/dev/null";
            pushMount({
                Type: "bind",
                Source: source,
                Target: path.join(containerMountPath, rel),
            });
            if (mirrorAtHostPath) {
                pushMount({ Type: "bind", Source: source, Target: absolutePath });
            }
        }
        // Mount Playwright config files with minimal config (shadows repo configs that may
        // import dotenv or other dependencies not available in the executor container)
        const playwrightConfigPaths = findExcludedPaths(workspacePath, PLAYWRIGHT_CONFIG_FILES);
        for (const absolutePath of playwrightConfigPaths) {
            const rel = path.relative(workspacePath, absolutePath);
            pushMount({
                Type: "bind",
                Source: MINIMAL_PLAYWRIGHT_CONFIG_PATH,
                Target: path.join(containerMountPath, rel),
            });
            if (mirrorAtHostPath) {
                pushMount({
                    Type: "bind",
                    Source: MINIMAL_PLAYWRIGHT_CONFIG_PATH,
                    Target: absolutePath,
                });
            }
        }
        // Detect and mount session files referenced outside the workspace
        // (anything inside the workspace is already covered by the dual mount
        // above; the session-file loop is the safety net for tests that point
        // at a session in some other directory).
        const sessionFiles = detectSessionFiles(options.testFile);
        for (const sessionFile of sessionFiles) {
            let sessionFileSource;
            let sessionFileTarget;
            if (path.isAbsolute(sessionFile)) {
                // Absolute path: mount at the same absolute path in container
                sessionFileSource = sessionFile;
                sessionFileTarget = sessionFile;
            }
            else {
                // Relative path: resolve from test file directory on host
                const testFileDir = path.dirname(options.testFile);
                sessionFileSource = path.resolve(testFileDir, sessionFile);
                // Mount at the corresponding relative path in the container
                const testFileDirInContainer = path.dirname(testFilePath);
                sessionFileTarget = path.resolve(testFileDirInContainer, sessionFile);
            }
            // Check if session file exists on host
            if (fs.existsSync(sessionFileSource)) {
                // Only mount if we haven't already mounted this path
                if (!mountedPaths.has(sessionFileTarget)) {
                    logger.info(` docker run -v ${sessionFileSource}:${sessionFileTarget}:ro ...`);
                    hostConfig.Mounts?.push({
                        Type: "bind",
                        Target: sessionFileTarget,
                        Source: sessionFileSource,
                        ReadOnly: true,
                    });
                    mountedPaths.add(sessionFileTarget);
                }
            }
            else {
                logger.error(`✗ Session file not found: ${sessionFileSource}`);
                logger.error(`  Referenced in test as: ${sessionFile}`);
            }
        }
        // Handle playwright save storage path
        let saveStorageTargetPath = "";
        if (options.playwrightSaveStoragePath) {
            let saveStorageSource;
            let saveStorageTarget;
            if (path.isAbsolute(options.playwrightSaveStoragePath)) {
                // Absolute path: use as-is
                saveStorageSource = path.dirname(options.playwrightSaveStoragePath);
                saveStorageTarget = path.dirname(options.playwrightSaveStoragePath);
                saveStorageTargetPath = options.playwrightSaveStoragePath;
            }
            else {
                // Relative path: resolve from workspace
                const absolutePath = path.resolve(workspacePath, options.playwrightSaveStoragePath);
                saveStorageSource = path.dirname(absolutePath);
                saveStorageTarget = path.join(containerMountPath, path.dirname(options.playwrightSaveStoragePath));
                saveStorageTargetPath = path.join(containerMountPath, options.playwrightSaveStoragePath);
            }
            // Ensure the directory exists on host
            if (!fs.existsSync(saveStorageSource)) {
                fs.mkdirSync(saveStorageSource, { recursive: true });
            }
            // Mount the directory as writable (not read-only)
            if (!mountedPaths.has(saveStorageTarget)) {
                logger.info(` docker run -v ${saveStorageSource}:${saveStorageTarget} ...`);
                hostConfig.Mounts?.push({
                    Type: "bind",
                    Target: saveStorageTarget,
                    Source: saveStorageSource,
                    ReadOnly: false,
                });
                mountedPaths.add(saveStorageTarget);
            }
        }
        const env = buildContainerEnv(options, saveStorageTargetPath);
        // Capture output
        let output = "";
        class DockerStream extends Writable {
            _write(data, encode, cb) {
                output += data.toString();
                cb();
            }
        }
        const stream = new DockerStream();
        try {
            // Report executing phase
            await onProgress?.({
                phase: "executing",
                message: "Starting test execution in Docker container...",
                percent: 40,
            });
            let statusCode = 0;
            let containerRef = null;
            // Start periodic progress reporter during execution
            // Runs in parallel with docker.run() to keep the AI agent informed
            let progressIntervalHandle;
            if (onProgress) {
                let progressTick = 0;
                progressIntervalHandle = setInterval(() => {
                    progressTick++;
                    const elapsed = Math.floor((Date.now() - startTime) / 1000);
                    // Progress moves from 40% to 79% during execution phase
                    // Slowly increment to show activity without reaching 80% (reserved for processing)
                    const percent = Math.min(40 + progressTick * 2, 79);
                    onProgress({
                        phase: "executing",
                        message: `Test running... (${elapsed}s elapsed)`,
                        percent,
                    }).catch(() => {
                        // Ignore progress notification errors
                    });
                }, EXECUTION_PROGRESS_INTERVAL);
            }
            // Run container with timeout
            const executionPromise = this.docker
                .run(EXECUTOR_DOCKER_IMAGE, command, stream, {
                Env: env,
                HostConfig: hostConfig,
                WorkingDir: containerMountPath, // Explicitly set working directory
            })
                .then(function (data) {
                const result = data[0];
                const container = data[1];
                containerRef = container; // Capture container reference for cleanup
                stream.end();
                statusCode = result.StatusCode;
                logger.debug("Docker container execution completed");
                return container.remove();
            })
                .then(function () {
                logger.debug("Docker container removed successfully");
                containerRef = null; // Container already removed
                return statusCode;
            })
                .catch(function (err) {
                logger.error("Docker container execution failed", {
                    error: err.message,
                });
                throw err;
            });
            // Apply timeout
            const timeout = options.timeout || DEFAULT_TIMEOUT;
            let timeoutHandle;
            const timeoutPromise = new Promise((_, reject) => {
                timeoutHandle = setTimeout(() => reject(new Error(`Test execution timeout after ${timeout}ms`)), timeout);
            });
            try {
                statusCode = await Promise.race([executionPromise, timeoutPromise]);
                // Clear timers on successful completion
                if (timeoutHandle)
                    clearTimeout(timeoutHandle);
                if (progressIntervalHandle)
                    clearInterval(progressIntervalHandle);
            }
            catch (error) {
                // Clear all timers on error
                if (timeoutHandle)
                    clearTimeout(timeoutHandle);
                if (progressIntervalHandle)
                    clearInterval(progressIntervalHandle);
                // Cleanup container on timeout or other errors
                if (containerRef !== null) {
                    const container = containerRef;
                    try {
                        logger.warning("Cleaning up orphaned Docker container after error");
                        await container.stop({ t: 5 }); // Give 5 seconds to stop gracefully
                        await container.remove({ force: true });
                        logger.debug("Orphaned container cleaned up successfully");
                    }
                    catch (cleanupError) {
                        logger.error("Failed to cleanup orphaned container", {
                            error: cleanupError.message,
                        });
                    }
                }
                throw error; // Re-throw the original error
            }
            // Report processing phase
            await onProgress?.({
                phase: "processing",
                message: "Test execution completed, processing results...",
                percent: 80,
            });
            const duration = Date.now() - startTime;
            const cleanOutput = stripVTControlCharacters(output);
            logger.info("Test execution completed", {
                output: cleanOutput.substring(0, 200) +
                    (cleanOutput.length > 200 ? "..." : ""),
                statusCode,
            });
            // Parse errors and warnings from output
            const errors = this.parseErrors(cleanOutput);
            const warnings = this.parseWarnings(cleanOutput);
            // Check if test crashed
            const crashed = statusCode !== 0 && statusCode !== 1;
            const result = {
                testFile: options.testFile,
                passed: statusCode === 0,
                executedAt,
                duration,
                errors,
                warnings,
                crashed,
                output: cleanOutput,
                exitCode: statusCode,
            };
            // Report completion
            await onProgress?.({
                phase: "processing",
                message: result.passed
                    ? "Test passed successfully"
                    : "Test execution completed with failures",
                percent: 100,
            });
            logger.debug(`Test ${result.passed ? "passed" : "failed"}: ${options.testFile}`);
            return result;
        }
        catch (error) {
            const duration = Date.now() - startTime;
            const cleanOutput = stripVTControlCharacters(output);
            logger.error(`Test execution error: ${error.message}`);
            return {
                testFile: options.testFile,
                passed: false,
                executedAt,
                duration,
                errors: [error.message],
                warnings: [],
                crashed: true,
                output: cleanOutput,
            };
        }
    }
    /**
     * Ensure Docker image is available
     * @param onProgress Optional callback for progress updates during pull
     * @returns Object indicating whether image was cached or pulled
     */
    async ensureDockerImage(onProgress) {
        try {
            const images = await this.docker.listImages();
            const imageExists = images.some((img) => (img.RepoTags && img.RepoTags.includes(EXECUTOR_DOCKER_IMAGE)) ||
                (img.RepoDigests && img.RepoDigests.includes(EXECUTOR_DOCKER_IMAGE)));
            if (imageExists) {
                logger.debug(`Docker image ${EXECUTOR_DOCKER_IMAGE} already available`);
                return { cached: true };
            }
            logger.info(`Pulling Docker image ${EXECUTOR_DOCKER_IMAGE}...`);
            await onProgress?.({
                phase: "docker-pull",
                message: `Pulling Docker image ${EXECUTOR_DOCKER_IMAGE}...`,
                percent: 10,
                details: { cached: false },
            });
            await new Promise((resolve, reject) => {
                this.docker.pull(EXECUTOR_DOCKER_IMAGE, { platform: DOCKER_PLATFORM }, (err, stream) => {
                    if (err)
                        return reject(err);
                    if (!stream)
                        return reject(new Error("No stream received from docker pull"));
                    this.docker.modem.followProgress(stream, (err, res) => {
                        if (err)
                            return reject(err);
                        logger.info(`Docker image ${EXECUTOR_DOCKER_IMAGE} pulled successfully`);
                        resolve(res);
                    }, 
                    // Progress callback for each layer
                    (event) => {
                        if (event.status && onProgress) {
                            const progressMsg = event.progress || event.status;
                            const layerId = event.id || "";
                            // Calculate approximate progress (10-19% range during pull)
                            let percent = 10;
                            if (event.progressDetail?.current &&
                                event.progressDetail?.total) {
                                const layerProgress = event.progressDetail.current / event.progressDetail.total;
                                percent = 10 + Math.floor(layerProgress * 9); // 10-19%
                            }
                            onProgress({
                                phase: "docker-pull",
                                message: `${event.status}${layerId ? ` [${layerId}]` : ""}: ${progressMsg}`,
                                percent,
                                details: {
                                    cached: false,
                                    pullProgress: {
                                        current: event.progressDetail?.current,
                                        total: event.progressDetail?.total,
                                        layer: layerId,
                                    },
                                },
                            }).catch(() => {
                                // Ignore progress notification errors
                            });
                        }
                    });
                });
            });
            return { cached: false };
        }
        catch (error) {
            logger.error(`Failed to ensure Docker image: ${error.message}`);
            throw new Error(`Docker image setup failed: ${error.message}`);
        }
    }
    /**
     * Parse errors from test output
     */
    parseErrors(output) {
        const errors = [];
        const lines = output.split("\n");
        for (const line of lines) {
            // Common error patterns
            if (line.includes("Error:") ||
                line.includes("ERROR") ||
                line.includes("AssertionError") ||
                line.includes("Exception") ||
                line.includes("FAILED") ||
                line.includes("Traceback")) {
                errors.push(line.trim());
            }
        }
        return errors.slice(0, 20); // Limit to first 20 errors
    }
    /**
     * Parse warnings from test output
     */
    parseWarnings(output) {
        const warnings = [];
        const lines = output.split("\n");
        for (const line of lines) {
            if (line.includes("Warning:") ||
                line.includes("WARN") ||
                line.includes("Deprecated")) {
                warnings.push(line.trim());
            }
        }
        return warnings.slice(0, 10); // Limit to first 10 warnings
    }
}
