/**
 * Chaos Monkey fetch wrapper for testing error handling.
 *
 * Randomly injects errors into requests to test retry logic and error
 * handling paths. This operates at the fetch level, making it realistic
 * for testing real-world failure scenarios.
 *
 * **Error types:**
 * 1. **HTTP errors**: Request fails before streaming starts (429, 500, 503, etc.)
 * 2. **Mid-stream HTTP errors**: Connection drops during streaming (ECONNRESET, timeout)
 * 3. **SSE error events**: Error chunks in the event stream
 *
 * Each fetch request represents one "step" in the AI SDK's multi-step
 * streaming architecture. The error rate controls what percentage of
 * steps will fail.
 */
import { getLogger } from "../../util/logger.js";
/**
 * Creates a fetch function that randomly injects errors for testing.
 *
 * This wraps the base fetch function and intercepts a percentage of requests,
 * injecting realistic errors including HTTP failures, mid-stream connection drops,
 * and SSE error events.
 *
 * @param options - Chaos configuration
 * @param baseFetch - Underlying fetch implementation to wrap (defaults to global fetch)
 * @returns Wrapped fetch function with error injection
 *
 * @example
 * ```typescript
 * // 30% error rate: 15% HTTP errors, 15% stream errors (7.5% abort, 7.5% SSE)
 * const chaosFetch = createChaosFetch(
 *   {
 *     errorRate: 0.3,              // 30% of requests error
 *     streamErrorRate: 0.5,         // 50% of errors are stream errors
 *     streamHttpErrorRate: 0.5,     // 50% of stream errors are HTTP aborts
 *     statusCodes: [429, 500, 503],
 *   },
 *   fetch
 * );
 *
 * // Use in provider settings
 * const settings = {
 *   clark: {
 *     fetch: chaosFetch
 *   }
 * };
 * ```
 */
export function createChaosFetch(options, baseFetch = fetch) {
    const logger = getLogger();
    const statusCodes = options.statusCodes || [429, 500, 503, 529];
    const errorCodes = options.errorCodes || [
        "rate_limit_error",
        "overloaded_error",
        "api_error",
    ];
    const streamErrorRate = options.streamErrorRate ?? 0.5;
    const streamErrorDelay = options.streamErrorDelay || { min: 1, max: 5 };
    const streamHttpErrorRate = options.streamHttpErrorRate ?? 0.5;
    let requestCount = 0;
    let errorCount = 0;
    let httpErrorCount = 0;
    let streamErrorCount = 0;
    let streamHttpErrorCount = 0;
    let streamSseErrorCount = 0;
    if (options.errorRate > 0) {
        logger.warn(`Chaos Monkey ACTIVE at fetch level: ${(options.errorRate * 100).toFixed(0)}% of requests will error (${(streamErrorRate * 100).toFixed(0)}% stream [${(streamHttpErrorRate * 100).toFixed(0)}% HTTP abort, ${((1 - streamHttpErrorRate) * 100).toFixed(0)}% SSE], ${((1 - streamErrorRate) * 100).toFixed(0)}% HTTP)`);
    }
    const chaosFetch = async (url, init) => {
        requestCount++;
        // Decide if this request should error
        const shouldError = Math.random() < options.errorRate;
        if (!shouldError) {
            return baseFetch(url, init);
        }
        errorCount++;
        // Pick error type: HTTP or stream
        const isStreamError = Math.random() < streamErrorRate;
        const code = pickRandom(errorCodes);
        if (!isStreamError) {
            // HTTP-level error
            httpErrorCount++;
            const status = pickRandom(statusCodes);
            logger.warn(`Chaos Monkey: Injecting HTTP error ${errorCount}/${requestCount} (${status} ${code})`);
            // Return error in clark proxy format (matches clark-error-handler.ts schema)
            return new Response(JSON.stringify({
                responseMetadata: {
                    status,
                    message: `Chaos monkey injected error: ${code}`,
                    success: false,
                    error: {
                        code,
                        message: `Simulated ${code} error for testing`,
                    },
                },
            }), {
                status,
                headers: { "Content-Type": "application/json" },
            });
        }
        // Stream-level error - let real fetch succeed, inject error into stream
        streamErrorCount++;
        const realResponse = await baseFetch(url, init);
        // Only transform successful SSE streams
        if (!realResponse.ok || !realResponse.body) {
            return realResponse;
        }
        const contentType = realResponse.headers.get("content-type") || "";
        if (!contentType.includes("text/event-stream")) {
            // Not an SSE stream, return as-is
            return realResponse;
        }
        // Decide stream error type: HTTP abort or SSE error event
        const isStreamHttpError = Math.random() < streamHttpErrorRate;
        // Inject error after random number of chunks
        const injectAfter = Math.floor(Math.random() * (streamErrorDelay.max - streamErrorDelay.min + 1)) + streamErrorDelay.min;
        if (isStreamHttpError) {
            // Mid-stream HTTP error (connection drop, timeout)
            streamHttpErrorCount++;
            logger.warn(`Chaos Monkey: Injecting mid-stream HTTP error ${errorCount}/${requestCount} after ${injectAfter} chunks (abort)`);
            let chunkCount = 0;
            const transformedBody = realResponse.body.pipeThrough(new TransformStream({
                transform(chunk, controller) {
                    chunkCount++;
                    // Pass through chunks until we hit the abort point
                    if (chunkCount < injectAfter) {
                        controller.enqueue(chunk);
                    }
                    else if (chunkCount === injectAfter) {
                        // Last chunk before abort - pass it through
                        controller.enqueue(chunk);
                        // Abort the stream by erroring the controller
                        controller.error(new Error("ECONNRESET: Connection reset by peer"));
                    }
                    // Chunks after abort point are never enqueued
                },
            }));
            return new Response(transformedBody, {
                status: realResponse.status,
                statusText: realResponse.statusText,
                headers: realResponse.headers,
            });
        }
        else {
            // SSE error event chunk
            streamSseErrorCount++;
            logger.warn(`Chaos Monkey: Injecting SSE error event ${errorCount}/${requestCount} after ${injectAfter} chunks (${code})`);
            let chunkCount = 0;
            let errorInjected = false;
            const transformedBody = realResponse.body.pipeThrough(new TransformStream({
                transform(chunk, controller) {
                    // Pass through real chunk
                    controller.enqueue(chunk);
                    // Inject error chunk at the right time
                    if (!errorInjected && ++chunkCount === injectAfter) {
                        errorInjected = true;
                        // Inject SSE error event
                        const errorEvent = `data: ${JSON.stringify({
                            type: "error",
                            error: {
                                message: `Chaos monkey injected stream error: ${code}`,
                                code,
                            },
                        })}\n\n`;
                        controller.enqueue(new TextEncoder().encode(errorEvent));
                    }
                },
            }));
            return new Response(transformedBody, {
                status: realResponse.status,
                statusText: realResponse.statusText,
                headers: realResponse.headers,
            });
        }
    };
    // Attach stats getter for observability
    chaosFetch.getStats = () => ({
        requestCount,
        errorCount,
        httpErrorCount,
        streamErrorCount,
        streamHttpErrorCount,
        streamSseErrorCount,
        errorRate: requestCount > 0 ? errorCount / requestCount : 0,
    });
    return chaosFetch;
}
/**
 * Picks a random element from an array.
 */
function pickRandom(arr) {
    const index = Math.floor(Math.random() * arr.length);
    return arr[index]; // Non-null assertion: array is never empty
}
//# sourceMappingURL=chaos-fetch.js.map