// Vitest unit coverage for `api/synthetic-report.ts`. The decoder
// mirrors `pkg/model/report.Report` JSON (as emitted by the Go CLI
// `tools/local-dev/legacy-report/cmd/legacy-report-to-json`) into the
// in-game-shell `GameReport` shape. The tests assert the decoder
// flattens all four planet kinds, looks the local player's tech
// levels up by race name, defaults missing routes to empty, and
// rejects malformed input.

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

import {
	SYNTHETIC_GAME_ID_PREFIX,
	SyntheticReportError,
	getSyntheticReport,
	isSyntheticGameId,
	loadSyntheticReportFromJSON,
} from "../src/api/synthetic-report";
import type { BattleReport } from "../src/api/battle-fetch";
import {
	lookupSyntheticBattle,
	resetSyntheticBattles,
} from "../src/api/synthetic-battle";

function syntheticJSON(extra: Record<string, unknown> = {}): unknown {
	return {
		turn: 39,
		mapWidth: 800,
		mapHeight: 800,
		mapPlanets: 700,
		race: "KnightErrants",
		votes: 16.02,
		voteFor: "KnightErrants",
		player: [
			{
				name: "KnightErrants",
				drive: 13.25,
				weapons: 6.11,
				shields: 7.09,
				cargo: 1,
				population: 16015.04,
				industry: 13668.76,
				planets: 22,
				relation: "-",
				votes: 16.02,
				extinct: false,
			},
			{
				name: "Other",
				drive: 9.5,
				weapons: 4.01,
				shields: 4.69,
				cargo: 1,
				population: 0,
				industry: 0,
				planets: 0,
				relation: "War",
				votes: 0,
				extinct: false,
			},
		],
		localPlanet: [
			{
				number: 17,
				name: "Castle",
				x: 171.05,
				y: 700.24,
				size: 1000,
				population: 1000,
				industry: 1000,
				resources: 10,
				production: "Drive_Research",
				capital: 0,
				material: 0.68,
				colonists: 88.78,
				freeIndustry: 1000,
			},
		],
		otherPlanet: [
			{
				owner: "Monstrai",
				number: 12,
				name: "Skarabei",
				x: 303.84,
				y: 579.23,
				size: 500,
				population: 500,
				industry: 500,
				resources: 10,
				production: "Capital",
				capital: 0,
				material: 70.99,
				colonists: 20.03,
				freeIndustry: 341.78,
			},
		],
		uninhabitedPlanet: [
			{
				number: 9,
				name: "Dw2",
				x: 117.87,
				y: 795.21,
				size: 500,
				resources: 10,
				capital: 0,
				material: 500,
			},
		],
		unidentifiedPlanet: [
			{ number: 0, x: 738.08, y: 600.26 },
			{ number: 1, x: 579.12, y: 489.37 },
		],
		localShipClass: [
			{
				name: "Frontier",
				drive: 11.37,
				armament: 0,
				weapons: 0,
				shields: 0,
				cargo: 1,
				mass: 12.37,
			},
		],
		...extra,
	};
}

describe("loadSyntheticReportFromJSON", () => {
	test("flattens all four planet kinds with kind-specific nullables", () => {
		const { gameId, report } = loadSyntheticReportFromJSON(syntheticJSON());

		expect(isSyntheticGameId(gameId)).toBe(true);
		expect(gameId.startsWith(SYNTHETIC_GAME_ID_PREFIX)).toBe(true);

		expect(report.turn).toBe(39);
		expect(report.mapWidth).toBe(800);
		expect(report.mapHeight).toBe(800);
		expect(report.planetCount).toBe(700);
		expect(report.race).toBe("KnightErrants");

		expect(report.planets).toHaveLength(5);

		const local = report.planets.find((p) => p.kind === "local")!;
		expect(local.name).toBe("Castle");
		expect(local.industryStockpile).toBe(0);
		expect(local.materialsStockpile).toBe(0.68);
		expect(local.industry).toBe(1000);
		expect(local.production).toBe("Drive_Research");

		const other = report.planets.find((p) => p.kind === "other")!;
		expect(other.owner).toBe("Monstrai");
		expect(other.name).toBe("Skarabei");

		const uninhab = report.planets.find((p) => p.kind === "uninhabited")!;
		expect(uninhab.name).toBe("Dw2");
		// Uninhabited planets carry size/resources/stockpiles but no
		// industry / population / production.
		expect(uninhab.size).toBe(500);
		expect(uninhab.industry).toBeNull();
		expect(uninhab.population).toBeNull();
		expect(uninhab.production).toBeNull();

		const unident = report.planets.filter((p) => p.kind === "unidentified");
		expect(unident).toHaveLength(2);
		expect(unident[0]!.name).toBe("");
		expect(unident[0]!.size).toBeNull();
	});

	test("derives local player tech from the matching player row", () => {
		const { report } = loadSyntheticReportFromJSON(syntheticJSON());
		expect(report.localPlayerDrive).toBe(13.25);
		expect(report.localPlayerWeapons).toBe(6.11);
		expect(report.localPlayerShields).toBe(7.09);
		expect(report.localPlayerCargo).toBe(1);
	});

	test("returns zeros when the local race row is missing", () => {
		const { report } = loadSyntheticReportFromJSON(
			syntheticJSON({ race: "GhostRace" }),
		);
		expect(report.localPlayerDrive).toBe(0);
		expect(report.localPlayerWeapons).toBe(0);
		expect(report.localPlayerShields).toBe(0);
		expect(report.localPlayerCargo).toBe(0);
	});

	test("emits empty routes (legacy format has no routes section)", () => {
		const { report } = loadSyntheticReportFromJSON(syntheticJSON());
		expect(report.routes).toEqual([]);
	});

	test("registers the report under the returned game id", () => {
		const { gameId, report } = loadSyntheticReportFromJSON(syntheticJSON());
		expect(getSyntheticReport(gameId)).toBe(report);
	});

	test("two loads produce distinct ids", () => {
		const a = loadSyntheticReportFromJSON(syntheticJSON());
		const b = loadSyntheticReportFromJSON(syntheticJSON());
		expect(a.gameId).not.toBe(b.gameId);
	});

	test("rejects non-object input", () => {
		expect(() => loadSyntheticReportFromJSON(null)).toThrow(
			SyntheticReportError,
		);
		expect(() => loadSyntheticReportFromJSON(42)).toThrow(
			SyntheticReportError,
		);
		expect(() => loadSyntheticReportFromJSON("a string")).toThrow(
			SyntheticReportError,
		);
	});

	test("ship classes survive with truncated armament", () => {
		const { report } = loadSyntheticReportFromJSON(
			syntheticJSON({
				localShipClass: [
					{
						name: "Bow105",
						drive: 74.77,
						armament: 105,
						weapons: 1,
						shields: 19.72,
						cargo: 1,
						mass: 148.49,
					},
				],
			}),
		);
		expect(report.localShipClass).toHaveLength(1);
		expect(report.localShipClass[0]!.name).toBe("Bow105");
		expect(report.localShipClass[0]!.armament).toBe(105);
	});
});

describe("isSyntheticGameId", () => {
	test("recognises the synthetic prefix", () => {
		expect(isSyntheticGameId("synthetic-abc")).toBe(true);
		expect(isSyntheticGameId("00000000-0000-0000-0000-000000000000")).toBe(
			false,
		);
		expect(isSyntheticGameId("")).toBe(false);
	});
});

describe("getSyntheticReport", () => {
	test("returns undefined for unknown ids", () => {
		expect(getSyntheticReport("synthetic-missing")).toBeUndefined();
	});
});

describe("envelope shape (v1)", () => {
	test("forwards battles to the synthetic-battle registry", () => {
		resetSyntheticBattles();
		const battle: BattleReport = {
			id: "11111111-1111-1111-1111-111111111111",
			planet: 17,
			planetName: "Castle",
			races: { "0": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" },
			ships: {
				"0": {
					race: "KnightErrants",
					className: "Drone",
					tech: { DRIVE: 1 },
					num: 1,
					numLeft: 0,
					loadType: "",
					loadQuantity: 0,
					inBattle: true,
				},
			},
			protocol: [],
		};
		const envelope = {
			version: 1,
			report: syntheticJSON(),
			battles: { [battle.id]: battle },
		};

		const { gameId, report } = loadSyntheticReportFromJSON(envelope);
		expect(gameId.startsWith(SYNTHETIC_GAME_ID_PREFIX)).toBe(true);
		expect(report.turn).toBe(39);
		expect(lookupSyntheticBattle(battle.id)).toEqual(battle);
	});

	test("missing battles field leaves the registry untouched", () => {
		resetSyntheticBattles();
		const envelope = {
			version: 1,
			report: syntheticJSON(),
		};
		loadSyntheticReportFromJSON(envelope);
		expect(lookupSyntheticBattle("any")).toBeNull();
	});

	test("bare Report (no envelope) still loads — backward compat", () => {
		resetSyntheticBattles();
		const { report } = loadSyntheticReportFromJSON(syntheticJSON());
		expect(report.turn).toBe(39);
		expect(lookupSyntheticBattle("any")).toBeNull();
	});
});
