// `OrderQueue` unit tests under JSDOM. The queue isolates the
// browser online/offline plumbing and the conflict / paused
// classification from the rest of the draft store, so these tests
// drive it directly with injected listeners and synthesised
// `SubmitResult`s.

import "@testing-library/jest-dom/vitest";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";

import { OrderQueue, classifyResult } from "../src/sync/order-queue.svelte";
import type {
	SubmitFailure,
	SubmitResult,
	SubmitSuccess,
} from "../src/sync/submit";

interface FakeBrowser {
	online: boolean;
	listeners: Map<string, Set<() => void>>;
	fireOnline: () => void;
	fireOffline: () => void;
}

function makeBrowser(initial: boolean): FakeBrowser {
	const listeners = new Map<string, Set<() => void>>();
	const browser: FakeBrowser = {
		online: initial,
		listeners,
		fireOnline: () => {
			browser.online = true;
			const bucket = listeners.get("online");
			if (bucket !== undefined) for (const h of [...bucket]) h();
		},
		fireOffline: () => {
			browser.online = false;
			const bucket = listeners.get("offline");
			if (bucket !== undefined) for (const h of [...bucket]) h();
		},
	};
	return browser;
}

function startQueue(
	browser: FakeBrowser,
	onOnline: () => void = () => undefined,
): OrderQueue {
	const queue = new OrderQueue();
	queue.start({
		onOnline,
		onlineProbe: () => browser.online,
		addEventListener: (event, handler) => {
			let bucket = browser.listeners.get(event);
			if (bucket === undefined) {
				bucket = new Set();
				browser.listeners.set(event, bucket);
			}
			bucket.add(handler);
		},
		removeEventListener: (event, handler) => {
			const bucket = browser.listeners.get(event);
			if (bucket !== undefined) bucket.delete(handler);
		},
	});
	return queue;
}

function success(updatedAt = 1): SubmitSuccess {
	return {
		ok: true,
		results: new Map([["id", "applied"]]),
		errorCodes: new Map([["id", null]]),
		updatedAt,
	};
}

function failure(resultCode: string, code = resultCode, message = ""): SubmitFailure {
	return {
		ok: false,
		resultCode,
		code,
		message: message || resultCode,
	};
}

describe("classifyResult", () => {
	test("ok result maps to success", () => {
		const out = classifyResult(success());
		expect(out.kind).toBe("success");
	});

	test("turn_already_closed resultCode maps to conflict", () => {
		const out = classifyResult(failure("turn_already_closed"));
		expect(out.kind).toBe("conflict");
		if (out.kind === "conflict") {
			expect(out.code).toBe("turn_already_closed");
		}
	});

	test("game_paused resultCode maps to paused", () => {
		const out = classifyResult(failure("game_paused", "game_paused", "paused"));
		expect(out.kind).toBe("paused");
		if (out.kind === "paused") {
			expect(out.code).toBe("game_paused");
			expect(out.message).toBe("paused");
		}
	});

	test("turn_already_closed via inner code (resultCode opaque) maps to conflict", () => {
		// gateway may set resultCode to something opaque while the
		// FBS error body carries the actionable code.
		const out = classifyResult(failure("conflict", "turn_already_closed"));
		expect(out.kind).toBe("conflict");
	});

	test("unknown failure code stays a rejected outcome", () => {
		const out = classifyResult(failure("validation_failed"));
		expect(out.kind).toBe("rejected");
	});
});

describe("OrderQueue.send", () => {
	let browser: FakeBrowser;
	let queue: OrderQueue;

	beforeEach(() => {
		browser = makeBrowser(true);
		queue = startQueue(browser);
	});

	afterEach(() => {
		queue.stop();
	});

	test("offline at call time short-circuits without invoking submit", async () => {
		browser.fireOffline();
		const submitFn = vi.fn<() => Promise<SubmitResult>>();
		const outcome = await queue.send(submitFn);
		expect(outcome.kind).toBe("offline");
		expect(submitFn).not.toHaveBeenCalled();
	});

	test("ok result is forwarded as success", async () => {
		const outcome = await queue.send(async () => success(42));
		expect(outcome.kind).toBe("success");
		if (outcome.kind === "success") {
			expect(outcome.result.updatedAt).toBe(42);
		}
	});

	test("turn_already_closed reply maps to conflict outcome", async () => {
		const outcome = await queue.send(async () =>
			failure("turn_already_closed", "turn_already_closed", "turn closed"),
		);
		expect(outcome.kind).toBe("conflict");
		if (outcome.kind === "conflict") {
			expect(outcome.message).toBe("turn closed");
		}
	});

	test("game_paused reply maps to paused outcome", async () => {
		const outcome = await queue.send(async () =>
			failure("game_paused", "game_paused", "paused"),
		);
		expect(outcome.kind).toBe("paused");
	});

	test("throw while online maps to failed outcome", async () => {
		const outcome = await queue.send(async () => {
			throw new Error("boom");
		});
		expect(outcome.kind).toBe("failed");
		if (outcome.kind === "failed") {
			expect(outcome.reason).toBe("boom");
		}
	});

	test("throw with offline probe maps to offline outcome", async () => {
		const outcome = await queue.send(async () => {
			browser.online = false;
			throw new Error("network down");
		});
		expect(outcome.kind).toBe("offline");
		expect(queue.online).toBe(false);
	});

	test("online event triggers the onOnline callback", () => {
		const seen: number[] = [];
		const newQueue = new OrderQueue();
		newQueue.start({
			onOnline: () => seen.push(seen.length + 1),
			onlineProbe: () => browser.online,
			addEventListener: (event, handler) => {
				let bucket = browser.listeners.get(event);
				if (bucket === undefined) {
					bucket = new Set();
					browser.listeners.set(event, bucket);
				}
				bucket.add(handler);
			},
			removeEventListener: (event, handler) => {
				const bucket = browser.listeners.get(event);
				if (bucket !== undefined) bucket.delete(handler);
			},
		});
		browser.fireOffline();
		expect(newQueue.online).toBe(false);
		browser.fireOnline();
		expect(newQueue.online).toBe(true);
		expect(seen).toEqual([1]);
		newQueue.stop();
	});

	test("start is idempotent for the same queue instance", () => {
		queue.start({
			onOnline: () => undefined,
			onlineProbe: () => browser.online,
			addEventListener: () => {
				throw new Error("must not be called");
			},
			removeEventListener: () => undefined,
		});
		expect(queue.online).toBe(true);
	});

	test("stop unsubscribes from browser events", () => {
		queue.stop();
		const before = queue.online;
		browser.fireOffline();
		// After stop, the queue no longer reflects browser flips.
		expect(queue.online).toBe(before);
	});
});
