/**
 * PR Comment Parser — extracts TestBot history from GitHub PR comments.
 *
 * Parses previous Skyramp TestBot comments on a PR to build context
 * about what has already been recommended, implemented, and tested.
 * This prevents re-recommendations and enables contextual awareness
 * across the PR lifecycle.
 */
import { execFileSync } from "child_process";
import { logger } from "./logger.js";
const TESTBOT_MARKERS = [
    "Generated by Skyramp",
    "Skyramp Testbot",
    "skyramp-testbot",
];
const TEST_TYPE_PATTERN = /\b(Smoke|Contract|Integration|Fuzz|Load|E2E|UI)\b/gi;
const TEST_FILE_PATTERN = /[\w/.-]+(?:_test|_smoke|_contract|_fuzz|_integration|_load|_e2e|_ui)\.\w+/gi;
const STATUS_PATTERN = /\b(Pass|Fail|Skipped)\b/gi;
function isTestBotComment(comment) {
    const hasMarker = TESTBOT_MARKERS.some((marker) => comment.body.includes(marker));
    if (!hasMarker)
        return false;
    // Require the comment author to be a bot account (suffix [bot]) to avoid
    // false positives from human reviewers mentioning the bot by name.
    const login = comment.user?.login ?? "";
    return login.endsWith("[bot]") || login === "";
}
/**
 * Section markers used by the bot's rendered report (see testbot/src/report.ts).
 * We split on these to classify entries as implemented vs. recommended.
 */
const SECTION_MARKERS = {
    newTests: /New Tests Created/i,
    additionalRecs: /Additional Recommendations/i,
    testMaintenance: /Test Maintenance/i,
    testResults: /Test Results/i,
    issuesFound: /Issues Found/i,
};
/**
 * Matches scenario names rendered as **`scenario-name`** in the Additional
 * Recommendations section of the bot comment.
 */
const SCENARIO_NAME_PATTERN = /\*\*`([^`]+)`\*\*/g;
/**
 * Matches the multi-method endpoint format the report uses:
 * "**Integration** for POST/GET/PUT/DELETE /items/ — `test_file.py`"
 * Captures the methods block and the path separately.
 */
const MULTI_METHOD_ENDPOINT_PATTERN = /\b((?:GET|POST|PUT|PATCH|DELETE)(?:\/(?:GET|POST|PUT|PATCH|DELETE))*)\s+(\/[^\s`*\])>]+)/gi;
/**
 * Split a comment body into named sections based on the rendered report headings.
 */
function splitIntoSections(body) {
    const lines = body.split("\n");
    let currentSection = "rest";
    const sections = { newTests: [], additionalRecs: [], rest: [] };
    for (const line of lines) {
        if (SECTION_MARKERS.newTests.test(line)) {
            currentSection = "newTests";
            continue;
        }
        if (SECTION_MARKERS.additionalRecs.test(line)) {
            currentSection = "additionalRecs";
            continue;
        }
        if (SECTION_MARKERS.testMaintenance.test(line) ||
            SECTION_MARKERS.testResults.test(line) ||
            SECTION_MARKERS.issuesFound.test(line)) {
            currentSection = "rest";
            continue;
        }
        sections[currentSection].push(line);
    }
    return {
        newTests: sections.newTests.join("\n"),
        additionalRecs: sections.additionalRecs.join("\n"),
        rest: sections.rest.join("\n"),
    };
}
/**
 * Parse endpoints from a text block, handling the multi-method format
 * (e.g. "POST/GET/PUT/DELETE /items/") by expanding into individual entries.
 */
function parseEndpointsFromSection(text) {
    const results = [];
    for (const m of text.matchAll(MULTI_METHOD_ENDPOINT_PATTERN)) {
        const methods = m[1].toUpperCase().split("/");
        const path = m[2];
        for (const method of methods) {
            results.push({ method, path, index: m.index });
        }
    }
    return results;
}
/**
 * Find the regex match closest to `targetIndex` with distance strictly less
 * than `maxDist` characters. Unlike Array.find (which returns the first
 * match in array order), this returns the match with the smallest absolute
 * distance.
 */
function closestMatch(matches, targetIndex, maxDist) {
    let best = null;
    let bestDist = maxDist;
    for (const m of matches) {
        const dist = Math.abs(m.index - targetIndex);
        if (dist < bestDist) {
            best = m;
            bestDist = dist;
        }
    }
    return best;
}
function extractRecommendations(comment) {
    const results = [];
    const commentId = String(comment.id);
    const { newTests, additionalRecs } = splitIntoSections(comment.body);
    // --- Implemented tests (from "New Tests Created" section) ---
    const implEndpoints = parseEndpointsFromSection(newTests);
    const implTypeMatches = [...newTests.matchAll(TEST_TYPE_PATTERN)];
    for (const ep of implEndpoints) {
        const nearbyType = closestMatch(implTypeMatches, ep.index, 200);
        results.push({
            testType: nearbyType ? nearbyType[1].toLowerCase() : "unknown",
            endpoint: `${ep.method} ${ep.path}`,
            status: "implemented",
            commentId,
        });
    }
    // --- Recommended-only tests (from "Additional Recommendations" section) ---
    const recEndpoints = parseEndpointsFromSection(additionalRecs);
    const recTypeMatches = [...additionalRecs.matchAll(TEST_TYPE_PATTERN)];
    const scenarioNames = [...additionalRecs.matchAll(SCENARIO_NAME_PATTERN)];
    for (const ep of recEndpoints) {
        const nearbyType = closestMatch(recTypeMatches, ep.index, 200);
        const nearbyScenario = closestMatch(scenarioNames, ep.index, 400);
        results.push({
            testType: nearbyType ? nearbyType[1].toLowerCase() : "unknown",
            endpoint: `${ep.method} ${ep.path}`,
            scenarioName: nearbyScenario ? nearbyScenario[1] : undefined,
            status: "recommended",
            commentId,
        });
    }
    return results;
}
/**
 * Extract the "Test Results" section text for isolated execution result parsing.
 */
function extractTestResultsSection(fullBody) {
    const lines = fullBody.split("\n");
    let inTestResults = false;
    const resultLines = [];
    for (const line of lines) {
        if (SECTION_MARKERS.testResults.test(line)) {
            inTestResults = true;
            continue;
        }
        if (inTestResults && (SECTION_MARKERS.testMaintenance.test(line) ||
            SECTION_MARKERS.newTests.test(line) ||
            SECTION_MARKERS.additionalRecs.test(line) ||
            SECTION_MARKERS.issuesFound.test(line))) {
            break;
        }
        if (inTestResults) {
            resultLines.push(line);
        }
    }
    return resultLines.join("\n");
}
function extractExecutionResults(body) {
    const testResultsText = extractTestResultsSection(body);
    const results = [];
    const fileMatches = [...testResultsText.matchAll(TEST_FILE_PATTERN)];
    const statusMatches = [...testResultsText.matchAll(STATUS_PATTERN)];
    for (let i = 0; i < fileMatches.length; i++) {
        const file = fileMatches[i][0];
        const nearbyStatus = closestMatch(statusMatches, fileMatches[i].index, 150);
        const statusStr = nearbyStatus?.[1].toLowerCase();
        const status = statusStr === "pass" ? "pass" : statusStr === "skipped" ? "skipped" : "fail";
        results.push({
            testFile: file,
            status,
            timestamp: new Date().toISOString(),
        });
    }
    return results;
}
function extractImplementedFiles(body) {
    const { newTests } = splitIntoSections(body);
    const matches = [...newTests.matchAll(TEST_FILE_PATTERN)];
    return [...new Set(matches.map((m) => m[0]))];
}
/**
 * Fetch and parse PR comments to build TestBot history context.
 *
 * Uses the `gh` CLI (available in CI and local dev environments with
 * GitHub CLI installed). Falls back gracefully if `gh` is unavailable.
 */
export async function parsePRComments(repoOwner, repoName, prNumber, _token) {
    const empty = {
        prNumber,
        previousRecommendations: [],
        implementedTestFiles: [],
        executionResults: [],
    };
    let rawComments;
    try {
        rawComments = execFileSync("gh", ["api", `repos/${repoOwner}/${repoName}/issues/${prNumber}/comments`, "--paginate"], {
            encoding: "utf-8",
            timeout: 15_000,
            stdio: ["pipe", "pipe", "pipe"],
        });
    }
    catch (err) {
        logger.warning("Failed to fetch PR comments via gh CLI — skipping PR context", {
            error: err instanceof Error ? err.message : String(err),
        });
        return empty;
    }
    let comments;
    try {
        comments = JSON.parse(rawComments);
        if (!Array.isArray(comments)) {
            return empty;
        }
    }
    catch {
        logger.warning("Failed to parse PR comments JSON");
        return empty;
    }
    const testBotComments = comments.filter((c) => isTestBotComment(c));
    if (testBotComments.length === 0) {
        return empty;
    }
    // Use only the latest TestBot comment — older comments represent stale
    // state from previous bot runs. The latest comment has the most accurate
    // picture of what was recommended, generated, and executed.
    const latestComment = testBotComments[testBotComments.length - 1];
    return {
        prNumber,
        previousRecommendations: extractRecommendations(latestComment),
        implementedTestFiles: extractImplementedFiles(latestComment.body),
        executionResults: extractExecutionResults(latestComment.body),
    };
}
