/**
 * Mock provider helpers for testing the stream architecture.
 *
 * Provides declarative mock builders that make tests readable.
 */
/**
 * Default chunks to return from mock streams.
 */
export const defaultChunks = [
    { type: "text-start", id: "1" },
    { type: "text-delta", id: "1", text: "Hello" },
    { type: "text-delta", id: "1", text: " world" },
    { type: "text-delta", id: "1", text: "!" },
    { type: "text-end", id: "1" },
    {
        type: "finish",
        finishReason: "stop",
        totalUsage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
    },
];
/**
 * Default response for mock results.
 */
const defaultResponse = {
    finishReason: "stop",
    usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
    totalUsage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
    steps: [],
    response: { messages: [] },
    text: "Hello world!",
    toolCalls: [],
    toolResults: [],
};
/** Default usage stats for sentence-based providers. */
export const sentenceUsage = {
    inputTokens: 100,
    outputTokens: 50,
    totalTokens: 150,
};
/**
 * Create full stream chunks from an array of text strings.
 */
export function chunksFromText(textChunks, usage = { inputTokens: 10, outputTokens: 5, totalTokens: 15 }) {
    return [
        { type: "text-start", id: "1" },
        ...textChunks.map((text) => ({
            type: "text-delta",
            id: "1",
            text,
        })),
        { type: "text-end", id: "1" },
        {
            type: "finish",
            finishReason: "stop",
            totalUsage: usage,
        },
    ];
}
/**
 * Split a sentence into word-based text chunks (space-separated).
 * Each word after the first is prefixed with a space.
 */
export function textToChunks(text) {
    const words = text.split(" ");
    return words.map((word, i) => (i === 0 ? word : ` ${word}`));
}
/**
 * Create a mock StreamTextResult from text chunks.
 */
export function createResultFromText(textChunks, usage = { inputTokens: 10, outputTokens: 5, totalTokens: 15 }) {
    const fullStreamChunks = chunksFromText(textChunks, usage);
    return {
        steps: Promise.resolve([]),
        textStream: asyncIterableFrom(textChunks),
        fullStream: asyncIterableFrom(fullStreamChunks),
        response: Promise.resolve({
            text: textChunks.join(""),
            finishReason: "stop",
            totalUsage: usage,
        }),
    };
}
/**
 * Create a mock StreamTextResult from a sentence (splits by spaces).
 */
export function createResultFromSentence(text) {
    return createResultFromText(textToChunks(text), sentenceUsage);
}
/**
 * Create an AsyncIterableStream from an array of items.
 * Each call to [Symbol.asyncIterator]() creates a fresh iterator.
 */
export function asyncIterableFrom(items) {
    // Create a fresh iterator for each consumer
    const createIterator = () => {
        let index = 0;
        return {
            async next() {
                if (index < items.length) {
                    return { done: false, value: items[index++] };
                }
                return { done: true, value: undefined };
            },
        };
    };
    // Create a ReadableStream with its own index
    let streamIndex = 0;
    const stream = new ReadableStream({
        pull(controller) {
            if (streamIndex < items.length) {
                controller.enqueue(items[streamIndex++]);
            }
            else {
                controller.close();
            }
        },
    });
    // Return the stream with fresh iterator for each async iteration
    return Object.assign(stream, {
        [Symbol.asyncIterator]: () => createIterator(),
    });
}
export const MockProvider = {
    /**
     * Provider that always succeeds with the given chunks.
     */
    success(chunks = defaultChunks) {
        return {
            async streamText(_options) {
                const fullStream = asyncIterableFrom(chunks);
                const textChunks = chunks
                    .filter((c) => c.type === "text-delta")
                    .map((c) => c.text);
                return {
                    steps: Promise.resolve([]),
                    fullStream,
                    textStream: asyncIterableFrom(textChunks),
                    response: Promise.resolve(defaultResponse),
                };
            },
        };
    },
    /**
     * Provider that always succeeds with the given text (splits by spaces).
     */
    successWithText(text) {
        return {
            async streamText() {
                return createResultFromSentence(text);
            },
        };
    },
    /**
     * Provider that fails on the first N attempts then succeeds.
     * Optionally tracks calls by pushing to the provided `calls` array.
     */
    failsThenSucceeds(failCount, options = {}) {
        const { error = new Error("Network error"), text, textChunks, calls, } = options;
        let attempts = 0;
        return {
            async streamText(streamOpts) {
                attempts++;
                if (calls) {
                    calls.push({
                        messages: JSON.parse(JSON.stringify(streamOpts.messages ?? [])),
                        timestamp: Date.now(),
                    });
                }
                if (attempts <= failCount) {
                    throw error;
                }
                if (textChunks) {
                    return createResultFromText(textChunks);
                }
                if (text) {
                    return createResultFromSentence(text);
                }
                return MockProvider.success().streamText(streamOpts);
            },
        };
    },
    /**
     * Provider where the stream fails after N chunks (sends error chunk).
     */
    streamFailsAfter(chunkCount, error = new Error("Stream error")) {
        return {
            async streamText(_options) {
                const chunks = [
                    ...defaultChunks.slice(0, chunkCount),
                    { type: "error", error },
                ];
                return {
                    steps: Promise.resolve([]),
                    fullStream: asyncIterableFrom(chunks),
                    textStream: asyncIterableFrom([]),
                    response: Promise.resolve(defaultResponse),
                };
            },
        };
    },
    /**
     * Provider where the iterator throws after N chunks (simulates network disconnect).
     * Unlike streamFailsAfter which sends an error chunk, this throws from iterator.next().
     */
    iteratorThrowsAfter(chunkCount, error = new Error("Network disconnected")) {
        return {
            async streamText(_options) {
                const chunks = defaultChunks.slice(0, chunkCount);
                let index = 0;
                const throwingIterator = {
                    async next() {
                        if (index < chunks.length) {
                            return { done: false, value: chunks[index++] };
                        }
                        // Simulate network disconnect by throwing
                        throw error;
                    },
                };
                const stream = new ReadableStream({
                    async pull(controller) {
                        if (index < chunks.length) {
                            controller.enqueue(chunks[index++]);
                        }
                        else {
                            // Simulate network disconnect
                            controller.error(error);
                        }
                    },
                });
                return {
                    steps: Promise.resolve([]),
                    fullStream: Object.assign(stream, {
                        [Symbol.asyncIterator]: () => throwingIterator,
                    }),
                    textStream: asyncIterableFrom([]),
                    response: Promise.resolve(defaultResponse),
                };
            },
        };
    },
    /**
     * Provider where iterator throws on first N attempts, then succeeds.
     * Simulates network issues that recover after retries.
     */
    iteratorThrowsThenSucceeds(failCount, options = {}) {
        const { error = new Error("Network disconnected"), chunksBeforeError = 2, text, } = options;
        let attempts = 0;
        return {
            async streamText(streamOpts) {
                attempts++;
                if (attempts <= failCount) {
                    // Return a stream that throws mid-iteration
                    const chunks = defaultChunks.slice(0, chunksBeforeError);
                    let index = 0;
                    const throwingIterator = {
                        async next() {
                            if (index < chunks.length) {
                                return { done: false, value: chunks[index++] };
                            }
                            throw error;
                        },
                    };
                    const stream = new ReadableStream({
                        async pull(controller) {
                            if (index < chunks.length) {
                                controller.enqueue(chunks[index++]);
                            }
                            else {
                                controller.error(error);
                            }
                        },
                    });
                    return {
                        steps: Promise.resolve([]),
                        fullStream: Object.assign(stream, {
                            [Symbol.asyncIterator]: () => throwingIterator,
                        }),
                        textStream: asyncIterableFrom([]),
                        response: Promise.resolve(defaultResponse),
                    };
                }
                // Success case
                if (text) {
                    return createResultFromSentence(text);
                }
                return MockProvider.success().streamText(streamOpts);
            },
        };
    },
    /**
     * Provider that stalls after N chunks.
     */
    streamStalls(chunkCount) {
        return {
            async streamText(_options) {
                const chunks = defaultChunks.slice(0, chunkCount);
                let index = 0;
                const stallingIterable = {
                    [Symbol.asyncIterator]: () => ({
                        async next() {
                            if (index < chunks.length) {
                                return { done: false, value: chunks[index++] };
                            }
                            // stalls forever
                            return new Promise(() => { });
                        },
                    }),
                };
                const stream = new ReadableStream({
                    async pull(controller) {
                        if (index < chunks.length) {
                            controller.enqueue(chunks[index++]);
                        }
                        // Stall - never close
                    },
                });
                return {
                    steps: Promise.resolve([]),
                    fullStream: Object.assign(stream, {
                        [Symbol.asyncIterator]: () => stallingIterable[Symbol.asyncIterator](),
                    }),
                    textStream: asyncIterableFrom([]),
                    response: Promise.resolve(defaultResponse),
                };
            },
        };
    },
    /**
     * Provider that tracks calls for verification.
     */
    tracked(text) {
        const calls = [];
        return {
            calls,
            provider: {
                async streamText(options) {
                    calls.push({ options, timestamp: Date.now() });
                    if (text) {
                        return createResultFromSentence(text);
                    }
                    return MockProvider.success().streamText(options);
                },
            },
        };
    },
    /**
     * Provider that always fails with the given error.
     */
    alwaysFails(error = new Error("Always fails")) {
        return {
            async streamText() {
                throw error;
            },
        };
    },
    /**
     * Provider that waits for a delay then fails.
     */
    slowAlwaysFails(delayMs, error = new Error("Slow failure")) {
        return {
            async streamText() {
                await new Promise((r) => setTimeout(r, delayMs));
                throw error;
            },
        };
    },
    /**
     * Provider that stalls for a specified duration then continues producing chunks.
     * This simulates the scenario where stall detection fires but the underlying
     * stream continues to produce data (e.g., tool calls appear after the stall error).
     *
     * @param stallDurationMs - How long to stall before continuing
     * @param chunksBeforeStall - Number of chunks to emit before stalling
     * @param chunksAfterStall - Optional chunks to emit after stall (defaults to success chunks)
     */
    stallsThenContinues(stallDurationMs, chunksBeforeStall = 1, chunksAfterStall) {
        return {
            async streamText(_options) {
                const preStallChunks = defaultChunks.slice(0, chunksBeforeStall);
                const postStallChunks = (chunksAfterStall ?? [
                    { type: "text-start", id: "after-stall" },
                    {
                        type: "text-delta",
                        id: "after-stall",
                        text: "Continued after stall",
                    },
                    { type: "text-end", id: "after-stall" },
                    {
                        type: "finish",
                        finishReason: "stop",
                        totalUsage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
                    },
                ]);
                const allChunks = [...preStallChunks, ...postStallChunks];
                let index = 0;
                let hasStalled = false;
                const stream = new ReadableStream({
                    async pull(controller) {
                        // After pre-stall chunks, stall for the specified duration
                        if (index === preStallChunks.length && !hasStalled) {
                            hasStalled = true;
                            await new Promise((r) => setTimeout(r, stallDurationMs));
                        }
                        if (index < allChunks.length) {
                            controller.enqueue(allChunks[index++]);
                        }
                        else {
                            controller.close();
                        }
                    },
                });
                const asyncIterator = {
                    index: 0,
                    hasStalled: false,
                    async next() {
                        // After pre-stall chunks, stall for the specified duration
                        if (this.index === preStallChunks.length && !this.hasStalled) {
                            this.hasStalled = true;
                            await new Promise((r) => setTimeout(r, stallDurationMs));
                        }
                        if (this.index < allChunks.length) {
                            return { done: false, value: allChunks[this.index++] };
                        }
                        return { done: true, value: undefined };
                    },
                };
                return {
                    steps: Promise.resolve([]),
                    fullStream: Object.assign(stream, {
                        [Symbol.asyncIterator]: () => asyncIterator,
                    }),
                    textStream: asyncIterableFrom([]),
                    response: Promise.resolve(defaultResponse),
                };
            },
        };
    },
    /**
     * Provider with slow steps on first N attempts, then succeeds.
     * Simulates slow LLM responses that trigger stall detection.
     */
    slowStepsThenSucceeds(slowCount, stepDelayMs, options = {}) {
        let attempts = 0;
        return {
            async streamText(streamOptions) {
                attempts++;
                const isSlowAttempt = attempts <= slowCount;
                // Call prepareStep if provided
                if (streamOptions.prepareStep) {
                    await streamOptions.prepareStep({
                        messages: streamOptions.messages ?? [],
                        stepNumber: 1,
                    });
                }
                if (isSlowAttempt) {
                    // Simulate slow step by waiting before calling onStepFinish
                    await new Promise((r) => setTimeout(r, stepDelayMs));
                }
                // Call onStepFinish
                if (streamOptions.onStepFinish) {
                    await streamOptions.onStepFinish({
                        text: isSlowAttempt
                            ? "Slow response"
                            : (options.text ?? "Fast response"),
                        finishReason: "stop",
                        usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
                        toolCalls: [],
                        toolResults: [],
                        response: { messages: [] },
                        rawResponse: undefined,
                        request: undefined,
                        warnings: [],
                        isContinued: false,
                        providerMetadata: undefined,
                        files: [],
                        reasoning: undefined,
                        reasoningDetails: [],
                        sources: [],
                    });
                }
                // Call onFinish
                if (streamOptions.onFinish) {
                    await streamOptions.onFinish({
                        text: options.text ?? "Response",
                        finishReason: "stop",
                        usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
                        totalUsage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
                        toolCalls: [],
                        toolResults: [],
                        response: { messages: [] },
                        steps: [],
                        rawResponse: undefined,
                        request: undefined,
                        warnings: [],
                        providerMetadata: undefined,
                        files: [],
                        reasoning: undefined,
                        reasoningDetails: [],
                        sources: [],
                    });
                }
                const text = options.text ?? "Response";
                return createResultFromSentence(text);
            },
        };
    },
};
//# sourceMappingURL=mock-provider.js.map