// Vitest unit coverage for `sync/submit.ts`. Drives the submit
// pipeline against a stub `GalaxyClient` whose `executeCommand`
// hand-builds FBS responses, so the parser is exercised against
// payloads identical to what the real gateway returns.

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,
	CommandPlanetProduce,
	CommandPlanetRename,
	CommandPlanetRouteRemove,
	CommandPlanetRouteSet,
	CommandPayload,
	PlanetProduction,
	PlanetRouteLoadType,
	UserGamesOrder,
	UserGamesOrderResponse,
} from "../src/proto/galaxy/fbs/order";
import { submitOrder } from "../src/sync/submit";
import type {
	CargoLoadType,
	OrderCommand,
	ProductionType,
} from "../src/sync/order-types";

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; applied: boolean | null; errorCode: number | null }[],
	updatedAt: number,
): Uint8Array {
	const builder = new Builder(256);
	const itemOffsets = commands.map((c) => {
		const cmdIdOffset = builder.createString(c.id);
		const nameOffset = builder.createString("ignored");
		const payloadOffset = CommandPlanetRename.createCommandPlanetRename(
			builder,
			BigInt(0),
			nameOffset,
		);
		CommandItem.startCommandItem(builder);
		CommandItem.addCmdId(builder, cmdIdOffset);
		if (c.applied !== null) CommandItem.addCmdApplied(builder, c.applied);
		if (c.errorCode !== null) {
			CommandItem.addCmdErrorCode(builder, BigInt(c.errorCode));
		}
		CommandItem.addPayloadType(builder, CommandPayload.CommandPlanetRename);
		CommandItem.addPayload(builder, payloadOffset);
		return CommandItem.endCommandItem(builder);
	});
	const commandsVec = UserGamesOrderResponse.createCommandsVector(builder, itemOffsets);
	const [hi, lo] = uuidToHiLo(GAME_ID);
	const gameIdOffset = UUID.createUUID(builder, hi, lo);
	UserGamesOrderResponse.startUserGamesOrderResponse(builder);
	UserGamesOrderResponse.addGameId(builder, gameIdOffset);
	UserGamesOrderResponse.addUpdatedAt(builder, BigInt(updatedAt));
	UserGamesOrderResponse.addCommands(builder, commandsVec);
	const offset = UserGamesOrderResponse.endUserGamesOrderResponse(builder);
	builder.finish(offset);
	return builder.asUint8Array();
}

const sampleRename: OrderCommand = {
	kind: "planetRename",
	id: "00000000-0000-0000-0000-00000000aaaa",
	planetNumber: 7,
	name: "Earth",
};

describe("submitOrder", () => {
	test("decodes per-command results from a populated response", async () => {
		const responsePayload = buildResponse(
			[{ id: sampleRename.id, applied: true, errorCode: null }],
			99,
		);
		const exec = vi.fn(async () => ({
			resultCode: "ok",
			payloadBytes: responsePayload,
		}));
		const result = await submitOrder(mockClient(exec), GAME_ID, [sampleRename]);

		expect(exec).toHaveBeenCalledOnce();
		expect(result.ok).toBe(true);
		if (!result.ok) return;
		expect(result.results.get(sampleRename.id)).toBe("applied");
		expect(result.errorCodes.get(sampleRename.id)).toBeNull();
		expect(result.updatedAt).toBe(99);
	});

	test("falls back to batch-level applied when commands array is empty", async () => {
		// Hand-craft an envelope without `commands` to mimic the legacy
		// gateway behaviour (or a 204 wrapped via the fallback path).
		const builder = new Builder(64);
		UserGamesOrderResponse.startUserGamesOrderResponse(builder);
		const offset = UserGamesOrderResponse.endUserGamesOrderResponse(builder);
		builder.finish(offset);
		const exec = vi.fn(async () => ({
			resultCode: "ok",
			payloadBytes: builder.asUint8Array(),
		}));

		const result = await submitOrder(mockClient(exec), GAME_ID, [sampleRename]);

		expect(result.ok).toBe(true);
		if (!result.ok) return;
		expect(result.results.get(sampleRename.id)).toBe("applied");
		expect(result.errorCodes.get(sampleRename.id)).toBeNull();
	});

	test("surfaces mixed applied / rejected entries by cmd id", async () => {
		const second: OrderCommand = {
			kind: "planetRename",
			id: "00000000-0000-0000-0000-00000000bbbb",
			planetNumber: 8,
			name: "Mars",
		};
		const responsePayload = buildResponse(
			[
				{ id: sampleRename.id, applied: true, errorCode: null },
				{ id: second.id, applied: false, errorCode: 42 },
			],
			120,
		);
		const exec = vi.fn(async () => ({
			resultCode: "ok",
			payloadBytes: responsePayload,
		}));

		const result = await submitOrder(mockClient(exec), GAME_ID, [sampleRename, second]);
		expect(result.ok).toBe(true);
		if (!result.ok) return;
		expect(result.results.get(sampleRename.id)).toBe("applied");
		expect(result.errorCodes.get(sampleRename.id)).toBeNull();
		expect(result.results.get(second.id)).toBe("rejected");
		expect(result.errorCodes.get(second.id)).toBe(42);
	});

	test("returns SubmitFailure on non-ok resultCode without throwing", async () => {
		const exec = vi.fn(async () => ({
			resultCode: "invalid_request",
			payloadBytes: new TextEncoder().encode(
				JSON.stringify({ code: "validation_failed", message: "bad name" }),
			),
		}));

		const result = await submitOrder(mockClient(exec), GAME_ID, [sampleRename]);

		expect(result.ok).toBe(false);
		if (result.ok) return;
		expect(result.resultCode).toBe("invalid_request");
		expect(result.code).toBe("validation_failed");
		expect(result.message).toBe("bad name");
	});

	test("posts a well-formed UserGamesOrder payload", async () => {
		let captured: Uint8Array | null = null;
		const exec = vi.fn(async (_messageType, payload: Uint8Array) => {
			captured = payload;
			return { resultCode: "ok", payloadBytes: new Uint8Array() };
		});
		await submitOrder(mockClient(exec), GAME_ID, [sampleRename]);
		expect(captured).not.toBeNull();
		const decoded = UserGamesOrder.getRootAsUserGamesOrder(
			new (await import("flatbuffers")).ByteBuffer(captured!),
		);
		expect(decoded.commandsLength()).toBe(1);
		const item = decoded.commands(0);
		expect(item).not.toBeNull();
		expect(item!.cmdId()).toBe(sampleRename.id);
		expect(item!.payloadType()).toBe(CommandPayload.CommandPlanetRename);
		const inner = new CommandPlanetRename();
		item!.payload(inner);
		expect(Number(inner.number())).toBe(7);
		expect(inner.name()).toBe("Earth");
	});

	test("encodes setProductionType as CommandPlanetProduce on the wire", async () => {
		let captured: Uint8Array | null = null;
		const exec = vi.fn(async (_messageType, payload: Uint8Array) => {
			captured = payload;
			return { resultCode: "ok", payloadBytes: new Uint8Array() };
		});
		const cmd: OrderCommand = {
			kind: "setProductionType",
			id: "00000000-0000-0000-0000-00000000cccc",
			planetNumber: 17,
			productionType: "SHIP",
			subject: "Scout",
		};
		await submitOrder(mockClient(exec), GAME_ID, [cmd]);
		expect(captured).not.toBeNull();
		const decoded = UserGamesOrder.getRootAsUserGamesOrder(
			new (await import("flatbuffers")).ByteBuffer(captured!),
		);
		expect(decoded.commandsLength()).toBe(1);
		const item = decoded.commands(0);
		expect(item).not.toBeNull();
		expect(item!.cmdId()).toBe(cmd.id);
		expect(item!.payloadType()).toBe(CommandPayload.CommandPlanetProduce);
		const inner = new CommandPlanetProduce();
		item!.payload(inner);
		expect(Number(inner.number())).toBe(17);
		expect(inner.production()).toBe(PlanetProduction.SHIP);
		expect(inner.subject()).toBe("Scout");
	});

	test("encodes setCargoRoute as CommandPlanetRouteSet on the wire", async () => {
		let captured: Uint8Array | null = null;
		const exec = vi.fn(async (_messageType, payload: Uint8Array) => {
			captured = payload;
			return { resultCode: "ok", payloadBytes: new Uint8Array() };
		});
		const cmd: OrderCommand = {
			kind: "setCargoRoute",
			id: "00000000-0000-0000-0000-00000000aaaa",
			sourcePlanetNumber: 17,
			destinationPlanetNumber: 23,
			loadType: "COL",
		};
		await submitOrder(mockClient(exec), GAME_ID, [cmd]);
		expect(captured).not.toBeNull();
		const decoded = UserGamesOrder.getRootAsUserGamesOrder(
			new (await import("flatbuffers")).ByteBuffer(captured!),
		);
		const item = decoded.commands(0);
		expect(item).not.toBeNull();
		expect(item!.payloadType()).toBe(CommandPayload.CommandPlanetRouteSet);
		const inner = new CommandPlanetRouteSet();
		item!.payload(inner);
		expect(Number(inner.origin())).toBe(17);
		expect(Number(inner.destination())).toBe(23);
		expect(inner.loadType()).toBe(PlanetRouteLoadType.COL);
	});

	test("encodes removeCargoRoute as CommandPlanetRouteRemove on the wire", async () => {
		let captured: Uint8Array | null = null;
		const exec = vi.fn(async (_messageType, payload: Uint8Array) => {
			captured = payload;
			return { resultCode: "ok", payloadBytes: new Uint8Array() };
		});
		const cmd: OrderCommand = {
			kind: "removeCargoRoute",
			id: "00000000-0000-0000-0000-00000000bbbb",
			sourcePlanetNumber: 17,
			loadType: "MAT",
		};
		await submitOrder(mockClient(exec), GAME_ID, [cmd]);
		const decoded = UserGamesOrder.getRootAsUserGamesOrder(
			new (await import("flatbuffers")).ByteBuffer(captured!),
		);
		const item = decoded.commands(0);
		expect(item!.payloadType()).toBe(CommandPayload.CommandPlanetRouteRemove);
		const inner = new CommandPlanetRouteRemove();
		item!.payload(inner);
		expect(Number(inner.origin())).toBe(17);
		expect(inner.loadType()).toBe(PlanetRouteLoadType.MAT);
	});

	test("maps every cargoLoadType literal to its FBS enum value", async () => {
		const cases: Array<{ loadType: CargoLoadType; fbs: PlanetRouteLoadType }> = [
			{ loadType: "COL", fbs: PlanetRouteLoadType.COL },
			{ loadType: "CAP", fbs: PlanetRouteLoadType.CAP },
			{ loadType: "MAT", fbs: PlanetRouteLoadType.MAT },
			{ loadType: "EMP", fbs: PlanetRouteLoadType.EMP },
		];
		for (const tc of cases) {
			let captured: Uint8Array | null = null;
			const exec = vi.fn(async (_messageType, payload: Uint8Array) => {
				captured = payload;
				return { resultCode: "ok", payloadBytes: new Uint8Array() };
			});
			const cmd: OrderCommand = {
				kind: "setCargoRoute",
				id: `id-${tc.loadType}`,
				sourcePlanetNumber: 5,
				destinationPlanetNumber: 6,
				loadType: tc.loadType,
			};
			await submitOrder(mockClient(exec), GAME_ID, [cmd]);
			const decoded = UserGamesOrder.getRootAsUserGamesOrder(
				new (await import("flatbuffers")).ByteBuffer(captured!),
			);
			const inner = new CommandPlanetRouteSet();
			decoded.commands(0)!.payload(inner);
			expect(inner.loadType()).toBe(tc.fbs);
		}
	});

	test("maps every productionType literal to its FBS enum value", async () => {
		const cases: Array<{
			productionType: ProductionType;
			fbs: PlanetProduction;
			subject: string;
		}> = [
			{ productionType: "MAT", fbs: PlanetProduction.MAT, subject: "" },
			{ productionType: "CAP", fbs: PlanetProduction.CAP, subject: "" },
			{ productionType: "DRIVE", fbs: PlanetProduction.DRIVE, subject: "" },
			{
				productionType: "WEAPONS",
				fbs: PlanetProduction.WEAPONS,
				subject: "",
			},
			{
				productionType: "SHIELDS",
				fbs: PlanetProduction.SHIELDS,
				subject: "",
			},
			{ productionType: "CARGO", fbs: PlanetProduction.CARGO, subject: "" },
			{
				productionType: "SCIENCE",
				fbs: PlanetProduction.SCIENCE,
				subject: "AlphaSci",
			},
			{
				productionType: "SHIP",
				fbs: PlanetProduction.SHIP,
				subject: "Scout",
			},
		];
		for (const tc of cases) {
			let captured: Uint8Array | null = null;
			const exec = vi.fn(async (_messageType, payload: Uint8Array) => {
				captured = payload;
				return { resultCode: "ok", payloadBytes: new Uint8Array() };
			});
			const cmd: OrderCommand = {
				kind: "setProductionType",
				id: `id-${tc.productionType}`,
				planetNumber: 5,
				productionType: tc.productionType,
				subject: tc.subject,
			};
			await submitOrder(mockClient(exec), GAME_ID, [cmd]);
			expect(captured).not.toBeNull();
			const decoded = UserGamesOrder.getRootAsUserGamesOrder(
				new (await import("flatbuffers")).ByteBuffer(captured!),
			);
			const inner = new CommandPlanetProduce();
			decoded.commands(0)!.payload(inner);
			expect(inner.production()).toBe(tc.fbs);
			expect(inner.subject()).toBe(tc.subject);
		}
	});
});
