"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ARTIFACT_TYPES = void 0;
exports.assertTestRunId = assertTestRunId;
exports.getStepRunId = getStepRunId;
exports.ensureDebugDir = ensureDebugDir;
exports.getCachedPath = getCachedPath;
exports.fetchUserLog = fetchUserLog;
exports.collapseStepEntries = collapseStepEntries;
exports.getStepEntries = getStepEntries;
exports.buildStepUrlMap = buildStepUrlMap;
exports.resolveStepArtifact = resolveStepArtifact;
exports.tagToRole = tagToRole;
exports.getTestRunOrThrow = getTestRunOrThrow;
exports.loadStepContext = loadStepContext;
exports.downloadOrUseCache = downloadOrUseCache;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const axios_1 = __importDefault(require("axios"));
const mablApiClientFactory_1 = require("@mablhq-internal/runtime/api/mablApiClientFactory");
function assertTestRunId(id) {
    if (!id.endsWith('-jr')) {
        throw new Error(`Expected a test run ID, e.g. abc123-jr. Got: ${id}`);
    }
}
function getStepRunId(entry) {
    if (entry.step_run_id) {
        return entry.step_run_id;
    }
    if (entry.flow_index === undefined || entry.step_index === undefined) {
        return undefined;
    }
    return `${entry.flow_index}:${entry.step_index}`;
}
function ensureDebugDir(runId) {
    const cwd = process.cwd();
    const expectedBase = path_1.default.resolve(path_1.default.join(cwd, '.mabl', 'debug'));
    const debugDir = path_1.default.resolve(path_1.default.join(cwd, '.mabl', 'debug', runId));
    if (!debugDir.startsWith(expectedBase + path_1.default.sep)) {
        throw new Error(`Invalid run ID: ${runId}`);
    }
    fs_1.default.mkdirSync(debugDir, { recursive: true });
    const gitignorePath = path_1.default.join(cwd, '.gitignore');
    const mablEntry = '.mabl/';
    if (fs_1.default.existsSync(gitignorePath)) {
        const content = fs_1.default.readFileSync(gitignorePath, 'utf-8');
        const lines = content.split('\n').map((line) => line.trim());
        if (!lines.includes(mablEntry)) {
            const separator = content.endsWith('\n') ? '' : '\n';
            fs_1.default.appendFileSync(gitignorePath, `${separator}${mablEntry}\n`);
        }
    }
    else {
        fs_1.default.writeFileSync(gitignorePath, `${mablEntry}\n`);
    }
    return debugDir;
}
function normalizeArtifactUri(uri) {
    const decodedUri = decodeURIComponent(uri);
    if (decodedUri.includes('?path=/')) {
        const bucketFragment = decodedUri.split('?path=/')?.[1];
        return `storage.googleapis.com/${bucketFragment}`;
    }
    const bucketRegex = /storage.googleapis.com\/([^/]+)\/(.*)$/;
    const parts = decodedUri.match(bucketRegex);
    if (!parts || parts.length < 3) {
        throw new Error(`Cannot decompose GCS URI [${decodedUri}] to bucket/key`);
    }
    return parts[0];
}
function getCachedPath(runId, filename) {
    const filePath = path_1.default.join(process.cwd(), '.mabl', 'debug', runId, filename);
    return fs_1.default.existsSync(filePath) ? filePath : undefined;
}
async function downloadArtifact(apiClient, artifactUrl, outputPath) {
    const normalizedPath = normalizeArtifactUri(artifactUrl);
    const response = await apiClient.getArtifactUrl(normalizedPath);
    if (!response.redirectUrl) {
        return undefined;
    }
    const dir = path_1.default.dirname(outputPath);
    fs_1.default.mkdirSync(dir, { recursive: true });
    let streamResponse;
    try {
        streamResponse = await axios_1.default.get(response.redirectUrl, {
            responseType: 'stream',
        });
    }
    catch (error) {
        if (isHttpNotFound(error)) {
            return undefined;
        }
        throw error;
    }
    const writer = fs_1.default.createWriteStream(outputPath);
    streamResponse.data.pipe(writer);
    return new Promise((resolve, reject) => {
        writer.on('finish', () => resolve(outputPath));
        writer.on('error', reject);
    });
}
function isHttpNotFound(error) {
    if (axios_1.default.isAxiosError(error)) {
        return error.response?.status === 404;
    }
    return false;
}
async function downloadArtifactAsText(apiClient, artifactUrl) {
    const normalizedPath = normalizeArtifactUri(artifactUrl);
    const response = await apiClient.getArtifactUrl(normalizedPath);
    if (!response.redirectUrl) {
        return undefined;
    }
    try {
        const textResponse = await axios_1.default.get(response.redirectUrl, {
            responseType: 'text',
        });
        return textResponse.data;
    }
    catch (error) {
        if (isHttpNotFound(error)) {
            return undefined;
        }
        throw error;
    }
}
async function fetchUserLog(apiClient, testRun) {
    const logUri = testRun.customer_log_uri;
    if (!logUri) {
        return [];
    }
    const content = await downloadArtifactAsText(apiClient, logUri);
    if (!content) {
        return [];
    }
    const allEntries = [];
    for (const line of content.split('\n')) {
        if (!line.trim()) {
            continue;
        }
        try {
            allEntries.push(JSON.parse(line));
        }
        catch {
        }
    }
    return allEntries;
}
const FAILURE_PREFIX = '[ERROR] Test failed:';
const RECOVERY_RESUME_FRAGMENT = 'Runtime recovery: resumed execution';
const SKIP_PREFIX = 'Skipping conditionally inactive step';
const CAPTURE_APPLICATION_STATE_ACTION = 'capture_application_state';
const SYNTHETIC_STEP_INDEX = -1;
function collapseStepEntries(userLogs) {
    const groups = new Map();
    for (const entry of userLogs) {
        if (entry.event_type !== 'END_ACTION') {
            continue;
        }
        if (entry.action_code === CAPTURE_APPLICATION_STATE_ACTION) {
            continue;
        }
        if (entry.step_index <= SYNTHETIC_STEP_INDEX) {
            continue;
        }
        const stepRunId = getStepRunId(entry);
        if (!stepRunId) {
            continue;
        }
        const list = groups.get(stepRunId) ?? [];
        list.push(entry);
        groups.set(stepRunId, list);
    }
    const collapsed = [];
    for (const [stepRunId, entries] of groups) {
        const recoveryEntry = entries.find((e) => (e.message ?? '').includes(RECOVERY_RESUME_FRAGMENT));
        const failureEntry = entries.find((e) => (e.message ?? '').startsWith(FAILURE_PREFIX));
        const skippedEntry = entries.find((e) => e.skipped === true || (e.message ?? '').startsWith(SKIP_PREFIX));
        const last = entries[entries.length - 1];
        let status;
        let canonical;
        let recoverySessionId;
        if (recoveryEntry) {
            status = 'recovered';
            canonical = failureEntry ?? recoveryEntry;
            recoverySessionId = recoveryEntry.agent_session_id;
        }
        else if (failureEntry) {
            status = 'failed';
            canonical = failureEntry;
        }
        else if (skippedEntry) {
            status = 'skipped';
            canonical = skippedEntry;
        }
        else {
            status = 'passed';
            canonical = last;
        }
        collapsed.push({
            step_run_id: stepRunId,
            status,
            canonical,
            artifactSource: last,
            recoverySessionId,
        });
    }
    return collapsed.sort((a, b) => {
        if (a.canonical.flow_index !== b.canonical.flow_index) {
            return a.canonical.flow_index - b.canonical.flow_index;
        }
        return a.canonical.step_index - b.canonical.step_index;
    });
}
function getStepEntries(userLogs) {
    return collapseStepEntries(userLogs).map((c) => c.canonical);
}
function buildStepUrlMap(userLogs) {
    const currentUrlMap = new Map();
    const visitTargetMap = new Map();
    const isVisitStep = new Set();
    for (const entry of userLogs) {
        if (entry.event_type !== 'START_ACTION') {
            continue;
        }
        const stepRunId = getStepRunId(entry);
        if (!stepRunId) {
            continue;
        }
        const key = stepRunId;
        const currentUrlPrefix = 'Current URL: ';
        if (entry.message.startsWith(currentUrlPrefix)) {
            if (!currentUrlMap.has(key)) {
                currentUrlMap.set(key, entry.message.substring(currentUrlPrefix.length));
            }
        }
        if (entry.action_code.includes('visit_url')) {
            isVisitStep.add(key);
            const navigatePrefix = 'Navigate to URL: ';
            if (entry.message.startsWith(navigatePrefix)) {
                visitTargetMap.set(key, entry.message.substring(navigatePrefix.length));
            }
        }
    }
    const allKeys = new Set([...currentUrlMap.keys(), ...isVisitStep]);
    const sortedKeys = [];
    for (const entry of userLogs) {
        const stepRunId = getStepRunId(entry);
        if (!stepRunId || !allKeys.has(stepRunId)) {
            continue;
        }
        if (!sortedKeys.includes(stepRunId)) {
            sortedKeys.push(stepRunId);
        }
    }
    const result = new Map();
    for (let i = 0; i < sortedKeys.length; i++) {
        const key = sortedKeys[i];
        const urlBefore = currentUrlMap.get(key);
        if (!urlBefore) {
            continue;
        }
        let urlAfter = urlBefore;
        if (isVisitStep.has(key)) {
            const navigateTarget = visitTargetMap.get(key);
            if (navigateTarget) {
                urlAfter = navigateTarget;
            }
            else {
                const nextKey = i + 1 < sortedKeys.length ? sortedKeys[i + 1] : undefined;
                const nextUrlBefore = nextKey ? currentUrlMap.get(nextKey) : undefined;
                if (nextUrlBefore) {
                    urlAfter = nextUrlBefore;
                }
            }
        }
        result.set(key, { url_before: urlBefore, url_after: urlAfter });
    }
    return result;
}
exports.ARTIFACT_TYPES = {
    Screenshot: 'screenshot',
    ConsoleLog: 'console_log',
    DomSnapshot: 'dom_snapshot',
    Har: 'har',
};
function resolveStepArtifact(userLogs, stepRunIdOrNumber, artifactType) {
    const stepEntries = getStepEntries(userLogs);
    const entry = typeof stepRunIdOrNumber === 'number'
        ? stepRunIdOrNumber >= 1 && stepRunIdOrNumber <= stepEntries.length
            ? stepEntries[stepRunIdOrNumber - 1]
            : undefined
        : stepEntries.find((stepEntry) => getStepRunId(stepEntry) === stepRunIdOrNumber);
    if (!entry) {
        return undefined;
    }
    switch (artifactType) {
        case 'screenshot':
            return entry.image_url;
        case 'console_log':
            return entry.console_log_url;
        case 'dom_snapshot':
            return entry.dom_snapshot_url;
        case 'har':
            return entry.har_url;
        default:
            return undefined;
    }
}
const IDENTITY_ROLES = new Set([
    'button',
    'form',
    'img',
    'main',
    'navigation',
    'table',
]);
const TAG_TO_ROLE = new Map([
    ['a', 'link'],
    ['nav', 'navigation'],
    ['header', 'banner'],
    ['footer', 'contentinfo'],
    ['aside', 'complementary'],
    ['ul', 'list'],
    ['ol', 'list'],
    ['li', 'listitem'],
    ['select', 'combobox'],
    ['input', 'textbox'],
    ['textarea', 'textbox'],
    ['h1', 'heading'],
    ['h2', 'heading'],
    ['h3', 'heading'],
    ['h4', 'heading'],
    ['h5', 'heading'],
    ['h6', 'heading'],
]);
function tagToRole(tag) {
    const lower = tag.toLowerCase();
    if (IDENTITY_ROLES.has(lower)) {
        return lower;
    }
    return TAG_TO_ROLE.get(lower);
}
async function getTestRunOrThrow(apiClient, testRunId) {
    try {
        return await apiClient.getTestRun(testRunId);
    }
    catch (error) {
        const status = error.statusCode;
        if (status === 401 || status === 403) {
            throw new Error(`Access denied for test run ${testRunId}`);
        }
        throw new Error(`Test run ${testRunId} not found`);
    }
}
async function loadStepContext(apiKey, testRunId, stepRunId) {
    const apiClient = await mablApiClientFactory_1.MablApiClientFactory.createApiClientFromOptionalApiKey(apiKey);
    const testRun = await getTestRunOrThrow(apiClient, testRunId);
    const userLogs = await fetchUserLog(apiClient, testRun);
    if (!getStepEntries(userLogs).some((e) => getStepRunId(e) === stepRunId)) {
        throw new Error(`Step run not found: ${stepRunId}`);
    }
    return { apiClient, testRun, userLogs };
}
async function downloadOrUseCache(apiClient, testRunId, url, cacheFilename) {
    const cached = getCachedPath(testRunId, cacheFilename);
    if (cached) {
        return cached;
    }
    const debugDir = ensureDebugDir(testRunId);
    return downloadArtifact(apiClient, url, `${debugDir}/${cacheFilename}`);
}
