// Vitest round-trip coverage for the eight Phase 20 ship-group
// command shapes. The encoder lives in `sync/submit.ts`; the
// decoder lives in `sync/order-load.ts`. We capture the request
// bytes the encoder produces, re-emit them inside a
// `UserGamesOrderGetResponse` envelope, and feed that to
// `fetchOrder`. The decoded command must match the original — any
// drift between encoder and decoder fails here first.

import { Builder, ByteBuffer } 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,
	CommandShipGroupBreak,
	CommandShipGroupDismantle,
	CommandShipGroupJoinFleet,
	CommandShipGroupLoad,
	CommandShipGroupSend,
	CommandShipGroupTransfer,
	CommandShipGroupUnload,
	CommandShipGroupUpgrade,
	UserGamesOrder,
	UserGamesOrderGetResponse,
	UserGamesOrderResponse,
} from "../src/proto/galaxy/fbs/order";
import { fetchOrder } from "../src/sync/order-load";
import { submitOrder } from "../src/sync/submit";
import type { OrderCommand } from "../src/sync/order-types";

const GAME_ID = "11111111-2222-3333-4444-555555555555";
const GROUP_ID = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";

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

// captureRequestBytes runs submitOrder against a mock that records
// the outgoing payload, then returns those bytes (which are a valid
// `UserGamesOrder` envelope).
async function captureRequestBytes(cmds: OrderCommand[]): Promise<Uint8Array> {
	let captured: Uint8Array | null = null;
	const exec = vi.fn(async (_msg: string, payload: Uint8Array) => {
		captured = payload;
		const builder = new Builder(64);
		const [hi, lo] = uuidToHiLo(GAME_ID);
		const gameIdOffset = UUID.createUUID(builder, hi, lo);
		UserGamesOrderResponse.startUserGamesOrderResponse(builder);
		UserGamesOrderResponse.addGameId(builder, gameIdOffset);
		UserGamesOrderResponse.addUpdatedAt(builder, BigInt(0));
		const offset = UserGamesOrderResponse.endUserGamesOrderResponse(builder);
		builder.finish(offset);
		return { resultCode: "ok", payloadBytes: builder.asUint8Array() };
	});
	const result = await submitOrder(mockClient(exec), GAME_ID, cmds);
	expect(result.ok).toBe(true);
	expect(captured).not.toBeNull();
	return captured!;
}

// wrapAsGetResponse rebuilds the captured `UserGamesOrder` inside a
// `UserGamesOrderGetResponse` envelope by walking each
// `CommandItem`, copying its identity fields, and re-packing each
// payload through `unpack().pack(builder)` — the FBS-generated
// helper that round-trips a typed table into a fresh builder.
function wrapAsGetResponse(orderBytes: Uint8Array): Uint8Array {
	const order = UserGamesOrder.getRootAsUserGamesOrder(
		new ByteBuffer(orderBytes),
	);
	const builder = new Builder(256);
	const itemOffsets: number[] = [];
	for (let i = 0; i < order.commandsLength(); i++) {
		const item = order.commands(i);
		if (item === null) continue;
		const cmdIdOffset = builder.createString(item.cmdId() ?? "");
		const payloadType = item.payloadType();
		const payloadOffset = packPayload(builder, item, payloadType);
		CommandItem.startCommandItem(builder);
		CommandItem.addCmdId(builder, cmdIdOffset);
		CommandItem.addPayloadType(builder, payloadType);
		CommandItem.addPayload(builder, payloadOffset);
		itemOffsets.push(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, order.updatedAt());
	UserGamesOrder.addCommands(builder, commandsVec);
	const orderOffset = UserGamesOrder.endUserGamesOrder(builder);

	UserGamesOrderGetResponse.startUserGamesOrderGetResponse(builder);
	UserGamesOrderGetResponse.addFound(builder, true);
	UserGamesOrderGetResponse.addOrder(builder, orderOffset);
	const resOffset =
		UserGamesOrderGetResponse.endUserGamesOrderGetResponse(builder);
	builder.finish(resOffset);
	return builder.asUint8Array();
}

function packPayload(
	builder: Builder,
	item: NonNullable<ReturnType<UserGamesOrder["commands"]>>,
	payloadType: CommandPayload,
): number {
	switch (payloadType) {
		case CommandPayload.CommandShipGroupBreak: {
			const inner = new CommandShipGroupBreak();
			item.payload(inner);
			return inner.unpack().pack(builder);
		}
		case CommandPayload.CommandShipGroupSend: {
			const inner = new CommandShipGroupSend();
			item.payload(inner);
			return inner.unpack().pack(builder);
		}
		case CommandPayload.CommandShipGroupLoad: {
			const inner = new CommandShipGroupLoad();
			item.payload(inner);
			return inner.unpack().pack(builder);
		}
		case CommandPayload.CommandShipGroupUnload: {
			const inner = new CommandShipGroupUnload();
			item.payload(inner);
			return inner.unpack().pack(builder);
		}
		case CommandPayload.CommandShipGroupUpgrade: {
			const inner = new CommandShipGroupUpgrade();
			item.payload(inner);
			return inner.unpack().pack(builder);
		}
		case CommandPayload.CommandShipGroupDismantle: {
			const inner = new CommandShipGroupDismantle();
			item.payload(inner);
			return inner.unpack().pack(builder);
		}
		case CommandPayload.CommandShipGroupTransfer: {
			const inner = new CommandShipGroupTransfer();
			item.payload(inner);
			return inner.unpack().pack(builder);
		}
		case CommandPayload.CommandShipGroupJoinFleet: {
			const inner = new CommandShipGroupJoinFleet();
			item.payload(inner);
			return inner.unpack().pack(builder);
		}
		default:
			throw new Error(`unsupported payload type ${payloadType}`);
	}
}

async function roundTrip(cmd: OrderCommand): Promise<OrderCommand> {
	const requestBytes = await captureRequestBytes([cmd]);
	const responseBytes = wrapAsGetResponse(requestBytes);
	const exec = vi.fn(async () => ({
		resultCode: "ok",
		payloadBytes: responseBytes,
	}));
	const result = await fetchOrder(mockClient(exec), GAME_ID, 0);
	expect(result.commands).toHaveLength(1);
	return result.commands[0]!;
}

describe("submit + order-load round-trip — ship-group commands", () => {
	test("breakShipGroup", async () => {
		const cmd: OrderCommand = {
			kind: "breakShipGroup",
			id: crypto.randomUUID(),
			groupId: GROUP_ID,
			newGroupId: "11112222-3333-4444-5555-666677778888",
			quantity: 3,
		};
		expect(await roundTrip(cmd)).toEqual(cmd);
	});

	test("sendShipGroup", async () => {
		const cmd: OrderCommand = {
			kind: "sendShipGroup",
			id: crypto.randomUUID(),
			groupId: GROUP_ID,
			destinationPlanetNumber: 42,
		};
		expect(await roundTrip(cmd)).toEqual(cmd);
	});

	test("loadShipGroup", async () => {
		const cmd: OrderCommand = {
			kind: "loadShipGroup",
			id: crypto.randomUUID(),
			groupId: GROUP_ID,
			cargo: "MAT",
			quantity: 12.5,
		};
		expect(await roundTrip(cmd)).toEqual(cmd);
	});

	test("unloadShipGroup", async () => {
		const cmd: OrderCommand = {
			kind: "unloadShipGroup",
			id: crypto.randomUUID(),
			groupId: GROUP_ID,
			quantity: 6.5,
		};
		expect(await roundTrip(cmd)).toEqual(cmd);
	});

	test("upgradeShipGroup ALL", async () => {
		const cmd: OrderCommand = {
			kind: "upgradeShipGroup",
			id: crypto.randomUUID(),
			groupId: GROUP_ID,
			tech: "ALL",
			level: 0,
		};
		expect(await roundTrip(cmd)).toEqual(cmd);
	});

	test("upgradeShipGroup DRIVE level 1.5", async () => {
		const cmd: OrderCommand = {
			kind: "upgradeShipGroup",
			id: crypto.randomUUID(),
			groupId: GROUP_ID,
			tech: "DRIVE",
			level: 1.5,
		};
		expect(await roundTrip(cmd)).toEqual(cmd);
	});

	test("dismantleShipGroup", async () => {
		const cmd: OrderCommand = {
			kind: "dismantleShipGroup",
			id: crypto.randomUUID(),
			groupId: GROUP_ID,
		};
		expect(await roundTrip(cmd)).toEqual(cmd);
	});

	test("transferShipGroup", async () => {
		const cmd: OrderCommand = {
			kind: "transferShipGroup",
			id: crypto.randomUUID(),
			groupId: GROUP_ID,
			acceptor: "Aliens",
		};
		expect(await roundTrip(cmd)).toEqual(cmd);
	});

	test("joinFleetShipGroup", async () => {
		const cmd: OrderCommand = {
			kind: "joinFleetShipGroup",
			id: crypto.randomUUID(),
			groupId: GROUP_ID,
			name: "Vanguard",
		};
		expect(await roundTrip(cmd)).toEqual(cmd);
	});
});
