// Vitest unit coverage for `sync/order-load.ts`. Builds FBS
// `UserGamesOrderGetResponse` payloads by hand and verifies the
// decoder produces the expected `OrderCommand[]`.

import { Builder } from "flatbuffers";
import { describe, expect, test, vi } from "vitest";

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,
	CommandPlanetProduce,
	CommandPlanetRename,
	CommandPlanetRouteRemove,
	CommandPlanetRouteSet,
	PlanetProduction,
	PlanetRouteLoadType,
	UserGamesOrder,
	UserGamesOrderGet,
	UserGamesOrderGetResponse,
} from "../src/proto/galaxy/fbs/order";
import { fetchOrder, OrderLoadError } from "../src/sync/order-load";

const GAME_ID = "11111111-2222-3333-4444-555555555555";

function mockClient(
	executeCommand: (
		messageType: string,
		payload: Uint8Array,
	) => Promise<{ resultCode: string; payloadBytes: Uint8Array }>,
): GalaxyClient {
	return { executeCommand } as unknown as GalaxyClient;
}

function buildResponse(
	commands: { id: string; planetNumber: number; name: string }[],
	updatedAt: number,
	found = true,
): Uint8Array {
	const builder = new Builder(256);

	let orderOffset = 0;
	if (found) {
		const itemOffsets = commands.map((c) => {
			const cmdIdOffset = builder.createString(c.id);
			const nameOffset = builder.createString(c.name);
			const inner = CommandPlanetRename.createCommandPlanetRename(
				builder,
				BigInt(c.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(GAME_ID);
		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();
}

describe("fetchOrder", () => {
	test("decodes a found response into typed commands", async () => {
		const responsePayload = buildResponse(
			[{ id: "cmd-1", planetNumber: 7, name: "Earth" }],
			42,
		);
		const exec = vi.fn(async (messageType: string) => {
			expect(messageType).toBe("user.games.order.get");
			return { resultCode: "ok", payloadBytes: responsePayload };
		});
		const result = await fetchOrder(mockClient(exec), GAME_ID, 5);

		expect(result.commands).toHaveLength(1);
		const cmd = result.commands[0]!;
		expect(cmd.kind).toBe("planetRename");
		if (cmd.kind !== "planetRename") return;
		expect(cmd.id).toBe("cmd-1");
		expect(cmd.planetNumber).toBe(7);
		expect(cmd.name).toBe("Earth");
		expect(result.updatedAt).toBe(42);
	});

	test("found=false surfaces as an empty draft", async () => {
		const responsePayload = buildResponse([], 0, false);
		const exec = vi.fn(async () => ({
			resultCode: "ok",
			payloadBytes: responsePayload,
		}));
		const result = await fetchOrder(mockClient(exec), GAME_ID, 5);
		expect(result.commands).toEqual([]);
		expect(result.updatedAt).toBe(0);
	});

	test("rejects negative turn before issuing a request", async () => {
		const exec = vi.fn(async () => ({
			resultCode: "ok",
			payloadBytes: new Uint8Array(),
		}));
		await expect(fetchOrder(mockClient(exec), GAME_ID, -1)).rejects.toBeInstanceOf(
			OrderLoadError,
		);
		expect(exec).not.toHaveBeenCalled();
	});

	test("throws OrderLoadError on non-ok resultCode", async () => {
		const exec = vi.fn(async () => ({
			resultCode: "internal_error",
			payloadBytes: new TextEncoder().encode(
				JSON.stringify({ code: "boom", message: "down" }),
			),
		}));
		await expect(fetchOrder(mockClient(exec), GAME_ID, 5)).rejects.toMatchObject({
			name: "OrderLoadError",
			resultCode: "internal_error",
			code: "boom",
		});
	});

	test("decodes a CommandPlanetProduce envelope into setProductionType", async () => {
		const builder = new Builder(256);
		const cmdIdOffset = builder.createString("cmd-prod");
		const subjectOffset = builder.createString("Scout");
		const inner = CommandPlanetProduce.createCommandPlanetProduce(
			builder,
			BigInt(17),
			PlanetProduction.SHIP,
			subjectOffset,
		);
		CommandItem.startCommandItem(builder);
		CommandItem.addCmdId(builder, cmdIdOffset);
		CommandItem.addPayloadType(builder, CommandPayload.CommandPlanetProduce);
		CommandItem.addPayload(builder, inner);
		const item = CommandItem.endCommandItem(builder);
		const commandsVec = UserGamesOrder.createCommandsVector(builder, [item]);
		const [hi, lo] = uuidToHiLo(GAME_ID);
		const gameIdOffset = UUID.createUUID(builder, hi, lo);
		UserGamesOrder.startUserGamesOrder(builder);
		UserGamesOrder.addGameId(builder, gameIdOffset);
		UserGamesOrder.addUpdatedAt(builder, BigInt(13));
		UserGamesOrder.addCommands(builder, commandsVec);
		const orderOffset = UserGamesOrder.endUserGamesOrder(builder);
		UserGamesOrderGetResponse.startUserGamesOrderGetResponse(builder);
		UserGamesOrderGetResponse.addFound(builder, true);
		UserGamesOrderGetResponse.addOrder(builder, orderOffset);
		const offset = UserGamesOrderGetResponse.endUserGamesOrderGetResponse(
			builder,
		);
		builder.finish(offset);
		const responsePayload = builder.asUint8Array();

		const exec = vi.fn(async () => ({
			resultCode: "ok",
			payloadBytes: responsePayload,
		}));
		const result = await fetchOrder(mockClient(exec), GAME_ID, 5);
		expect(result.commands).toHaveLength(1);
		const cmd = result.commands[0]!;
		expect(cmd.kind).toBe("setProductionType");
		if (cmd.kind !== "setProductionType") return;
		expect(cmd.id).toBe("cmd-prod");
		expect(cmd.planetNumber).toBe(17);
		expect(cmd.productionType).toBe("SHIP");
		expect(cmd.subject).toBe("Scout");
		expect(result.updatedAt).toBe(13);
	});

	test("skips a CommandPlanetProduce with PlanetProduction.UNKNOWN", async () => {
		const builder = new Builder(256);
		const cmdIdOffset = builder.createString("cmd-unknown");
		const subjectOffset = builder.createString("");
		const inner = CommandPlanetProduce.createCommandPlanetProduce(
			builder,
			BigInt(0),
			PlanetProduction.UNKNOWN,
			subjectOffset,
		);
		CommandItem.startCommandItem(builder);
		CommandItem.addCmdId(builder, cmdIdOffset);
		CommandItem.addPayloadType(builder, CommandPayload.CommandPlanetProduce);
		CommandItem.addPayload(builder, inner);
		const item = CommandItem.endCommandItem(builder);
		const commandsVec = UserGamesOrder.createCommandsVector(builder, [item]);
		const [hi, lo] = uuidToHiLo(GAME_ID);
		const gameIdOffset = UUID.createUUID(builder, hi, lo);
		UserGamesOrder.startUserGamesOrder(builder);
		UserGamesOrder.addGameId(builder, gameIdOffset);
		UserGamesOrder.addUpdatedAt(builder, BigInt(0));
		UserGamesOrder.addCommands(builder, commandsVec);
		const orderOffset = UserGamesOrder.endUserGamesOrder(builder);
		UserGamesOrderGetResponse.startUserGamesOrderGetResponse(builder);
		UserGamesOrderGetResponse.addFound(builder, true);
		UserGamesOrderGetResponse.addOrder(builder, orderOffset);
		const offset = UserGamesOrderGetResponse.endUserGamesOrderGetResponse(
			builder,
		);
		builder.finish(offset);

		const exec = vi.fn(async () => ({
			resultCode: "ok",
			payloadBytes: builder.asUint8Array(),
		}));
		const result = await fetchOrder(mockClient(exec), GAME_ID, 5);
		expect(result.commands).toEqual([]);
	});

	test("decodes CommandPlanetRouteSet into setCargoRoute", async () => {
		const builder = new Builder(256);
		const cmdIdOffset = builder.createString("cmd-route-set");
		const inner = CommandPlanetRouteSet.createCommandPlanetRouteSet(
			builder,
			BigInt(11),
			BigInt(22),
			PlanetRouteLoadType.MAT,
		);
		CommandItem.startCommandItem(builder);
		CommandItem.addCmdId(builder, cmdIdOffset);
		CommandItem.addPayloadType(builder, CommandPayload.CommandPlanetRouteSet);
		CommandItem.addPayload(builder, inner);
		const item = CommandItem.endCommandItem(builder);
		const commandsVec = UserGamesOrder.createCommandsVector(builder, [item]);
		const [hi, lo] = uuidToHiLo(GAME_ID);
		const gameIdOffset = UUID.createUUID(builder, hi, lo);
		UserGamesOrder.startUserGamesOrder(builder);
		UserGamesOrder.addGameId(builder, gameIdOffset);
		UserGamesOrder.addUpdatedAt(builder, BigInt(7));
		UserGamesOrder.addCommands(builder, commandsVec);
		const orderOffset = UserGamesOrder.endUserGamesOrder(builder);
		UserGamesOrderGetResponse.startUserGamesOrderGetResponse(builder);
		UserGamesOrderGetResponse.addFound(builder, true);
		UserGamesOrderGetResponse.addOrder(builder, orderOffset);
		const offset = UserGamesOrderGetResponse.endUserGamesOrderGetResponse(
			builder,
		);
		builder.finish(offset);

		const exec = vi.fn(async () => ({
			resultCode: "ok",
			payloadBytes: builder.asUint8Array(),
		}));
		const result = await fetchOrder(mockClient(exec), GAME_ID, 5);
		expect(result.commands).toHaveLength(1);
		const cmd = result.commands[0]!;
		expect(cmd.kind).toBe("setCargoRoute");
		if (cmd.kind !== "setCargoRoute") return;
		expect(cmd.id).toBe("cmd-route-set");
		expect(cmd.sourcePlanetNumber).toBe(11);
		expect(cmd.destinationPlanetNumber).toBe(22);
		expect(cmd.loadType).toBe("MAT");
	});

	test("decodes CommandPlanetRouteRemove into removeCargoRoute", async () => {
		const builder = new Builder(256);
		const cmdIdOffset = builder.createString("cmd-route-remove");
		const inner = CommandPlanetRouteRemove.createCommandPlanetRouteRemove(
			builder,
			BigInt(33),
			PlanetRouteLoadType.EMP,
		);
		CommandItem.startCommandItem(builder);
		CommandItem.addCmdId(builder, cmdIdOffset);
		CommandItem.addPayloadType(
			builder,
			CommandPayload.CommandPlanetRouteRemove,
		);
		CommandItem.addPayload(builder, inner);
		const item = CommandItem.endCommandItem(builder);
		const commandsVec = UserGamesOrder.createCommandsVector(builder, [item]);
		const [hi, lo] = uuidToHiLo(GAME_ID);
		const gameIdOffset = UUID.createUUID(builder, hi, lo);
		UserGamesOrder.startUserGamesOrder(builder);
		UserGamesOrder.addGameId(builder, gameIdOffset);
		UserGamesOrder.addUpdatedAt(builder, BigInt(8));
		UserGamesOrder.addCommands(builder, commandsVec);
		const orderOffset = UserGamesOrder.endUserGamesOrder(builder);
		UserGamesOrderGetResponse.startUserGamesOrderGetResponse(builder);
		UserGamesOrderGetResponse.addFound(builder, true);
		UserGamesOrderGetResponse.addOrder(builder, orderOffset);
		const offset = UserGamesOrderGetResponse.endUserGamesOrderGetResponse(
			builder,
		);
		builder.finish(offset);

		const exec = vi.fn(async () => ({
			resultCode: "ok",
			payloadBytes: builder.asUint8Array(),
		}));
		const result = await fetchOrder(mockClient(exec), GAME_ID, 5);
		expect(result.commands).toHaveLength(1);
		const cmd = result.commands[0]!;
		expect(cmd.kind).toBe("removeCargoRoute");
		if (cmd.kind !== "removeCargoRoute") return;
		expect(cmd.sourcePlanetNumber).toBe(33);
		expect(cmd.loadType).toBe("EMP");
	});

	test("skips a CommandPlanetRouteSet with PlanetRouteLoadType.UNKNOWN", async () => {
		const builder = new Builder(256);
		const cmdIdOffset = builder.createString("cmd-unknown-load");
		const inner = CommandPlanetRouteSet.createCommandPlanetRouteSet(
			builder,
			BigInt(1),
			BigInt(2),
			PlanetRouteLoadType.UNKNOWN,
		);
		CommandItem.startCommandItem(builder);
		CommandItem.addCmdId(builder, cmdIdOffset);
		CommandItem.addPayloadType(builder, CommandPayload.CommandPlanetRouteSet);
		CommandItem.addPayload(builder, inner);
		const item = CommandItem.endCommandItem(builder);
		const commandsVec = UserGamesOrder.createCommandsVector(builder, [item]);
		const [hi, lo] = uuidToHiLo(GAME_ID);
		const gameIdOffset = UUID.createUUID(builder, hi, lo);
		UserGamesOrder.startUserGamesOrder(builder);
		UserGamesOrder.addGameId(builder, gameIdOffset);
		UserGamesOrder.addUpdatedAt(builder, BigInt(0));
		UserGamesOrder.addCommands(builder, commandsVec);
		const orderOffset = UserGamesOrder.endUserGamesOrder(builder);
		UserGamesOrderGetResponse.startUserGamesOrderGetResponse(builder);
		UserGamesOrderGetResponse.addFound(builder, true);
		UserGamesOrderGetResponse.addOrder(builder, orderOffset);
		const offset = UserGamesOrderGetResponse.endUserGamesOrderGetResponse(
			builder,
		);
		builder.finish(offset);

		const exec = vi.fn(async () => ({
			resultCode: "ok",
			payloadBytes: builder.asUint8Array(),
		}));
		const result = await fetchOrder(mockClient(exec), GAME_ID, 5);
		expect(result.commands).toEqual([]);
	});

	test("posts a well-formed UserGamesOrderGet payload", async () => {
		let captured: Uint8Array | null = null;
		const exec = vi.fn(async (_messageType, payload: Uint8Array) => {
			captured = payload;
			return {
				resultCode: "ok",
				payloadBytes: buildResponse([], 0, false),
			};
		});
		await fetchOrder(mockClient(exec), GAME_ID, 9);
		expect(captured).not.toBeNull();
		const decoded = UserGamesOrderGet.getRootAsUserGamesOrderGet(
			new (await import("flatbuffers")).ByteBuffer(captured!),
		);
		expect(Number(decoded.turn())).toBe(9);
		const id = decoded.gameId();
		expect(id).not.toBeNull();
	});
});
