import cp from 'node:child_process';
import { existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { WorkflowRunSchema } from '@workflow/world';
import chalk from 'chalk';
import jsonlines from 'jsonlines';
import { assert, onTestFailed, onTestFinished } from 'vitest';
import * as z from 'zod';
export const Control = z.object({
    state: z.literal('listening'),
    info: z.object({
        port: z.number(),
    }),
});
export async function startServer(opts) {
    let serverPath = new URL('./server.mts', import.meta.url);
    if (!existsSync(serverPath)) {
        serverPath = new URL('./server.mjs', import.meta.url);
    }
    const proc = cp.spawn('node', [fileURLToPath(serverPath)], {
        stdio: ['ignore', 'pipe', 'pipe', 'pipe'],
        env: {
            ...process.env,
            WORKFLOW_TARGET_WORLD: opts.world,
            CONTROL_FD: '3',
        },
    });
    onTestFinished(() => {
        proc.kill();
    });
    const stdio = [];
    proc.stdout?.on('data', (chunk) => {
        stdio.push({ stream: chalk.white, chunk: chunk.toString() });
    });
    proc.stderr?.on('data', (chunk) => {
        stdio.push({ stream: chalk.red, chunk: chunk.toString() });
    });
    onTestFailed(() => {
        console.log('=== SERVER STDIO ===');
        let buffer = '';
        for (const { stream, chunk } of stdio) {
            buffer += stream.inverse(chunk);
        }
        console.log(buffer);
    });
    const fd3 = proc.stdio[3];
    assert(fd3, 'fd3 should be defined');
    for await (const chunk of fd3.pipe(jsonlines.parse())) {
        return Control.parse(chunk);
    }
    throw new Error('Server did not start correctly');
}
const Invoke = z.object({ runId: z.coerce.string() });
export function createFetcher(control) {
    return {
        async invoke(file, workflow, args) {
            const x = await fetch(`http://localhost:${control.info.port}/invoke`, {
                method: 'POST',
                headers: {
                    'content-type': 'application/json',
                },
                body: JSON.stringify({ file, workflow, args }),
            });
            const data = await x.json().then(Invoke.parse);
            onTestFailed(() => {
                console.error('Workflow run:', data.runId);
            });
            return data;
        },
        async getRun(id) {
            const x = await fetch(`http://localhost:${control.info.port}/runs/${encodeURIComponent(id)}`);
            const text = await x.text();
            // Custom JSON reviver to decode base64 back to Uint8Array
            const data = JSON.parse(text, (_key, value) => {
                if (value !== null &&
                    typeof value === 'object' &&
                    value.__type === 'Uint8Array' &&
                    typeof value.data === 'string') {
                    return new Uint8Array(Buffer.from(value.data, 'base64'));
                }
                return value;
            });
            return WorkflowRunSchema.parseAsync(data);
        },
        async getReadable(id) {
            const x = await fetch(`http://localhost:${control.info.port}/runs/${encodeURIComponent(id)}/readable`);
            if (!x.ok) {
                throw new Error(`Failed to get readable stream: ${x.status} ${x.statusText}`);
            }
            if (!x.body) {
                throw new Error('No body in response');
            }
            return x.body;
        },
        async resumeHook(token, payload) {
            const res = await fetch(`http://localhost:${control.info.port}/hooks/${token}`, {
                method: 'POST',
                headers: {
                    'content-type': 'application/json',
                },
                body: JSON.stringify(payload),
            });
            res.arrayBuffer().catch(() => { }); // Drain the body to avoid resource leaks
            if (!res.ok) {
                throw new Error(`Failed to resume hook: ${res.status} ${res.statusText}`);
            }
        },
    };
}
//# sourceMappingURL=util.mjs.map