"use strict";
/**
 * Copyright: (c) Myia SAS 2026.
 * This file and its contents are licensed under the AGPLv3 License.
 * Please see the LICENSE file at the root of this repository
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.isSafeIdForRouting = exports.discoverExecutions = exports.buildIndex = exports.discoverTraces = exports.parseSummaryId = void 0;
const promises_1 = require("node:fs/promises");
const path = __importStar(require("node:path"));
const statement_trace_schema_1 = require("../../domain/trace-event-recorder/model/statement-trace-schema");
const heal_traces_layout_1 = require("../heal-traces-layout");
const isSafeId = (id) => id.length > 0 && !id.includes('..') && !id.includes('/') && !id.includes('\\');
const parseLine = (line) => {
    try {
        return JSON.parse(line);
    }
    catch {
        return null;
    }
};
const readHeader = async (ndjsonPath) => {
    const content = await (0, promises_1.readFile)(ndjsonPath, 'utf-8');
    const firstLine = content.split('\n').find((l) => l.trim().length > 0);
    if (!firstLine) {
        return null;
    }
    const parsed = parseLine(firstLine);
    if (!parsed || parsed.kind !== 'test-header') {
        return null;
    }
    return parsed.test;
};
/**
 * Read the tail of the NDJSON to extract the test-result and the
 * test-attachments record (which the reporter appends after
 * test-result). Both are optional. Reads the file once.
 */
const readTail = async (ndjsonPath) => {
    const content = await (0, promises_1.readFile)(ndjsonPath, 'utf-8');
    const lines = content.split('\n').filter((l) => l.trim().length > 0);
    let result = null;
    let attachments = [];
    for (let i = lines.length - 1; i >= 0 && (!result || attachments.length === 0); i -= 1) {
        const parsed = parseLine(lines[i]);
        if (!parsed)
            continue;
        if (!result && parsed.kind === 'test-result') {
            result = parsed;
        }
        else if (attachments.length === 0 && parsed.kind === 'test-attachments') {
            attachments = parsed.attachments;
        }
    }
    return { result, attachments };
};
const buildSummaryId = (playwrightTestId, attempt) => `${playwrightTestId}_${attempt}`;
/**
 * Decode the compound id back into (playwrightTestId, attempt). Splits
 * on the LAST underscore so test ids that themselves contain underscores
 * round-trip correctly. Returns null on invalid shapes.
 */
const parseSummaryId = (id) => {
    if (!isSafeId(id))
        return null;
    const lastUnderscore = id.lastIndexOf('_');
    if (lastUnderscore <= 0 || lastUnderscore >= id.length - 1)
        return null;
    const tid = id.slice(0, lastUnderscore);
    const attemptStr = id.slice(lastUnderscore + 1);
    if (!/^\d+$/.test(attemptStr))
        return null;
    const attempt = Number.parseInt(attemptStr, 10);
    if (attempt <= 0)
        return null;
    return { playwrightTestId: tid, attempt };
};
exports.parseSummaryId = parseSummaryId;
const safeReaddir = async (dir) => {
    try {
        return await (0, promises_1.readdir)(dir, { withFileTypes: true });
    }
    catch {
        return [];
    }
};
const findAttempts = async (executionDir) => {
    const out = [];
    const testEntries = await safeReaddir(executionDir);
    for (const testEntry of testEntries) {
        if (!testEntry.isDirectory())
            continue;
        const playwrightTestId = testEntry.name;
        const testDir = path.join(executionDir, playwrightTestId);
        const attemptEntries = await safeReaddir(testDir);
        for (const attemptEntry of attemptEntries) {
            if (!attemptEntry.isDirectory())
                continue;
            if (!/^\d+$/.test(attemptEntry.name))
                continue;
            const attempt = Number.parseInt(attemptEntry.name, 10);
            if (attempt <= 0)
                continue;
            const ndjsonPathAbs = path.join(testDir, attemptEntry.name, heal_traces_layout_1.HealTracesLayout.NDJSON_FILENAME);
            try {
                await (0, promises_1.stat)(ndjsonPathAbs);
            }
            catch {
                continue;
            }
            const ndjsonPathRel = path.relative(executionDir, ndjsonPathAbs);
            out.push({ playwrightTestId, attempt, ndjsonPathAbs, ndjsonPathRel });
        }
    }
    return out;
};
const summarize = async (found) => {
    const header = await readHeader(found.ndjsonPathAbs);
    if (!header) {
        return null;
    }
    const { result, attachments } = await readTail(found.ndjsonPathAbs);
    return {
        id: buildSummaryId(found.playwrightTestId, found.attempt),
        playwrightTestId: found.playwrightTestId,
        attempt: found.attempt,
        ndjsonPath: found.ndjsonPathRel,
        title: header.title,
        titlePath: header.titlePath,
        file: header.file,
        project: header.project,
        status: result?.status ?? 'unknown',
        duration: result?.duration ?? 0,
        startedAt: header.startedAt,
        attachments,
    };
};
const discoverTraces = async (rootDir, executionId) => {
    const layout = new heal_traces_layout_1.HealTracesLayout(rootDir, executionId);
    const executionDir = layout.executionDir();
    const found = await findAttempts(executionDir);
    const summaries = [];
    for (const f of found) {
        try {
            const s = await summarize(f);
            if (s)
                summaries.push(s);
        }
        catch (err) {
            console.warn(`[heal-tracer] skipped ${f.ndjsonPathRel}:`, err);
        }
    }
    summaries.sort((a, b) => {
        const byFile = a.file.localeCompare(b.file);
        if (byFile !== 0)
            return byFile;
        const byAttempt = a.attempt - b.attempt;
        if (byAttempt !== 0)
            return byAttempt;
        return a.title.localeCompare(b.title);
    });
    return summaries;
};
exports.discoverTraces = discoverTraces;
const buildIndex = (executionId, tests) => ({
    schemaVersion: statement_trace_schema_1.HEAL_TRACE_SCHEMA_VERSION,
    executionId,
    tests,
});
exports.buildIndex = buildIndex;
/**
 * List executions known under `<rootDir>/heal-traces/`. Tries to read
 * `executions.ndjson` first; if absent or partial, falls back to
 * scanning subdirectory names. The index file lists records in the
 * order runs were appended; we surface them newest-first to match
 * the typical viewer use ("show me the latest run").
 */
const discoverExecutions = async (rootDir) => {
    const tracesRoot = path.join(rootDir, heal_traces_layout_1.HealTracesLayout.DIRNAME);
    const indexed = await readExecutionsIndex(tracesRoot);
    const indexedIds = new Set(indexed.map((e) => e.executionId));
    // Backfill: any execution dir on disk that the index file doesn't
    // mention (interrupted runs that didn't reach onEnd, or runs from
    // before the reporter started writing the index).
    const dirEntries = await safeReaddir(tracesRoot);
    for (const entry of dirEntries) {
        if (!entry.isDirectory())
            continue;
        if (indexedIds.has(entry.name))
            continue;
        if (!isSafeId(entry.name))
            continue;
        const manifestPath = path.join(tracesRoot, entry.name, heal_traces_layout_1.HealTracesLayout.EXECUTION_MANIFEST);
        try {
            const body = await (0, promises_1.readFile)(manifestPath, 'utf-8');
            const manifest = JSON.parse(body);
            indexed.push({
                executionId: manifest.executionId,
                source: manifest.source,
                startedAt: manifest.startedAt,
                endedAt: manifest.endedAt,
                durationMs: manifest.durationMs,
                totals: manifest.totals,
                git: manifest.git,
                playwrightVersion: manifest.playwrightVersion,
            });
        }
        catch {
            indexed.push({ executionId: entry.name });
        }
    }
    indexed.sort((a, b) => (b.startedAt ?? 0) - (a.startedAt ?? 0));
    return indexed;
};
exports.discoverExecutions = discoverExecutions;
const readExecutionsIndex = async (tracesRoot) => {
    const indexPath = path.join(tracesRoot, heal_traces_layout_1.HealTracesLayout.EXECUTIONS_NDJSON);
    let body;
    try {
        body = await (0, promises_1.readFile)(indexPath, 'utf-8');
    }
    catch {
        return [];
    }
    const out = [];
    for (const line of body.split('\n')) {
        if (line.trim().length === 0)
            continue;
        const parsed = parseLine(line);
        if (!parsed || parsed.kind !== 'execution')
            continue;
        if (!isSafeId(parsed.executionId))
            continue;
        out.push({
            executionId: parsed.executionId,
            source: parsed.source,
            startedAt: parsed.startedAt,
            endedAt: parsed.endedAt,
            durationMs: parsed.durationMs,
            totals: parsed.totals,
            git: parsed.git,
            playwrightVersion: parsed.playwrightVersion,
        });
    }
    return out;
};
exports.isSafeIdForRouting = isSafeId;
