/**
 * Mock `RuntimeDeps` for testing.
 *
 * Provides a fully controllable runtime implementation for unit tests.
 * Consumer projects can extend `MockRuntime` with project-specific helpers
 * (e.g. `setup_mock_zap_config`) that stay local.
 *
 * @module
 */
/**
 * Create a mock `RuntimeDeps` for testing.
 *
 * The mock `exit` records the code on `exit_calls` and throws `MockExitError`
 * (so the never-returning contract holds in tests). `fetch` throws `TypeError`
 * when no `mock_fetch_responses` pattern matches the request URL.
 *
 * @returns `MockRuntime` with controllable state
 *
 * @example
 * ```ts
 * const runtime = create_mock_runtime(['apply', 'zap.ts']);
 * runtime.mock_env.set('HOME', '/home/test');
 * runtime.mock_fs.set('/home/test/.app/config.json', '{}');
 *
 * await some_function(runtime);
 *
 * assert.strictEqual(runtime.command_calls.length, 1);
 * assert.deepStrictEqual(runtime.exit_calls, [0]);
 * ```
 */
export const create_mock_runtime = (args = []) => {
    const mock_env = new Map();
    const mock_fs = new Map();
    const mock_fs_bytes = new Map();
    const mock_dirs = new Set();
    const exit_calls = [];
    const command_calls = [];
    const command_inherit_calls = [];
    const stdout_writes = [];
    const mock_command_results = new Map();
    const fetch_calls = [];
    const mock_fetch_responses = new Map();
    let stdin_buffer = null;
    const runtime = {
        args,
        mock_env,
        mock_fs,
        mock_fs_bytes,
        mock_dirs,
        exit_calls,
        command_calls,
        command_inherit_calls,
        stdout_writes,
        mock_command_results,
        fetch_calls,
        mock_fetch_responses,
        get stdin_buffer() {
            return stdin_buffer;
        },
        set stdin_buffer(value) {
            stdin_buffer = value;
        },
        // === Environment ===
        env_get: (name) => mock_env.get(name),
        env_set: (name, value) => {
            mock_env.set(name, value);
        },
        env_all: () => Object.fromEntries(mock_env),
        // === Process ===
        cwd: () => '/mock/cwd',
        exit: (code) => {
            exit_calls.push(code);
            throw new MockExitError(code);
        },
        // === Local File System ===
        stat: async (path) => {
            const bytes = mock_fs_bytes.get(path);
            if (bytes !== undefined) {
                return { is_file: true, is_directory: false, size: bytes.length };
            }
            const text = mock_fs.get(path);
            if (text !== undefined) {
                return { is_file: true, is_directory: false, size: new TextEncoder().encode(text).length };
            }
            if (mock_dirs.has(path)) {
                return { is_file: false, is_directory: true, size: 0 };
            }
            return null;
        },
        mkdir: async (path, options) => {
            if (options?.recursive) {
                const parts = path.split('/').filter(Boolean);
                let current = '';
                for (const part of parts) {
                    current += '/' + part;
                    mock_dirs.add(current);
                }
            }
            else {
                mock_dirs.add(path);
            }
        },
        read_text_file: async (path) => {
            const content = mock_fs.get(path);
            if (content !== undefined)
                return content;
            const bytes = mock_fs_bytes.get(path);
            if (bytes !== undefined)
                return new TextDecoder().decode(bytes);
            const error = new Error(`ENOENT: no such file or directory: ${path}`);
            error.code = 'ENOENT';
            throw error;
        },
        read_file: async (path) => {
            const bytes = mock_fs_bytes.get(path);
            if (bytes !== undefined)
                return bytes;
            const content = mock_fs.get(path);
            if (content !== undefined)
                return new TextEncoder().encode(content);
            const error = new Error(`ENOENT: no such file or directory: ${path}`);
            error.code = 'ENOENT';
            throw error;
        },
        read_file_stream: async (path) => {
            let bytes = mock_fs_bytes.get(path);
            if (bytes === undefined) {
                const content = mock_fs.get(path);
                if (content !== undefined)
                    bytes = new TextEncoder().encode(content);
            }
            if (bytes === undefined) {
                const error = new Error(`ENOENT: no such file or directory: ${path}`);
                error.code = 'ENOENT';
                throw error;
            }
            // Single-chunk stream — the mock buffers in memory (fine for tests);
            // real runtimes stream incrementally.
            const data = bytes;
            return new ReadableStream({
                start(controller) {
                    controller.enqueue(data);
                    controller.close();
                },
            });
        },
        read_text_from_offset: async (path, offset) => {
            let bytes;
            const stored_bytes = mock_fs_bytes.get(path);
            if (stored_bytes !== undefined) {
                bytes = stored_bytes;
            }
            else {
                const content = mock_fs.get(path);
                if (content === undefined) {
                    const error = new Error(`ENOENT: no such file or directory: ${path}`);
                    error.code = 'ENOENT';
                    throw error;
                }
                bytes = new TextEncoder().encode(content);
            }
            const file_size = bytes.length;
            const bytes_to_read = Math.max(0, file_size - offset);
            if (bytes_to_read === 0)
                return { content: '', bytes_read: 0, file_size };
            const slice = bytes.subarray(offset, offset + bytes_to_read);
            return {
                content: new TextDecoder().decode(slice),
                bytes_read: slice.length,
                file_size,
            };
        },
        readdir: async (path) => {
            const prefix = path.endsWith('/') ? path : path + '/';
            const seen = new Set();
            const collect = (key) => {
                if (!key.startsWith(prefix))
                    return;
                const rest = key.slice(prefix.length);
                const slash = rest.indexOf('/');
                seen.add(slash === -1 ? rest : rest.slice(0, slash));
            };
            for (const key of mock_fs.keys())
                collect(key);
            for (const key of mock_fs_bytes.keys())
                collect(key);
            for (const key of mock_dirs)
                collect(key);
            if (seen.size === 0 && !mock_dirs.has(path)) {
                const error = new Error(`ENOENT: no such file or directory: ${path}`);
                error.code = 'ENOENT';
                throw error;
            }
            return Array.from(seen).sort();
        },
        write_text_file: async (path, content) => {
            mock_fs.set(path, content);
        },
        write_file: async (path, data) => {
            mock_fs_bytes.set(path, data);
        },
        write_file_stream: async (path, data) => {
            // Drain the stream into one buffer (the mock has no real disk;
            // real runtimes write incrementally with backpressure).
            const chunks = [];
            let total = 0;
            const reader = data.getReader();
            for (;;) {
                const { done, value } = await reader.read();
                if (done)
                    break;
                if (value) {
                    chunks.push(value);
                    total += value.length;
                }
            }
            const merged = new Uint8Array(total);
            let offset = 0;
            for (const chunk of chunks) {
                merged.set(chunk, offset);
                offset += chunk.length;
            }
            mock_fs_bytes.set(path, merged);
        },
        rename: async (old_path, new_path) => {
            const content = mock_fs.get(old_path);
            if (content !== undefined) {
                mock_fs.set(new_path, content);
                mock_fs.delete(old_path);
            }
            const bytes = mock_fs_bytes.get(old_path);
            if (bytes !== undefined) {
                mock_fs_bytes.set(new_path, bytes);
                mock_fs_bytes.delete(old_path);
            }
        },
        remove: async (path, options) => {
            mock_fs.delete(path);
            mock_fs_bytes.delete(path);
            mock_dirs.delete(path);
            if (options?.recursive) {
                const prefix = path.endsWith('/') ? path : path + '/';
                for (const key of mock_fs.keys()) {
                    if (key.startsWith(prefix))
                        mock_fs.delete(key);
                }
                for (const key of mock_fs_bytes.keys()) {
                    if (key.startsWith(prefix))
                        mock_fs_bytes.delete(key);
                }
                for (const key of mock_dirs) {
                    if (key.startsWith(prefix))
                        mock_dirs.delete(key);
                }
            }
        },
        // === HTTP ===
        fetch: async (input, init) => {
            fetch_calls.push({ input: input, init });
            const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
            for (const [pattern, response] of mock_fetch_responses) {
                if (url.includes(pattern))
                    return response.clone();
            }
            throw new TypeError(`fetch failed (no mock for ${url})`);
        },
        // === Local Commands ===
        run_command: async (cmd, args, options) => {
            command_calls.push(options ? { cmd, args, options } : { cmd, args });
            const key = `${cmd} ${args.join(' ')}`;
            const mocked = mock_command_results.get(key);
            if (mocked) {
                if (options?.timeout_ms !== undefined && mocked.timed_out === undefined) {
                    return { ...mocked, timed_out: false };
                }
                return mocked;
            }
            const result = { success: true, code: 0, stdout: '', stderr: '' };
            if (options?.timeout_ms !== undefined)
                result.timed_out = false;
            return result;
        },
        run_command_inherit: async (cmd, args) => {
            command_inherit_calls.push({ cmd, args });
            const key = `${cmd} ${args.join(' ')}`;
            const mocked = mock_command_results.get(key);
            if (mocked)
                return mocked.code;
            return 0;
        },
        // === Terminal I/O ===
        stdout_write: async (data) => {
            stdout_writes.push(new TextDecoder().decode(data));
            return data.length;
        },
        stdin_read: async (buffer) => {
            if (stdin_buffer === null)
                return null;
            const len = Math.min(buffer.length, stdin_buffer.length);
            buffer.set(stdin_buffer.subarray(0, len));
            stdin_buffer = null;
            return len;
        },
        // === Logging ===
        warn: (..._args) => { },
    };
    return runtime;
};
/**
 * Reset a mock runtime to initial state.
 *
 * @mutates runtime - clears all mock state (env, fs, dirs, exit/command/stdout/fetch call records, mock results, stdin buffer)
 */
export const reset_mock_runtime = (runtime) => {
    runtime.mock_env.clear();
    runtime.mock_fs.clear();
    runtime.mock_fs_bytes.clear();
    runtime.mock_dirs.clear();
    runtime.exit_calls.length = 0;
    runtime.command_calls.length = 0;
    runtime.command_inherit_calls.length = 0;
    runtime.stdout_writes.length = 0;
    runtime.mock_command_results.clear();
    runtime.fetch_calls.length = 0;
    runtime.mock_fetch_responses.clear();
    runtime.stdin_buffer = null;
};
/**
 * Set stdin buffer for simulating user input.
 *
 * @param input - string to provide as stdin input
 * @mutates `runtime.stdin_buffer`
 */
export const set_mock_stdin = (runtime, input) => {
    runtime.stdin_buffer = new TextEncoder().encode(input);
};
/**
 * Error thrown when mock `runtime.exit()` is called.
 *
 * Tests can catch this to verify exit behavior.
 */
export class MockExitError extends Error {
    code;
    constructor(code) {
        super(`exit(${code})`);
        this.name = 'MockExitError';
        this.code = code;
    }
}
