"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.NetworkCaptureSession = void 0;
// NetworkCaptureSession — one test's worth of HTTP traffic capture.
//
// Subscribes to `request` / `response` / `requestfinished` /
// `requestfailed` on every wired BrowserContext (and on the
// runner-side `APIRequestContext` if any) and coalesces each
// exchange into a single `NetworkRecord` written to the per-test
// `heal-network.ndjson` sidecar.
//
// Lifecycle of a successful exchange:
//   1. `request` fires →
//      - `urlFilter` decides whether to drop;
//      - `coalescer.begin(seed)` registers the in-flight record with
//        the issue-time correlation snapshot (t, statementSeq, ...);
//   2. `response` fires →
//      - `coalescer.update` patches status/headers/body-preview;
//   3. `requestfinished` fires →
//      - the session optionally awaits `response.body()` per
//        bodyMode policy, then calls `coalescer.finalize` to pop the
//        record and writes it (or buffers bodies for `'on-error'`).
//
// On `requestfailed`, the `failure` field is populated and the
// record is flushed immediately — there will be no `response` event.
//
// `attachToContext` and `attachToApiRequestContext` are idempotent
// (WeakSet); the session can be wired to many contexts created
// during a test without worrying about double-listening.
const fs = __importStar(require("fs"));
const network_coalescer_1 = require("./network-coalescer");
const redaction_1 = require("./redaction");
const body_preview_1 = require("./body-preview");
const with_timeout_1 = require("../../util/with-timeout");
const DEFAULT_MAX_BODY_BYTES = 8192;
const BODY_READ_TIMEOUT_MS = 2000;
/**
 * Public HTTP methods on `APIRequestContext` that we patch to capture
 * runner-side traffic. `fetch` is the unified entry point; the others
 * are convenience methods that may or may not delegate to it depending
 * on Playwright's version, so we patch all of them.
 */
const API_METHODS = Object.freeze([
    'fetch',
    'get',
    'post',
    'put',
    'patch',
    'delete',
    'head',
]);
class NetworkCaptureSession {
    constructor(ndjsonPath, deps, config) {
        this.ndjsonPath = ndjsonPath;
        this.deps = deps;
        this.closed = false;
        this.contexts = new WeakSet();
        this.apiContexts = new WeakSet();
        this.apiPatches = [];
        this.apiCounter = 1;
        this.coalescer = new network_coalescer_1.NetworkCoalescer();
        this.bufferedBodies = new Map();
        this.bodyMode = config.bodyMode ?? 'never';
        this.maxBodyBytes = config.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
        this.extraDenylist = config.redactHeaders;
        this.contentTypeAllowlist = config.contentTypeAllowlist;
        this.urlFilter = config.urlFilter;
    }
    attachToContext(ctx) {
        if (this.closed || this.contexts.has(ctx))
            return;
        this.contexts.add(ctx);
        ctx.on('request', (req) => this.handleRequest(req, 'browser-context'));
        ctx.on('response', (resp) => this.handleResponse(resp));
        ctx.on('requestfinished', (req) => {
            void this.handleRequestFinished(req);
        });
        ctx.on('requestfailed', (req) => this.handleRequestFailed(req));
    }
    attachToApiRequestContext(api) {
        if (this.closed || this.apiContexts.has(api))
            return;
        this.apiContexts.add(api);
        // `APIRequestContext` does NOT expose public `request`/`response`
        // events the way BrowserContext does — the event surface is
        // browser-side only. To capture runner-side `request.get(...)`
        // traffic we patch the public HTTP methods on the instance,
        // synthesize a `NetworkRecord` per call, and write it directly.
        // No coalescer / redirect tracking — the Playwright fetch
        // internally follows redirects and only surfaces the final
        // `APIResponse`, so a single record per call is correct.
        //
        // Patches are reverted in `stop()` (api contexts are usually
        // worker-scoped, so a stale patch would leak into the next test
        // in the same worker).
        const patchTarget = api;
        for (const method of API_METHODS) {
            const original = patchTarget[method];
            if (typeof original !== 'function')
                continue;
            const originalFn = original;
            this.apiPatches.push({ target: patchTarget, method, original: originalFn });
            patchTarget[method] = async (...args) => {
                if (this.closed)
                    return originalFn.apply(api, args);
                const record = this.beginApiRecord(method, args);
                try {
                    const response = await originalFn.apply(api, args);
                    this.completeApiRecord(record, response);
                    return response;
                }
                catch (err) {
                    this.failApiRecord(record, err);
                    throw err;
                }
            };
        }
    }
    beginApiRecord(method, args) {
        const [first, second] = args;
        const options = (typeof second === 'object' && second !== null ? second : {}) ??
            {};
        const optionsHeaders = (options.headers && typeof options.headers === 'object'
            ? options.headers
            : {}) ?? {};
        const optionsMethod = typeof options.method === 'string' ? options.method.toUpperCase() : undefined;
        let url;
        if (typeof first === 'string') {
            url = first;
        }
        else if (first && typeof first.url === 'function') {
            url = first.url();
        }
        else {
            url = '';
        }
        const httpMethod = optionsMethod ?? (method === 'fetch' ? 'GET' : method.toUpperCase());
        const requestId = `api-${this.apiCounter++}`;
        const requestHeaders = (0, redaction_1.redactHeaders)(optionsHeaders, this.extraDenylist);
        return {
            kind: 'network',
            t: this.deps.clock.now() - this.deps.startedAt,
            requestId,
            source: 'api-request-context',
            method: httpMethod,
            url,
            requestHeaders,
            ...this.snapshotApiCorrelation(),
        };
    }
    completeApiRecord(record, response) {
        if (response && typeof response === 'object') {
            const r = response;
            try {
                if (typeof r.status === 'function')
                    record.status = r.status();
            }
            catch {
                /* ignore */
            }
            try {
                if (typeof r.statusText === 'function')
                    record.statusText = r.statusText();
            }
            catch {
                /* ignore */
            }
            try {
                if (typeof r.headers === 'function') {
                    record.responseHeaders = (0, redaction_1.redactHeaders)(r.headers(), this.extraDenylist);
                }
            }
            catch {
                /* ignore */
            }
        }
        record.duration = Math.max(0, this.deps.clock.now() - this.deps.startedAt - record.t);
        this.write(record);
    }
    failApiRecord(record, err) {
        record.failure = {
            errorText: err instanceof Error ? err.message : String(err),
        };
        record.duration = Math.max(0, this.deps.clock.now() - this.deps.startedAt - record.t);
        this.write(record);
    }
    snapshotApiCorrelation() {
        const out = {};
        const seq = this.deps.getCurrentStatementSeq();
        if (seq !== undefined)
            out.statementSeq = seq;
        const stepPath = this.deps.getCurrentStepPath();
        if (stepPath)
            out.stepPath = stepPath;
        return out;
    }
    handleRequest(request, source) {
        if (this.closed)
            return;
        const url = safeUrl(request);
        if (this.urlFilter) {
            try {
                if (!this.urlFilter(url, request))
                    return;
            }
            catch {
                // user filter threw — keep the request rather than drop it.
            }
        }
        const requestId = this.coalescer.idFor(request);
        const priorId = redirectedFromIdFor(request, this.coalescer);
        const requestHeaders = (0, redaction_1.redactHeaders)(safeHeaders(request), this.extraDenylist);
        const seed = {
            kind: 'network',
            t: this.deps.clock.now() - this.deps.startedAt,
            requestId,
            source,
            method: safeMethod(request),
            url,
            ...(safeResourceType(request) ? { resourceType: safeResourceType(request) } : {}),
            isNavigationRequest: safeIsNavigationRequest(request),
            ...(priorId ? { redirectedFromId: priorId } : {}),
            requestHeaders,
            ...this.snapshotCorrelation(request),
            __request: request,
        };
        this.coalescer.begin(seed);
        if (this.bodyMode !== 'never') {
            const buf = safePostDataBuffer(request);
            if (buf) {
                const ct = headerLookup(seed.requestHeaders, 'content-type');
                const decode = (0, body_preview_1.isTextualContentType)(ct, this.contentTypeAllowlist);
                const preview = (0, body_preview_1.previewBody)(buf, this.maxBodyBytes, decode);
                if (this.bodyMode === 'always') {
                    seed.requestBody = preview;
                }
                else {
                    // 'on-error' — buffer, don't write.
                    this.bufferedBodies.set(requestId, {
                        ...(this.bufferedBodies.get(requestId) ?? {}),
                        request: preview,
                    });
                }
            }
            else {
                const len = parseContentLength(headerLookup(safeHeaders(request), 'content-length'));
                const bytesOnly = (0, body_preview_1.bytesOnlyPreview)(len);
                if (bytesOnly && this.bodyMode === 'always')
                    seed.requestBody = bytesOnly;
            }
        }
    }
    handleResponse(response) {
        if (this.closed)
            return;
        const request = response.request();
        if (!this.coalescer.has(request))
            return;
        const requestId = this.coalescer.idFor(request);
        const responseHeaders = (0, redaction_1.redactHeaders)(safeResponseHeaders(response), this.extraDenylist);
        const update = {
            status: response.status(),
            statusText: response.statusText(),
            responseHeaders,
            fromServiceWorker: safeFromServiceWorker(response),
        };
        const security = safeSecurityDetails(response);
        if (security)
            update.securityDetails = security;
        const server = safeServerAddr(response);
        if (server)
            update.serverAddr = server;
        this.coalescer.update(requestId, update);
    }
    async handleRequestFinished(request) {
        if (this.closed)
            return;
        if (!this.coalescer.has(request))
            return;
        const requestId = this.coalescer.idFor(request);
        const response = await safeResponse(request);
        if (response && this.bodyMode !== 'never') {
            const headers = safeResponseHeaders(response);
            const ct = headerLookup(headers, 'content-type');
            const len = parseContentLength(headerLookup(headers, 'content-length'));
            const wantText = (0, body_preview_1.isTextualContentType)(ct, this.contentTypeAllowlist);
            // Skip buffering huge payloads even before reading.
            const willBuffer = wantText && (len === undefined || len <= this.maxBodyBytes * 4);
            if (willBuffer) {
                try {
                    const buf = await (0, with_timeout_1.withTimeout)(response.body(), BODY_READ_TIMEOUT_MS, 'response.body');
                    const preview = (0, body_preview_1.previewBody)(buf, this.maxBodyBytes, true);
                    if (this.bodyMode === 'always') {
                        this.coalescer.update(requestId, { responseBody: preview });
                    }
                    else {
                        this.bufferedBodies.set(requestId, {
                            ...(this.bufferedBodies.get(requestId) ?? {}),
                            response: preview,
                        });
                    }
                }
                catch {
                    // body unavailable (interrupted, redirect, …) — record bytes-only.
                    const fallback = (0, body_preview_1.bytesOnlyPreview)(len);
                    if (fallback && this.bodyMode === 'always') {
                        this.coalescer.update(requestId, { responseBody: fallback });
                    }
                }
            }
            else {
                const fallback = (0, body_preview_1.bytesOnlyPreview)(len);
                if (fallback && this.bodyMode === 'always') {
                    this.coalescer.update(requestId, { responseBody: fallback });
                }
            }
        }
        const timing = safeTiming(request);
        if (timing)
            this.coalescer.update(requestId, { timing });
        const finishedAt = this.deps.clock.now() - this.deps.startedAt;
        const record = this.coalescer.finalize(requestId);
        if (!record)
            return;
        record.duration = Math.max(0, finishedAt - record.t);
        this.write(record);
    }
    handleRequestFailed(request) {
        if (this.closed)
            return;
        if (!this.coalescer.has(request))
            return;
        const requestId = this.coalescer.idFor(request);
        const failureText = safeFailure(request);
        if (failureText) {
            this.coalescer.update(requestId, { failure: { errorText: failureText } });
        }
        const failedAt = this.deps.clock.now() - this.deps.startedAt;
        const record = this.coalescer.finalize(requestId);
        if (!record)
            return;
        record.duration = Math.max(0, failedAt - record.t);
        this.write(record);
    }
    snapshotCorrelation(request) {
        const out = {};
        const seq = this.deps.getCurrentStatementSeq();
        if (seq !== undefined)
            out.statementSeq = seq;
        const stepPath = this.deps.getCurrentStepPath();
        if (stepPath)
            out.stepPath = stepPath;
        const frame = safeFrame(request);
        if (frame) {
            try {
                const frameUrl = frame.url();
                if (frameUrl)
                    out.frameUrl = frameUrl;
                const page = frame.page();
                const pageUrl = page?.url();
                if (pageUrl)
                    out.pageUrl = pageUrl;
            }
            catch {
                // ignore — frame may have been detached.
            }
        }
        return out;
    }
    write(record) {
        if (this.closed)
            return;
        if (this.fd === undefined)
            this.fd = fs.openSync(this.ndjsonPath, 'a');
        fs.writeSync(this.fd, JSON.stringify(record) + '\n');
    }
    /**
     * Drain any pending exchanges (writing them as-is) and, when the
     * test ended in a failing status and `bodyMode === 'on-error'`,
     * flush buffered bodies as `network-body` records. Idempotent.
     *
     * The body is currently fully synchronous (every disk write is
     * `fs.writeSync`); the `Promise<void>` return type is preserved so
     * the fixture can await it under `withTimeout` exactly like
     * `projector.finalize`.
     */
    stop(testFailed) {
        if (this.closed)
            return Promise.resolve();
        // Revert APIRequestContext method patches first so any callbacks
        // that fire during the rest of teardown see the originals.
        for (const patch of this.apiPatches) {
            patch.target[patch.method] = patch.original;
        }
        this.apiPatches.length = 0;
        for (const stale of this.coalescer.drain()) {
            stale.duration = Math.max(0, this.deps.clock.now() - this.deps.startedAt - stale.t);
            this.write(stale);
        }
        if (this.bodyMode === 'on-error' && testFailed) {
            for (const [requestId, bodies] of this.bufferedBodies) {
                if (!bodies.request && !bodies.response)
                    continue;
                const bodyRecord = {
                    kind: 'network-body',
                    requestId,
                    ...(bodies.request ? { requestBody: bodies.request } : {}),
                    ...(bodies.response ? { responseBody: bodies.response } : {}),
                };
                if (this.fd === undefined)
                    this.fd = fs.openSync(this.ndjsonPath, 'a');
                fs.writeSync(this.fd, JSON.stringify(bodyRecord) + '\n');
            }
        }
        this.bufferedBodies.clear();
        this.closed = true;
        if (this.fd !== undefined) {
            try {
                fs.closeSync(this.fd);
            }
            catch {
                // best-effort.
            }
            this.fd = undefined;
        }
        return Promise.resolve();
    }
}
exports.NetworkCaptureSession = NetworkCaptureSession;
// ---- Playwright surface guards ----
//
// These tiny wrappers swallow the very narrow set of throws Playwright
// can surface from frozen / closed contexts. Without them, a single
// torn-down page would crash the entire trace. Each helper returns a
// safe default and the session keeps going.
function safeUrl(request) {
    try {
        return request.url();
    }
    catch {
        return '';
    }
}
function safeMethod(request) {
    try {
        return request.method();
    }
    catch {
        return '';
    }
}
function safeHeaders(request) {
    try {
        return request.headers();
    }
    catch {
        return {};
    }
}
function safeResponseHeaders(response) {
    try {
        return response.headers();
    }
    catch {
        return {};
    }
}
function safeResourceType(request) {
    try {
        const fn = request.resourceType;
        return typeof fn === 'function' ? fn.call(request) : undefined;
    }
    catch {
        return undefined;
    }
}
function safeIsNavigationRequest(request) {
    try {
        const fn = request.isNavigationRequest;
        return typeof fn === 'function' ? fn.call(request) : undefined;
    }
    catch {
        return undefined;
    }
}
function safePostDataBuffer(request) {
    try {
        return request.postDataBuffer();
    }
    catch {
        return null;
    }
}
async function safeResponse(request) {
    try {
        return await request.response();
    }
    catch {
        return null;
    }
}
function safeFromServiceWorker(response) {
    try {
        const fn = response.fromServiceWorker;
        return typeof fn === 'function' ? fn.call(response) : undefined;
    }
    catch {
        return undefined;
    }
}
function safeSecurityDetails(response) {
    try {
        const fn = response.securityDetails;
        if (typeof fn !== 'function')
            return undefined;
        // Playwright's API is async; we don't await here to keep the
        // response handler synchronous. If users want full detail they
        // can extend later — the field stays undefined for now.
        void fn;
        return undefined;
    }
    catch {
        return undefined;
    }
}
function safeServerAddr(response) {
    try {
        const fn = response.serverAddr;
        if (typeof fn !== 'function')
            return undefined;
        void fn;
        return undefined;
    }
    catch {
        return undefined;
    }
}
function safeTiming(request) {
    try {
        const fn = request.timing;
        return typeof fn === 'function' ? fn.call(request) : undefined;
    }
    catch {
        return undefined;
    }
}
function safeFailure(request) {
    try {
        const fn = request.failure;
        if (typeof fn !== 'function')
            return undefined;
        return fn.call(request)?.errorText;
    }
    catch {
        return undefined;
    }
}
function safeFrame(request) {
    try {
        const fn = request.frame;
        return typeof fn === 'function' ? fn.call(request) : null;
    }
    catch {
        return null;
    }
}
function redirectedFromIdFor(request, coalescer) {
    try {
        const fn = request.redirectedFrom;
        const prior = typeof fn === 'function' ? fn.call(request) : null;
        if (!prior)
            return undefined;
        return coalescer.idFor(prior);
    }
    catch {
        return undefined;
    }
}
function headerLookup(headers, name) {
    const lower = name.toLowerCase();
    for (const [key, value] of Object.entries(headers)) {
        if (key.toLowerCase() === lower)
            return value;
    }
    return undefined;
}
function parseContentLength(value) {
    if (!value)
        return undefined;
    const n = Number.parseInt(value, 10);
    return Number.isFinite(n) && n >= 0 ? n : undefined;
}
