"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
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.StatementProjector = void 0;
const statement_trace_schema_1 = require("../../model/statement-trace-schema");
const error_serializer_1 = require("../serializers/error-serializer");
class StatementProjector {
    constructor(output) {
        this.output = output;
        // Every live statement, keyed by its enter-event seq. Used for
        // parent lookup on nested enters and for ok/throw stamping.
        this.allBySeq = new Map();
        // Subset: statements whose runtime parent was null. These are
        // the only ones that become their own NDJSON record.
        this.rootsBySeq = new Map();
        // Per-test, 0-based contiguous counter stamped onto every
        // Statement at enter-time as `index`. Reset by `clear()` so each
        // test starts at 0.
        this.statementCount = 0;
        this.headerEmitted = false;
        this.finalized = false;
    }
    write(event) {
        if (this.finalized)
            return;
        switch (event.type) {
            case 'meta': {
                if (!this.headerEmitted) {
                    this.output.write({
                        kind: 'test-header',
                        schemaVersion: statement_trace_schema_1.HEAL_TRACE_SCHEMA_VERSION,
                        test: buildTestHeader(event),
                    });
                    this.headerEmitted = true;
                }
                return;
            }
            case 'enter': {
                const live = {
                    stmt: createStatement(event, this.statementCount++),
                    enter: event,
                };
                this.allBySeq.set(event.seq, live);
                if (event.parentSeq == null) {
                    this.rootsBySeq.set(event.seq, live);
                }
                else {
                    const parent = this.allBySeq.get(event.parentSeq);
                    if (parent)
                        parent.stmt.children.push(live.stmt);
                    // Orphaned (parent was never seen) — promote to root so
                    // the statement is not lost.
                    else
                        this.rootsBySeq.set(event.seq, live);
                }
                return;
            }
            case 'ok': {
                const live = this.allBySeq.get(event.enterSeq);
                if (!live)
                    return;
                live.stmt.duration = event.duration;
                if (event.vars)
                    live.stmt.vars = event.vars;
                // Screenshot may have been stamped onto the enter event
                // after the enter was emitted (locator-screenshots
                // feature); pick it up now.
                if (live.enter.screenshot)
                    live.stmt.screenshot = live.enter.screenshot;
                this.maybeEmitRoot(event.enterSeq, live);
                return;
            }
            case 'throw': {
                if (event.enterSeq == null)
                    return;
                const live = this.allBySeq.get(event.enterSeq);
                if (!live)
                    return;
                live.stmt.status = 'threw';
                live.stmt.duration = event.duration;
                live.stmt.error = event.error;
                if (live.enter.screenshot)
                    live.stmt.screenshot = live.enter.screenshot;
                this.maybeEmitRoot(event.enterSeq, live);
                return;
            }
            default:
                return;
        }
    }
    // Called by `reset()` at the start of every test. Drops all
    // in-progress statement state so the next test begins with a
    // blank slate. Does NOT close the inner exporter — the fixture
    // owns that lifecycle.
    clear() {
        this.allBySeq.clear();
        this.rootsBySeq.clear();
        this.statementCount = 0;
        this.headerEmitted = false;
    }
    /**
     * Emit the final `test-result` record and close the underlying
     * exporter. Called once at test teardown; the projector is unusable
     * afterwards.
     *
     * Any root statements still live in `rootsBySeq` at this point had
     * their `__enter` fire but never their matching `__ok`/`__throw` —
     * typically because Playwright's test timeout short-circuited
     * teardown before the hanging action rejected. Flushing them as
     * `threw` with the test-level error makes the partial work visible
     * in the trace instead of silently dropped.
     */
    async finalize(result, pendingError) {
        if (this.finalized)
            return;
        this.flushPendingRoots(result.duration, pendingError);
        this.finalized = true;
        this.output.write({ kind: 'test-result', ...result });
        await this.output.close();
    }
    flushPendingRoots(testDuration, pendingError) {
        if (this.rootsBySeq.size === 0)
            return;
        const error = (0, error_serializer_1.serializeError)(pendingError ?? new Error('Statement still pending when test ended'));
        const pending = Array.from(this.rootsBySeq.entries()).sort(([a], [b]) => a - b);
        for (const [seq, live] of pending) {
            live.stmt.status = 'threw';
            live.stmt.duration = Math.max(0, testDuration - live.stmt.t);
            live.stmt.error = error;
            if (live.enter.screenshot)
                live.stmt.screenshot = live.enter.screenshot;
            sortChildrenDeep(live.stmt);
            this.output.write({ kind: 'statement', statement: live.stmt });
            this.dropSubtree(live.stmt);
            this.rootsBySeq.delete(seq);
        }
    }
    maybeEmitRoot(seq, live) {
        if (!this.rootsBySeq.has(seq))
            return;
        sortChildrenDeep(live.stmt);
        this.output.write({ kind: 'statement', statement: live.stmt });
        // Drop the subtree from the working maps — memory bound is
        // now the maximum in-flight depth rather than the whole test.
        this.dropSubtree(live.stmt);
        this.rootsBySeq.delete(seq);
    }
    dropSubtree(stmt) {
        this.allBySeq.delete(stmt.seq);
        for (const child of stmt.children)
            this.dropSubtree(child);
    }
}
exports.StatementProjector = StatementProjector;
function createStatement(event, index) {
    const stmt = {
        seq: event.seq,
        index,
        file: event.file,
        line: event.startLine,
        endLine: event.endLine,
        kind: event.kind,
        scope: event.scope,
        source: event.source,
        hasAwait: event.hasAwait,
        step: event.step,
        stepPath: event.stepPath,
        status: 'ok',
        duration: 0,
        t: event.t,
        pageUrl: event.pageUrl,
        children: [],
    };
    if (event.screenshot)
        stmt.screenshot = event.screenshot;
    if (event.leadingComment != null)
        stmt.leadingComment = event.leadingComment;
    return stmt;
}
function sortChildrenDeep(stmt) {
    if (stmt.children.length === 0)
        return;
    stmt.children.sort((a, b) => a.seq - b.seq);
    for (const child of stmt.children)
        sortChildrenDeep(child);
}
function buildTestHeader(meta) {
    const env = {
        nodeVersion: meta.nodeVersion,
        platform: meta.platform,
        arch: meta.arch,
        hostname: meta.hostname,
        isCI: meta.isCI,
        cwd: meta.cwd,
        gitSha: meta.gitSha,
        pid: meta.pid,
    };
    const context = {
        testId: meta.testId ?? '',
        attempt: meta.attempt ?? 1,
        ...(meta.testCaseId !== undefined ? { testCaseId: meta.testCaseId } : {}),
        ...(meta.executionId !== undefined ? { executionId: meta.executionId } : {}),
    };
    return {
        title: meta.testTitle ?? '',
        titlePath: meta.titlePath ?? [],
        file: meta.testFile ?? '',
        project: meta.projectName ?? '',
        workerIndex: meta.workerIndex ?? 0,
        retry: meta.retry ?? 0,
        startedAt: meta.wallTime ?? 0,
        env,
        context,
    };
}
