import { describe, expect, test } from "bun:test";
import { SseAuthError, SseParser, sseFrames } from "../src/sse.js";
import type { ResolvedConfig } from "../src/config.js";

describe("SseParser", () => {
	test("parses frames split across arbitrary chunks, skipping comments", () => {
		const p = new SseParser();
		let frames = p.push("id: 4\nevent: library.ch");
		expect(frames.length).toBe(0);
		frames = p.push('anged\ndata: {"seq":4}\n\n: keep-alive\n\nid: 5\n');
		expect(frames.length).toBe(1);
		expect(frames[0]).toEqual({ event: "library.changed", data: '{"seq":4}', id: "4" });
		frames = p.push("data: x\n\n");
		expect(frames.length).toBe(1);
		expect(frames[0]?.id).toBe("5");
		expect(frames[0]?.event).toBe("message");
	});

	test("joins multi-line data and handles CRLF", () => {
		const p = new SseParser();
		const frames = p.push("data: a\r\ndata: b\r\n\r\n");
		expect(frames[0]?.data).toBe("a\nb");
	});
});

const cfgFor = (port: number, token = "t"): ResolvedConfig => ({
	url: `http://127.0.0.1:${port}`,
	token,
	fetch,
});

describe("sseFrames", () => {
	test("reads frames, reconnects with Last-Event-ID after a server close", async () => {
		const lastEventIds: Array<string | null> = [];
		let connection = 0;
		const server = Bun.serve({
			port: 0,
			fetch(req) {
				lastEventIds.push(req.headers.get("last-event-id"));
				connection += 1;
				const first = connection === 1;
				const body = new ReadableStream({
					start(controller) {
						const enc = new TextEncoder();
						if (first) {
							controller.enqueue(enc.encode('id: 1\nevent: library.changed\ndata: {"seq":1}\n\n'));
							controller.close(); // server closes → client must reconnect
						} else {
							controller.enqueue(enc.encode('id: 2\nevent: library.changed\ndata: {"seq":2}\n\n'));
							// stay open
						}
					},
				});
				return new Response(body, { headers: { "content-type": "text/event-stream" } });
			},
		});
		try {
			const gen = sseFrames(cfgFor(server.port as number), { onWarning: () => {} });
			const f1 = await gen.next();
			expect(f1.value?.id).toBe("1");
			const f2 = await gen.next(); // spans the reconnect
			expect(f2.value?.id).toBe("2");
			await gen.return(undefined);
			// No `since` = live-tail-only: the first connect carries the beyond-tip cursor,
			// the reconnect carries the last REAL id.
			expect(lastEventIds[0]).toBe(String(Number.MAX_SAFE_INTEGER));
			expect(lastEventIds[1]).toBe("1");
		} finally {
			server.stop(true);
		}
	});

	test("401 is terminal (no retry loop)", async () => {
		const server = Bun.serve({
			port: 0,
			fetch: () => new Response("{}", { status: 401 }),
		});
		try {
			const gen = sseFrames(cfgFor(server.port as number), { onWarning: () => {} });
			await expect(gen.next()).rejects.toBeInstanceOf(SseAuthError);
		} finally {
			server.stop(true);
		}
	});
});
