import { describe, expect, it } from "vitest";
import { encode } from "./framing.js";
import type { SpawnedProcess, SpawnProcess } from "./transport.js";
import { createStdioTransport } from "./transport.js";

/**
 * In-memory pipe pair: simulates a child process. `emitStdout` pushes framed
 * bytes the server "wrote" to stdout (which we read); `writtenToStdin` captures
 * what we wrote to the child's stdin (our outgoing framed messages).
 */
function makePipe(): {
	process: SpawnedProcess;
	emitStdout: (data: Uint8Array) => void;
	emitEnd: () => void;
	writtenToStdin: () => Uint8Array[];
	killed: () => boolean;
} {
	const dataListeners: Array<(data: Uint8Array) => void> = [];
	const endListeners: Array<() => void> = [];
	const stdinWrites: Uint8Array[] = [];
	let killed = false;

	const process: SpawnedProcess = {
		stdin: {
			write: (bytes: Uint8Array) => {
				stdinWrites.push(bytes);
			},
		},
		stdout: {
			on: (event: string, cb: (data: Uint8Array) => void) => {
				if (event === "data") dataListeners.push(cb);
				else if (event === "end") endListeners.push(cb as unknown as () => void);
			},
		},
		pid: 12345,
		kill: () => {
			killed = true;
		},
	};

	return {
		process,
		emitStdout: (data: Uint8Array) => {
			for (const cb of dataListeners) cb(data);
		},
		emitEnd: () => {
			for (const cb of endListeners) cb();
		},
		writtenToStdin: () => stdinWrites,
		killed: () => killed,
	};
}

describe("createStdioTransport", () => {
	it("creates connection with correct pid", () => {
		const pair = makePipe();
		const spawn: SpawnProcess = () => pair.process;

		const { connection } = createStdioTransport({ spawn, command: ["test-server"] }, "/tmp");

		expect(connection.pid).toBe(12345);
		connection.close();
	});

	it("connection sends framed messages via stdin", () => {
		const pair = makePipe();
		const spawn: SpawnProcess = () => pair.process;

		const { connection } = createStdioTransport({ spawn, command: ["test"] }, "/tmp");

		connection.notify("test/method", { key: "value" });

		const writes = pair.writtenToStdin();
		expect(writes.length).toBe(1);
		const text = new TextDecoder().decode(writes[0]);
		expect(text).toContain("Content-Length:");
		expect(text).toContain('"method":"test/method"');
		connection.close();
	});

	it("close kills the child process", () => {
		const pair = makePipe();
		const spawn: SpawnProcess = () => pair.process;

		const { connection } = createStdioTransport({ spawn, command: ["test"] }, "/tmp");

		connection.close();
		expect(pair.killed()).toBe(true);
	});

	it("pipes stdout through framing: a notification triggers onNotification", async () => {
		const pair = makePipe();
		const spawn: SpawnProcess = () => pair.process;

		const { connection } = createStdioTransport({ spawn, command: ["test"] }, "/tmp");

		let received: unknown = null;
		connection.onNotification("notifications/tools/list_changed", (params) => {
			received = params;
		});

		// Simulate the server writing a framed notification to stdout.
		const notification = JSON.stringify({
			jsonrpc: "2.0",
			method: "notifications/tools/list_changed",
			params: { reason: "tools added" },
		});
		pair.emitStdout(encode(notification));

		// onNotification is invoked synchronously inside the data handler.
		expect(received).toEqual({ reason: "tools added" });
		connection.close();
	});

	it("pipes stdout through framing: a response resolves a request", async () => {
		const pair = makePipe();
		const spawn: SpawnProcess = () => pair.process;

		const { connection } = createStdioTransport({ spawn, command: ["test"] }, "/tmp");

		const resultPromise = connection.send("tools/list");

		// The request was framed and written to stdin; respond via stdout.
		const response = JSON.stringify({
			jsonrpc: "2.0",
			id: 1,
			result: { tools: [{ name: "t", description: "d", inputSchema: { type: "object" } }] },
		});
		pair.emitStdout(encode(response));

		const result = await resultPromise;
		expect(result).toEqual({
			tools: [{ name: "t", description: "d", inputSchema: { type: "object" } }],
		});
		connection.close();
	});

	it("handles a frame split across two stdout chunks", async () => {
		const pair = makePipe();
		const spawn: SpawnProcess = () => pair.process;

		const { connection } = createStdioTransport({ spawn, command: ["test"] }, "/tmp");

		const resultPromise = connection.send("ping");

		const response = encode(JSON.stringify({ jsonrpc: "2.0", id: 1, result: { ok: true } }));
		const mid = Math.floor(response.length / 2);
		pair.emitStdout(response.slice(0, mid));
		pair.emitStdout(response.slice(mid));

		await expect(resultPromise).resolves.toEqual({ ok: true });
		connection.close();
	});
});
