// Test helpers that fabricate `GalaxyClient` stand-ins for the
// auto-sync pipeline. Two flavours:
//
//   - `recordingClient` — captures every `submitOrder` call and lets
//     the test assert on the order of in-flight payloads. The
//     outcome (`ok` / `rejected`) is settable per call so tests can
//     simulate retry loops.
//   - `fakeFetchClient` — wires a synthetic `user.games.order.get`
//     response so `OrderDraftStore.hydrateFromServer` exercises the
//     decoder against a populated FBS envelope.
//
// Both helpers live under `tests/helpers/` so they can be reused
// across `order-draft.test.ts`, `inspector-overlay.test.ts`, and
// future Phase 14+ specs.

import { Builder } from "flatbuffers";

import type { GalaxyClient } from "../../src/api/galaxy-client";
import { uuidToHiLo } from "../../src/api/game-state";
import { UUID } from "../../src/proto/galaxy/fbs/common";
import {
	CommandItem,
	CommandPayload,
	CommandPlanetRename,
	UserGamesOrder,
	UserGamesOrderGetResponse,
	UserGamesOrderResponse,
} from "../../src/proto/galaxy/fbs/order";
import type { OrderCommand } from "../../src/sync/order-types";

interface RecordedCall {
	messageType: string;
	commandIds: string[];
}

/**
 * RecordingOutcome enumerates the synthetic server reactions a test
 * can drive through `recordingClient.setOutcome`. Phase 25 adds the
 * `turn_already_closed` and `game_paused` codes (the order-queue
 * classifies them into `conflict` / `paused` outcomes) and `throw`
 * which lets the test exercise the network-error branch of
 * `OrderQueue.send`.
 */
export type RecordingOutcome =
	| "ok"
	| "rejected"
	| "turn_already_closed"
	| "game_paused"
	| "throw";

interface RecordingHandle {
	client: GalaxyClient;
	calls: RecordedCall[];
	setOutcome(outcome: RecordingOutcome): void;
	waitForCalls(n: number): Promise<void>;
	waitForIdle(): Promise<void>;
}

/**
 * recordingClient returns a fake GalaxyClient whose `executeCommand`
 * decodes the in-flight UserGamesOrder, records the cmd_ids, and
 * answers with a synthesised UserGamesOrderResponse where every
 * cmdApplied is true (when outcome="ok") or false (when outcome=
 * "rejected"). An optional `delayMs` simulates network latency so
 * tests can exercise the coalescing path.
 */
export function recordingClient(
	gameId: string,
	initialOutcome: RecordingOutcome,
	options: { delayMs?: number } = {},
): RecordingHandle {
	const calls: RecordedCall[] = [];
	let outcome: RecordingOutcome = initialOutcome;
	let inFlight = 0;
	const waiters: (() => void)[] = [];

	const client: GalaxyClient = {
		async executeCommand(messageType: string, payload: Uint8Array) {
			inFlight += 1;
			try {
				if (options.delayMs !== undefined) {
					await new Promise<void>((resolve) =>
						setTimeout(resolve, options.delayMs),
					);
				}
				if (messageType === "user.games.order") {
					const decoded = UserGamesOrder.getRootAsUserGamesOrder(
						new (await import("flatbuffers")).ByteBuffer(payload),
					);
					const length = decoded.commandsLength();
					const commandIds: string[] = [];
					for (let i = 0; i < length; i++) {
						const item = decoded.commands(i);
						if (item === null) continue;
						const id = item.cmdId();
						if (id !== null) commandIds.push(id);
					}
					calls.push({ messageType, commandIds });
					switch (outcome) {
						case "ok":
							return {
								resultCode: "ok",
								payloadBytes: encodeApplied(gameId, commandIds, true),
							};
						case "turn_already_closed":
							return {
								resultCode: "turn_already_closed",
								payloadBytes: new TextEncoder().encode(
									JSON.stringify({
										code: "turn_already_closed",
										message: "turn closed before submit",
									}),
								),
							};
						case "game_paused":
							return {
								resultCode: "game_paused",
								payloadBytes: new TextEncoder().encode(
									JSON.stringify({
										code: "game_paused",
										message: "game is paused",
									}),
								),
							};
						case "throw":
							throw new Error("network down");
						default:
							return {
								resultCode: "invalid_request",
								payloadBytes: new TextEncoder().encode(
									JSON.stringify({
										code: "validation_failed",
										message: "rejected by fixture",
									}),
								),
							};
					}
				}
				throw new Error(`unexpected messageType ${messageType}`);
			} finally {
				inFlight -= 1;
				if (inFlight === 0) {
					while (waiters.length > 0) {
						const wake = waiters.shift();
						wake?.();
					}
				}
			}
		},
	} as unknown as GalaxyClient;

	return {
		client,
		calls,
		setOutcome(next: RecordingOutcome) {
			outcome = next;
		},
		async waitForCalls(n: number) {
			while (calls.length < n) {
				await new Promise<void>((resolve) => setTimeout(resolve, 5));
			}
		},
		async waitForIdle() {
			if (inFlight === 0) return;
			await new Promise<void>((resolve) => waiters.push(resolve));
		},
	};
}

/**
 * fakeFetchClient returns a GalaxyClient stand-in whose
 * `executeCommand` answers a single hard-coded
 * UserGamesOrderGetResponse — enough for `hydrateFromServer` to
 * decode a realistic payload without standing up a full mock
 * gateway.
 */
export function fakeFetchClient(
	gameId: string,
	commands: OrderCommand[],
	updatedAt: number,
	found = true,
): { client: GalaxyClient } {
	const client: GalaxyClient = {
		async executeCommand(messageType: string) {
			if (messageType !== "user.games.order.get") {
				throw new Error(`unexpected messageType ${messageType}`);
			}
			return {
				resultCode: "ok",
				payloadBytes: encodeOrderGet(gameId, commands, updatedAt, found),
			};
		},
	} as unknown as GalaxyClient;
	return { client };
}

function encodeApplied(
	gameId: string,
	cmdIds: string[],
	applied: boolean,
): Uint8Array {
	const builder = new Builder(256);
	const itemOffsets = cmdIds.map((id) => {
		const cmdIdOffset = builder.createString(id);
		const nameOffset = builder.createString("ignored");
		const inner = CommandPlanetRename.createCommandPlanetRename(
			builder,
			BigInt(0),
			nameOffset,
		);
		CommandItem.startCommandItem(builder);
		CommandItem.addCmdId(builder, cmdIdOffset);
		CommandItem.addCmdApplied(builder, applied);
		CommandItem.addPayloadType(builder, CommandPayload.CommandPlanetRename);
		CommandItem.addPayload(builder, inner);
		return CommandItem.endCommandItem(builder);
	});
	const commandsVec = UserGamesOrderResponse.createCommandsVector(
		builder,
		itemOffsets,
	);
	const [hi, lo] = uuidToHiLo(gameId);
	const gameIdOffset = UUID.createUUID(builder, hi, lo);
	UserGamesOrderResponse.startUserGamesOrderResponse(builder);
	UserGamesOrderResponse.addGameId(builder, gameIdOffset);
	UserGamesOrderResponse.addUpdatedAt(builder, BigInt(Date.now()));
	UserGamesOrderResponse.addCommands(builder, commandsVec);
	const offset = UserGamesOrderResponse.endUserGamesOrderResponse(builder);
	builder.finish(offset);
	return builder.asUint8Array();
}

function encodeOrderGet(
	gameId: string,
	commands: OrderCommand[],
	updatedAt: number,
	found: boolean,
): Uint8Array {
	const builder = new Builder(256);

	let orderOffset = 0;
	if (found) {
		const itemOffsets = commands.map((cmd) => {
			if (cmd.kind !== "planetRename") {
				throw new Error(`unsupported command kind ${cmd.kind}`);
			}
			const cmdIdOffset = builder.createString(cmd.id);
			const nameOffset = builder.createString(cmd.name);
			const inner = CommandPlanetRename.createCommandPlanetRename(
				builder,
				BigInt(cmd.planetNumber),
				nameOffset,
			);
			CommandItem.startCommandItem(builder);
			CommandItem.addCmdId(builder, cmdIdOffset);
			CommandItem.addPayloadType(builder, CommandPayload.CommandPlanetRename);
			CommandItem.addPayload(builder, inner);
			return CommandItem.endCommandItem(builder);
		});
		const commandsVec = UserGamesOrder.createCommandsVector(builder, itemOffsets);
		const [hi, lo] = uuidToHiLo(gameId);
		const gameIdOffset = UUID.createUUID(builder, hi, lo);
		UserGamesOrder.startUserGamesOrder(builder);
		UserGamesOrder.addGameId(builder, gameIdOffset);
		UserGamesOrder.addUpdatedAt(builder, BigInt(updatedAt));
		UserGamesOrder.addCommands(builder, commandsVec);
		orderOffset = UserGamesOrder.endUserGamesOrder(builder);
	}

	UserGamesOrderGetResponse.startUserGamesOrderGetResponse(builder);
	UserGamesOrderGetResponse.addFound(builder, found);
	if (orderOffset !== 0) {
		UserGamesOrderGetResponse.addOrder(builder, orderOffset);
	}
	const offset =
		UserGamesOrderGetResponse.endUserGamesOrderGetResponse(builder);
	builder.finish(offset);
	return builder.asUint8Array();
}
