import * as fs from "fs";
import * as path from "path";
import { logger } from "../utils/logger.js";
import { TestSource } from "../types/TestAnalysis.js";
import fg from "fast-glob";
export class TestDiscoveryService {
    EXCLUDED_DIRS = [
        "node_modules",
        "venv",
        ".venv",
        "build",
        "dist",
        ".git",
        "__pycache__",
        ".pytest_cache",
        "coverage",
        ".next",
        "out",
        "target",
    ];
    SKYRAMP_MARKER = "Generated by Skyramp";
    // Supported test file extensions — covers all common languages/frameworks
    SUPPORTED_EXTENSIONS = [
        ".py", ".js", ".ts", ".tsx", ".jsx", ".java",
        ".go", ".rb", ".kt", ".kts", ".cs", ".rs",
        ".php", ".scala", ".swift", ".m",
    ];
    // Concurrency control for parallel operations
    MAX_CONCURRENT_OPERATIONS = 10;
    // Max external files to fully extract in full-repo mode (no diff context).
    // In PR mode this hard limit is replaced by relevance-based partitioning which
    // naturally bounds the read set to files relevant to the changed endpoints.
    MAX_EXTERNAL_FULL_REPO = 100;
    // Test file naming patterns — match against basename.
    // Includes Skyramp-generated suffixes so that files like orders_smoke.py placed
    // outside a recognized test directory are still discovered.
    TEST_FILE_PATTERNS = [
        /^test_.*\.(py|js|ts|rb|go|php)$/, // test_*.py, test_*.rb, test_*.go
        /.*_test\.(py|ts|js|go|rs)$/, // *_test.py, *_test.go, *_test.rs
        /.*\.test\.(ts|js|tsx|jsx)$/, // *.test.ts, *.test.js, *.test.tsx
        /.*\.spec\.(ts|js|tsx|jsx|rb)$/, // *.spec.ts, *.spec.js, *.spec.rb
        /.*Test\.(java|kt|kts|cs|scala|swift|m)$/, // *Test.java, *Test.kt, *Test.m (ObjC)
        /.*Tests\.(cs|swift|m)$/, // *Tests.cs, *Tests.swift, *Tests.m (ObjC)
        /.*_spec\.rb$/, // *_spec.rb (RSpec)
        /.*\.test\.php$/, // *.test.php
        /.*Test\.php$/, // *Test.php (PHPUnit)
        // Skyramp-generated test suffixes — must be matched even outside test dirs
        /.*_(?:smoke|contract|fuzz|integration|load|e2e|ui)\.(py|ts|js|java|rb|go|cs|kt|kts|scala|swift|php|rs)$/i,
    ];
    // Directory patterns that signal test directories — works with both / and \ separators
    TEST_DIR_PATTERNS = [
        /[\\/]tests?[\\/]/,
        /[\\/]__tests__[\\/]/,
        /[\\/]spec[\\/]/,
    ];
    /**
     * Discover all tests under testDir — both Skyramp-generated and external (user-written).
     * Uses fast-glob for cross-platform file scanning, then classifies discovered files
     * as Skyramp-generated tests, external tests, or not-a-test during processing.
     *
     * External test handling depends on `options.changedResources`:
     * - `string[]` with entries (PR mode, endpoints detected): partition by relevance.
     * - `[]` empty array (PR mode, scanner found no endpoints): skip external tests entirely
     *   rather than flooding context with irrelevant files.
     * - `undefined` (full-repo mode, no diff): cap at MAX_EXTERNAL_FULL_REPO.
     */
    async discoverTests(testDir, options = {}) {
        logger.info(`Starting test discovery in: ${testDir}`);
        if (!fs.existsSync(testDir)) {
            throw new Error(`Test directory does not exist: ${testDir}`);
        }
        const stats = fs.statSync(testDir);
        if (!stats.isDirectory()) {
            throw new Error(`Path is not a directory: ${testDir}`);
        }
        // File classification: skyramp vs external vs not-a-test (carries content forward).
        const classified = this.classifyTestFiles(testDir);
        logger.info(`Found ${classified.skyramp.length} Skyramp test files, ${classified.external.length} external test files`);
        // Process Skyramp tests (content already cached from classification)
        const skyrampTests = await this.processFilesInBatches(classified.skyramp, false, classified.contentCache);
        skyrampTests.forEach(t => { t.source = TestSource.Skyramp; });
        // Partition external tests into relevant (full extraction) and low-relevance (name-only).
        //
        // PR mode + endpoints detected (changedResources is non-empty array):
        //   Files whose path/name token-overlaps with the changed resource names are "relevant".
        //   Only they get full endpoint extraction. Low-relevance files get name-only entries.
        //   No hard cap — the relevance filter naturally bounds the read set to PR scope.
        //   The sentinel ["unknown"] falls into this branch — most files score 0 (low-relevance)
        //   and get name-only entries, so external coverage is preserved without context flood.
        //
        // PR mode + truly no endpoints (changedResources is empty array []):
        //   Diff contained no endpoints at all (new, modified, or removed) — skip external
        //   tests entirely rather than flooding the prompt with hundreds of irrelevant files.
        //
        // Full-repo mode (changedResources is undefined):
        //   No diff context — all external files treated as potentially relevant.
        //   Cap at MAX_EXTERNAL_FULL_REPO to avoid reading hundreds of files.
        const { changedResources } = options;
        let relevantExternal;
        let otherExternal;
        if (changedResources?.length) {
            // PR mode with detected endpoints — partition by relevance
            ({ relevant: relevantExternal, other: otherExternal } =
                this.partitionByRelevance(classified.external, changedResources));
        }
        else if (changedResources !== undefined) {
            // PR mode with an explicit empty endpoint list from diff parsing — don't flood
            // context with irrelevant external tests. The LLM will work from Skyramp tests
            // and scanned endpoints only.
            relevantExternal = [];
            otherExternal = [];
        }
        else {
            // Full-repo mode (no diff context): cap full-extraction set, remaining become name-only
            relevantExternal = classified.external.slice(0, this.MAX_EXTERNAL_FULL_REPO);
            otherExternal = classified.external.slice(this.MAX_EXTERNAL_FULL_REPO);
        }
        logger.info(`External tests: ${relevantExternal.length} relevant (full extraction), ${otherExternal.length} low-relevance (name-only)`);
        // Free cached content for files we will not extract — their content is not needed.
        for (const f of otherExternal) {
            classified.contentCache.delete(f);
        }
        // Full metadata + endpoint extraction only for relevant external files
        const relevantExternalTests = await this.processFilesInBatches(relevantExternal, true, classified.contentCache);
        // Minimal entries for low-relevance files — no content reads, no endpoint extraction
        const otherExternalTests = otherExternal.map(filePath => ({
            testFile: filePath,
            testType: this.detectExternalTestType(filePath, ""),
            language: this.detectLanguage(filePath),
            framework: "",
            apiEndpoint: "",
            source: TestSource.External,
        }));
        const externalTests = [...relevantExternalTests, ...otherExternalTests];
        logger.info(`Discovered ${skyrampTests.length} Skyramp tests, ${externalTests.length} external tests`);
        return {
            tests: [...skyrampTests, ...externalTests],
            // Expose the relevant file paths so callers can build read instructions for the LLM.
            // These are the files the agent should read in Step 0 for definitive coverage
            // verification — includes files where regex extraction may have failed (e.g. helper
            // abstractions) since those most need semantic inspection.
            relevantExternalTestPaths: relevantExternal,
        };
    }
    /**
     * Score an external test file's relevance to a set of changed resource names.
     * Tokenises the last two segments of the file path (split by /, \, -, _, .)
     * and counts overlapping tokens with the changed resources.
     * Example: "test_orders_api.py" vs ["orders"] → score 1.
     */
    scoreRelevance(filePath, changedResources) {
        const normalized = filePath.toLowerCase().replace(/\\/g, "/");
        // Use the last two path segments to avoid false matches from deeply-nested dirs
        const segments = normalized.split("/").slice(-2).join("/");
        const tokens = new Set(segments.split(/[/\-_.]+/).filter(Boolean));
        return changedResources.filter(r => {
            const rLower = r.toLowerCase();
            // Direct single-token match (e.g. "orders" in file path tokens)
            if (tokens.has(rLower))
                return true;
            // Compound resource names (e.g. "order-items" from /api/order-items) — check
            // that ALL hyphen/underscore-separated parts appear as individual tokens.
            // This handles test_order_items.py correctly matching resource "order-items".
            const parts = rLower.split(/[-_]/);
            return parts.length > 1 && parts.every(p => p.length >= 3 && tokens.has(p));
        }).length;
    }
    /**
     * Partition external test files into relevant (score > 0) and low-relevance (score = 0)
     * based on file path/name overlap with the changed resource names from the PR diff.
     */
    partitionByRelevance(files, changedResources) {
        const relevant = [];
        const other = [];
        for (const f of files) {
            (this.scoreRelevance(f, changedResources) > 0 ? relevant : other).push(f);
        }
        return { relevant, other };
    }
    /**
     * Process test files in parallel batches with concurrency control
     * @param isExternal When true, uses external test metadata extraction
     * @param contentCache Optional pre-read file contents from classification pass
     */
    async processFilesInBatches(testFiles, isExternal = false, contentCache) {
        const results = [];
        // Process files in batches to control concurrency
        for (let i = 0; i < testFiles.length; i += this.MAX_CONCURRENT_OPERATIONS) {
            const batch = testFiles.slice(i, i + this.MAX_CONCURRENT_OPERATIONS);
            const batchResults = await Promise.all(batch.map(async (testFile) => {
                try {
                    const cachedContent = contentCache?.get(testFile);
                    return isExternal
                        ? await this.extractExternalTestMetadata(testFile, cachedContent)
                        : await this.extractTestMetadata(testFile, cachedContent);
                }
                catch (error) {
                    logger.error(`Error processing test file ${testFile}: ${error}`);
                    return null;
                }
            }));
            // Filter out null results and add to final results
            results.push(...batchResults.filter((test) => test !== null));
        }
        return results;
    }
    /**
     * Classify candidate files as skyramp (has marker), external (test file without
     * marker), or not-a-test. Caches file contents so downstream metadata extraction
     * doesn't need to re-read from disk.
     */
    classifyTestFiles(repositoryPath) {
        const globPatterns = this.SUPPORTED_EXTENSIONS.map((ext) => `**/*${ext}`);
        const ignorePatterns = this.EXCLUDED_DIRS.map((dir) => `**/${dir}/**`);
        const allFiles = fg.sync(globPatterns, {
            cwd: repositoryPath,
            ignore: ignorePatterns,
            absolute: true,
            caseSensitiveMatch: false,
        });
        // Pre-filter to likely test files before reading contents.
        // TEST_FILE_PATTERNS includes Skyramp-generated suffixes (_smoke, _contract, etc.)
        // so Skyramp files outside standard test directories are still discovered.
        // This avoids reading every source file in the repo just to check the marker.
        const testCandidates = allFiles.filter(f => this.isExternalTestFile(f));
        logger.debug(`Found ${allFiles.length} candidate files, narrowed to ${testCandidates.length} likely test files`);
        const result = { skyramp: [], external: [] };
        const contentCache = new Map();
        const marker = this.SKYRAMP_MARKER;
        const batchSize = 100;
        for (let i = 0; i < testCandidates.length; i += batchSize) {
            const batch = testCandidates.slice(i, i + batchSize);
            for (const file of batch) {
                try {
                    const content = fs.readFileSync(file, "utf-8");
                    if (content.includes(marker)) {
                        result.skyramp.push(file);
                        contentCache.set(file, content);
                    }
                    else {
                        result.external.push(file);
                        contentCache.set(file, content);
                    }
                }
                catch (error) {
                    logger.debug(`Skipping file ${file}: ${error}`);
                }
            }
        }
        logger.debug(`Classified ${result.skyramp.length} Skyramp files, ${result.external.length} external test files`);
        return { ...result, contentCache };
    }
    /**
     * Check if a file is an external (non-Skyramp) test file based on naming convention
     * or directory placement.
     */
    isExternalTestFile(filePath) {
        const basename = path.basename(filePath);
        if (this.TEST_FILE_PATTERNS.some(p => p.test(basename)))
            return true;
        if (this.TEST_DIR_PATTERNS.some(p => p.test(filePath)))
            return true;
        return false;
    }
    /**
     * Extract metadata from a test file
     * File is already confirmed to contain Skyramp marker by file search
     * @param cachedContent Optional pre-read content from classification pass
     */
    async extractTestMetadata(testFile, cachedContent) {
        let content;
        try {
            content = cachedContent ?? fs.readFileSync(testFile, "utf-8");
        }
        catch (error) {
            logger.debug(`Could not read file ${testFile}: ${error}`);
            return null;
        }
        const language = this.detectLanguage(testFile);
        const testType = this.detectTestType(content);
        const apiSchema = this.extractApiSchema(content);
        const framework = this.extractFramework(content);
        const apiEndpoint = this.extractCoveredEndpoints(content);
        return {
            testFile,
            testType,
            language,
            framework,
            apiSchema,
            apiEndpoint,
        };
    }
    /**
     * Extract the HTTP methods and paths covered by this test file.
     * Returns a comma-separated string like "GET /orders/{id}, DELETE /orders/{id}".
     * Handles Python (send_request / check_schema) and TypeScript/JS (sendRequest).
     */
    extractCoveredEndpoints(content) {
        const seen = new Set();
        // Python / Java: send_request("METHOD", "/path", ...)
        for (const m of content.matchAll(/send_request\(\s*["']([A-Z]+)["']\s*,\s*["']([^"']+)["']/g)) {
            seen.add(`${m[1]} ${m[2]}`);
        }
        // Python contract: check_schema("/path", "METHOD", ...)  — path comes first
        for (const m of content.matchAll(/check_schema\(\s*["']([^"']+)["']\s*,\s*["']([A-Z]+)["']/g)) {
            seen.add(`${m[2]} ${m[1]}`);
        }
        // TypeScript / JavaScript: sendRequest("METHOD", "/path", ...)
        for (const m of content.matchAll(/sendRequest\(\s*["']([A-Z]+)["']\s*,\s*["']([^"']+)["']/g)) {
            seen.add(`${m[1]} ${m[2]}`);
        }
        return [...seen].join(", ");
    }
    /**
     * Extract metadata from an external (non-Skyramp) test file.
     * Uses heuristic patterns for endpoint extraction, test type, and framework detection.
     * @param cachedContent Optional pre-read content from classification pass
     */
    async extractExternalTestMetadata(testFile, cachedContent) {
        let content;
        try {
            content = cachedContent ?? fs.readFileSync(testFile, "utf-8");
        }
        catch {
            return null;
        }
        const language = this.detectLanguage(testFile);
        const testType = this.detectExternalTestType(testFile, content);
        const framework = this.detectExternalFramework(content);
        const apiEndpoint = this.extractExternalEndpoints(content);
        return {
            testFile,
            testType,
            language,
            framework,
            apiEndpoint,
            source: TestSource.External,
        };
    }
    /**
     * Extract HTTP endpoints from external (non-Skyramp) test files.
     * Matches common patterns: requests.get, axios.post, fetch, supertest, RestAssured.
     */
    extractExternalEndpoints(content) {
        const seen = new Set();
        // Python requests / test client: requests.get("/path"), client.get("/path"), self.client.get("/path")
        // Also handles f-strings: requests.get(f"{BASE_URL}/path") — strips the variable prefix
        for (const m of content.matchAll(/(?:requests|client|self\.client)\.(get|post|put|patch|delete)\(\s*f?["'`](?:\{[^}]+\})?([^"'`\s]+)["'`]/gi)) {
            const ep = this.normalizeEndpointPath(m[2]);
            if (ep)
                seen.add(`${m[1].toUpperCase()} ${ep}`);
        }
        // JS fetch: fetch("/path", { method: "POST" }) — extract path, try to find method
        for (const m of content.matchAll(/fetch\(\s*["'`]([^"'`\s]+)["'`](?:\s*,\s*\{[^}]*method\s*:\s*["'](\w+)["'])?/g)) {
            const ep = this.normalizeEndpointPath(m[1]);
            if (ep)
                seen.add(`${(m[2] || "GET").toUpperCase()} ${ep}`);
        }
        // Axios: axios.get("/path"), axios.post("/path")
        for (const m of content.matchAll(/axios\.(get|post|put|patch|delete)\(\s*["'`]([^"'`\s]+)["'`]/gi)) {
            const ep = this.normalizeEndpointPath(m[2]);
            if (ep)
                seen.add(`${m[1].toUpperCase()} ${ep}`);
        }
        // Supertest: .get("/path"), .post("/path") — only if file references supertest
        if (/supertest|request\(app\)/i.test(content)) {
            for (const m of content.matchAll(/\.(get|post|put|patch|delete)\(\s*["'`](\/[^"'`\s]+)["'`]/gi)) {
                seen.add(`${m[1].toUpperCase()} ${m[2]}`);
            }
        }
        // Java RestAssured: when().get("/path"), .get("/path") after given()/when()
        for (const m of content.matchAll(/(?:when|then)\(\)\s*\.(get|post|put|patch|delete)\(\s*["']([^"']+)["']/gi)) {
            const ep = this.normalizeEndpointPath(m[2]);
            if (ep)
                seen.add(`${m[1].toUpperCase()} ${ep}`);
        }
        // Go net/http: http.Get("/path"), http.Post("/path"), http.NewRequest("METHOD", "/path")
        for (const m of content.matchAll(/http\.(Get|Post|Head)\(\s*["'`]([^"'`\s]+)["'`]/g)) {
            const ep = this.normalizeEndpointPath(m[2]);
            if (ep)
                seen.add(`${m[1].toUpperCase()} ${ep}`);
        }
        for (const m of content.matchAll(/http\.NewRequest\(\s*["'`]?(GET|POST|PUT|PATCH|DELETE|HEAD)["'`]?\s*,\s*["'`]([^"'`\s]+)["'`]/gi)) {
            const ep = this.normalizeEndpointPath(m[2]);
            if (ep)
                seen.add(`${m[1].toUpperCase()} ${ep}`);
        }
        // Ruby Net::HTTP / Faraday / HTTParty: get("/path"), post("/path")
        for (const m of content.matchAll(/(?:Net::HTTP|Faraday|HTTParty|RestClient)\.(get|post|put|patch|delete)\(\s*["']([^"'\s]+)["']/gi)) {
            const ep = this.normalizeEndpointPath(m[2]);
            if (ep)
                seen.add(`${m[1].toUpperCase()} ${ep}`);
        }
        // C# HttpClient: client.GetAsync("/path"), client.PostAsync("/path")
        for (const m of content.matchAll(/\.(Get|Post|Put|Patch|Delete)Async\(\s*["']([^"'\s]+)["']/g)) {
            const ep = this.normalizeEndpointPath(m[2]);
            if (ep)
                seen.add(`${m[1].toUpperCase()} ${ep}`);
        }
        // C# RestSharp: new RestRequest("/path", Method.Get)
        for (const m of content.matchAll(/RestRequest\(\s*["']([^"']+)["']\s*,\s*Method\.(Get|Post|Put|Patch|Delete)/gi)) {
            const ep = this.normalizeEndpointPath(m[1]);
            if (ep)
                seen.add(`${m[2].toUpperCase()} ${ep}`);
        }
        // PHP Guzzle/Laravel: $client->get("/path"), $this->getJson("/path"), $this->postJson("/path")
        for (const m of content.matchAll(/(?:->|\$this->)(get|post|put|patch|delete)(?:Json)?\(\s*["']([^"'\s]+)["']/gi)) {
            const ep = this.normalizeEndpointPath(m[2]);
            if (ep)
                seen.add(`${m[1].toUpperCase()} ${ep}`);
        }
        return [...seen].join(", ");
    }
    /**
     * Normalize an endpoint path extracted from test code.
     * Strips base URL prefixes and returns only the path portion.
     * Returns null if the path doesn't look like an API endpoint.
     */
    normalizeEndpointPath(raw) {
        let ep = raw;
        // Strip http(s)://host:port prefix
        try {
            const url = new URL(ep);
            ep = url.pathname;
        }
        catch {
            // Already a path, not a full URL — strip query string and fragment manually
            const qIdx = ep.indexOf("?");
            if (qIdx !== -1)
                ep = ep.substring(0, qIdx);
            const hIdx = ep.indexOf("#");
            if (hIdx !== -1)
                ep = ep.substring(0, hIdx);
        }
        // Must start with /
        if (!ep.startsWith("/"))
            return null;
        // Skip obviously non-API paths
        if (/^\/(favicon|static|assets|public|_next)\b/.test(ep))
            return null;
        return ep;
    }
    /**
     * Detect test type from external test file path and content.
     * Falls back to directory/filename heuristics since external tests lack Skyramp command lines.
     */
    detectExternalTestType(filePath, content) {
        const lowerPath = filePath.toLowerCase();
        if (/integration|_integration/.test(lowerPath))
            return "integration";
        if (/e2e|end[_-]to[_-]end/.test(lowerPath))
            return "e2e";
        if (/contract/.test(lowerPath))
            return "contract";
        if (/smoke/.test(lowerPath))
            return "smoke";
        if (/load|performance/.test(lowerPath))
            return "load";
        if (/[\\/]unit[\\/]|_unit/.test(lowerPath))
            return "unit";
        // Heuristic: files making HTTP calls are likely integration/API tests
        if (/(?:requests|fetch|axios|supertest|RestAssured)/i.test(content))
            return "integration";
        return "unknown";
    }
    /**
     * Detect test framework from external test file content by checking imports.
     */
    detectExternalFramework(content) {
        // Python
        if (/import pytest|from pytest/.test(content))
            return "pytest";
        if (/import unittest|from unittest/.test(content))
            return "unittest";
        // JavaScript/TypeScript
        if (/from ['"]vitest['"]|import.*vitest|require\(['"]vitest['"]\)|\bvi\./.test(content))
            return "vitest";
        if (/from ['"]@jest\/globals['"]|from ['"]jest['"]|import.*jest|require\(['"]jest['"]\)|jest\./.test(content) ||
            (/\b(?:test|it)\s*\(/.test(content) && /\bexpect\s*\(/.test(content))) {
            return "jest";
        }
        if (/from ['"]mocha['"]|require\(['"]mocha['"]\)/.test(content))
            return "mocha";
        if (/supertest/.test(content))
            return "supertest";
        if (/from ['"]@playwright/.test(content))
            return "playwright";
        if (/from ['"]cypress/.test(content))
            return "cypress";
        // Java / Kotlin — check TestNG before @Test since TestNG also uses @Test
        if (/import org\.testng/.test(content))
            return "testng";
        if (/@Test|import org\.junit/.test(content))
            return "junit";
        if (/RestAssured/.test(content))
            return "rest-assured";
        // Go
        if (/import.*"testing"|func\s+Test\w+\(t\s+\*testing\.T\)/.test(content))
            return "go-testing";
        if (/httptest\.NewServer|httptest\.NewRequest/.test(content))
            return "go-httptest";
        // Ruby
        if (/require ['"]rspec|RSpec\.describe/.test(content))
            return "rspec";
        if (/require ['"]minitest|Minitest::Test/.test(content))
            return "minitest";
        // C# / .NET
        if (/\[TestMethod\]/.test(content))
            return "mstest";
        if (/\[Fact\]|\[Theory\]/.test(content))
            return "xunit";
        if (/\[Test\]|NUnit/.test(content))
            return "nunit";
        // PHP
        if (/extends TestCase|PHPUnit/.test(content))
            return "phpunit";
        // Rust
        if (/\#\[test\]|#\[cfg\(test\)\]/.test(content))
            return "rust-test";
        // Swift
        if (/import XCTest|XCTestCase/.test(content))
            return "xctest";
        // Scala
        if (/import org\.scalatest|FunSuite|FlatSpec/.test(content))
            return "scalatest";
        return "";
    }
    /**
     * Detect programming language from file extension
     */
    detectLanguage(testFile) {
        const ext = path.extname(testFile);
        const languageMap = {
            ".py": "python",
            ".js": "javascript",
            ".ts": "typescript",
            ".tsx": "typescript",
            ".jsx": "javascript",
            ".java": "java",
            ".kt": "kotlin",
            ".kts": "kotlin",
            ".go": "go",
            ".rb": "ruby",
            ".cs": "csharp",
            ".rs": "rust",
            ".php": "php",
            ".scala": "scala",
            ".swift": "swift",
            ".m": "objective-c",
        };
        return languageMap[ext] || "unknown";
    }
    /**
     * Detect test type from file content and name
     * Checks Skyramp command line, filename, and content patterns
     */
    detectTestType(content) {
        const testTypes = [
            "smoke",
            "integration",
            "load",
            "contract",
            "fuzz",
            "e2e",
            "ui",
        ];
        // First, try to extract from Skyramp command line
        // Pattern: skyramp generate smoke rest ...
        const commandMatch = content.match(/skyramp generate (\w+)/i);
        if (commandMatch && commandMatch[1]) {
            const type = commandMatch[1].toLowerCase();
            if (testTypes.includes(type)) {
                return type;
            }
        }
        return "";
    }
    /**
     * Extract API schema path from test content
     * Looks for Skyramp command line first, then fallback to other patterns
     */
    extractApiSchema(content) {
        // First, try to extract from Skyramp command line
        // Pattern: --api-schema http://localhost:8000/openapi.json
        const apiSchemaMatch = content.match(/--api-schema\s+([^\s\\]+)/);
        if (apiSchemaMatch && apiSchemaMatch[1]) {
            return apiSchemaMatch[1];
        }
        return "";
    }
    /**
     * Extract Framework from test content
     * Looks for Skyramp command line first, then fallback to other patterns
     */
    extractFramework(content) {
        // First, try to extract from Skyramp command line
        // Pattern: --framework pytest
        const frameworkMatch = content.match(/--framework\s+([^\s\\]+)/);
        if (frameworkMatch && frameworkMatch[1]) {
            return frameworkMatch[1];
        }
        return "";
    }
}
