import { expect, test } from "bun:test";
import * as Path from "../path.ts";
import { Reader, Writer } from "../stream.ts";
import {
	decodeSubscribeResponse,
	encodeSubscribeResponse,
	Subscribe,
	SubscribeDrop,
	SubscribeEnd,
	SubscribeOk,
	type SubscribeResponse,
	SubscribeStart,
	SubscribeUpdate,
} from "./subscribe.ts";
import { Version } from "./version.ts";

function concat(chunks: Uint8Array[]): Uint8Array {
	const total = chunks.reduce((sum, c) => sum + c.byteLength, 0);
	const out = new Uint8Array(total);
	let offset = 0;
	for (const c of chunks) {
		out.set(c, offset);
		offset += c.byteLength;
	}
	return out;
}

async function encode(version: Version, resp: SubscribeResponse): Promise<Uint8Array> {
	const written: Uint8Array[] = [];
	const writer = new Writer(
		new WritableStream<Uint8Array>({ write: (chunk) => void written.push(new Uint8Array(chunk)) }),
	);
	await encodeSubscribeResponse(writer, resp, version);
	writer.close();
	await writer.closed;
	return concat(written);
}

async function responseRoundtrip(version: Version, resp: SubscribeResponse): Promise<SubscribeResponse> {
	const reader = new Reader(undefined, await encode(version, resp));
	return decodeSubscribeResponse(reader, version);
}

async function encodeMessage(
	version: Version,
	message: { encode(writer: Writer, version: Version): Promise<void> },
): Promise<Uint8Array> {
	const written: Uint8Array[] = [];
	const writer = new Writer(
		new WritableStream<Uint8Array>({ write: (chunk) => void written.push(new Uint8Array(chunk)) }),
	);
	await message.encode(writer, version);
	writer.close();
	await writer.closed;
	return concat(written);
}

test("SubscribeOk round-trips priority/ordered/groups on draft-04", async () => {
	const got = await responseRoundtrip(Version.DRAFT_04, {
		ok: new SubscribeOk({ priority: 7, ordered: true, maxLatency: 250, startGroup: 3 }),
	});
	expect("ok" in got).toBe(true);
	if (!("ok" in got)) throw new Error("expected ok");
	expect(got.ok.priority).toBe(7);
	expect(got.ok.ordered).toBe(true);
	expect(got.ok.startGroup).toBe(3);
});

test("Subscribe round-trips every option including startGroup 0", async () => {
	const message = new Subscribe({
		id: 4n,
		broadcast: Path.from("test"),
		track: "video",
		priority: 7,
		ordered: true,
		maxLatency: 250,
		startGroup: 0,
		endGroup: 9,
	});
	const got = await Subscribe.decode(
		new Reader(undefined, await encodeMessage(Version.DRAFT_05, message)),
		Version.DRAFT_05,
	);
	expect(got.priority).toBe(7);
	expect(got.ordered).toBe(true);
	expect(got.maxLatency).toBe(250);
	expect(got.startGroup).toBe(0);
	expect(got.endGroup).toBe(9);
});

test("SubscribeUpdate round-trips every option including startGroup 0", async () => {
	const message = new SubscribeUpdate({
		priority: 8,
		ordered: true,
		maxLatency: 500,
		startGroup: 0,
		endGroup: 12,
	});
	const got = await SubscribeUpdate.decode(
		new Reader(undefined, await encodeMessage(Version.DRAFT_05, message)),
		Version.DRAFT_05,
	);
	expect(got.priority).toBe(8);
	expect(got.ordered).toBe(true);
	expect(got.maxLatency).toBe(500);
	expect(got.startGroup).toBe(0);
	expect(got.endGroup).toBe(12);
});

test("SubscribeStart round-trips on draft-05", async () => {
	const got = await responseRoundtrip(Version.DRAFT_05, { start: new SubscribeStart(42) });
	expect("start" in got).toBe(true);
	if (!("start" in got)) throw new Error("expected start");
	expect(got.start.group).toBe(42);
});

test("SubscribeEnd round-trips on draft-05", async () => {
	const got = await responseRoundtrip(Version.DRAFT_05, { end: new SubscribeEnd(7) });
	expect("end" in got).toBe(true);
	if (!("end" in got)) throw new Error("expected end");
	expect(got.end.group).toBe(7);
});

test("SubscribeDrop is type 0x2 on draft-05 and 0x1 on draft-04", async () => {
	const drop: SubscribeResponse = { drop: new SubscribeDrop({ start: 1, end: 3, error: 0 }) };

	const wire05 = await encode(Version.DRAFT_05, drop);
	expect(wire05[0]).toBe(2);

	const wire04 = await encode(Version.DRAFT_04, drop);
	expect(wire04[0]).toBe(1);

	const got = await responseRoundtrip(Version.DRAFT_05, drop);
	expect("drop" in got).toBe(true);
	if (!("drop" in got)) throw new Error("expected drop");
	expect([got.drop.start, got.drop.end]).toEqual([1, 3]);
});

test("SUBSCRIBE_OK is rejected on draft-05", async () => {
	await expect(encode(Version.DRAFT_05, { ok: new SubscribeOk({ priority: 1 }) })).rejects.toThrow();
});
