// Vitest component coverage for the Phase 16 cargo-routes
// subsection of the planet inspector. Drives the component against
// a real `OrderDraftStore` (with `fake-indexeddb` standing in for
// the browser IDB factory) and a stub `MapPickService` whose
// `pick(...)` resolves to a script-controlled answer. The tests
// assert the four-slot rendering, the picker invocation, the
// per-(source, loadType) collapse rule, and the cancel path.

import "@testing-library/jest-dom/vitest";
import "fake-indexeddb/auto";
import { fireEvent, render, waitFor } from "@testing-library/svelte";
import { afterEach, beforeEach, describe, expect, test } from "vitest";

import { i18n } from "../src/lib/i18n/index.svelte";
import type { ReportPlanet, ReportRoute } from "../src/api/game-state";
import CargoRoutes from "../src/lib/inspectors/planet/cargo-routes.svelte";
import {
	MAP_PICK_CONTEXT_KEY,
	MapPickService,
	type MapPickRequest,
} from "../src/lib/map-pick.svelte";
import {
	ORDER_DRAFT_CONTEXT_KEY,
	OrderDraftStore,
} from "../src/sync/order-draft.svelte";
import { IDBCache } from "../src/platform/store/idb-cache";
import { openGalaxyDB, type GalaxyDB } from "../src/platform/store/idb";
import type { Cache } from "../src/platform/store/index";
import type { IDBPDatabase } from "idb";

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

let db: IDBPDatabase<GalaxyDB>;
let dbName: string;
let cache: Cache;
let draft: OrderDraftStore;

beforeEach(async () => {
	dbName = `galaxy-cargo-${crypto.randomUUID()}`;
	db = await openGalaxyDB(dbName);
	cache = new IDBCache(db);
	draft = new OrderDraftStore();
	await draft.init({ cache, gameId: GAME_ID });
	i18n.resetForTests("en");
});

afterEach(async () => {
	draft.dispose();
	db.close();
	await new Promise<void>((resolve) => {
		const req = indexedDB.deleteDatabase(dbName);
		req.onsuccess = () => resolve();
		req.onerror = () => resolve();
		req.onblocked = () => resolve();
	});
});

function makePlanet(
	overrides: Partial<ReportPlanet> & Pick<ReportPlanet, "number">,
): ReportPlanet {
	return {
		name: `Planet-${overrides.number}`,
		x: 0,
		y: 0,
		kind: "local",
		owner: null,
		size: 100,
		resources: 1,
		industryStockpile: 0,
		materialsStockpile: 0,
		industry: 0,
		population: 0,
		colonists: 0,
		production: null,
		freeIndustry: 0,
		...overrides,
	};
}

interface PickInvocation {
	request: MapPickRequest;
	resolve: (id: number | null) => void;
}

class StubPickService extends MapPickService {
	invocations: PickInvocation[] = [];
	override pick(request: MapPickRequest): Promise<number | null> {
		this.active = true;
		return new Promise((resolve) => {
			this.invocations.push({
				request,
				resolve: (id) => {
					this.active = false;
					resolve(id);
				},
			});
		});
	}
	override cancel(): void {
		const inv = this.invocations.shift();
		inv?.resolve(null);
	}
}

function mount(
	planet: ReportPlanet,
	planets: ReportPlanet[],
	routes: ReportRoute[] = [],
	localPlayerDrive = 2,
	mapWidth = 4000,
	mapHeight = 4000,
) {
	const pick = new StubPickService();
	const context = new Map<unknown, unknown>([
		[ORDER_DRAFT_CONTEXT_KEY, draft],
		[MAP_PICK_CONTEXT_KEY, pick],
	]);
	const ui = render(CargoRoutes, {
		props: {
			planet,
			routes,
			planets,
			mapWidth,
			mapHeight,
			localPlayerDrive,
		},
		context,
	});
	return { ui, pick };
}

describe("planet inspector — cargo routes", () => {
	test("renders four slots in COL/CAP/MAT/EMP order", () => {
		const { ui, pick } = mount(
			makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }),
			[makePlanet({ number: 1, name: "Earth", x: 100, y: 100 })],
		);
		const slots = ui.container.querySelectorAll(
			"[data-testid^='inspector-planet-cargo-slot-']",
		);
		const slotIds = Array.from(slots).map((el) =>
			el.getAttribute("data-testid"),
		);
		// Each slot generates several test ids (label + body items);
		// pick the row data-testid (slot itself, no suffix).
		const rowIds = slotIds.filter((id) =>
			/^inspector-planet-cargo-slot-(col|cap|mat|emp)$/.test(id ?? ""),
		);
		expect(rowIds).toEqual([
			"inspector-planet-cargo-slot-col",
			"inspector-planet-cargo-slot-cap",
			"inspector-planet-cargo-slot-mat",
			"inspector-planet-cargo-slot-emp",
		]);
	});

	test("an empty slot exposes the Add button and the (no route) marker", () => {
		const { ui, pick } = mount(
			makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }),
			[makePlanet({ number: 1, name: "Earth", x: 100, y: 100 })],
		);
		expect(
			ui.getByTestId("inspector-planet-cargo-slot-col-empty"),
		).toBeInTheDocument();
		expect(
			ui.getByTestId("inspector-planet-cargo-slot-col-add"),
		).toBeInTheDocument();
		expect(
			ui.queryByTestId("inspector-planet-cargo-slot-col-edit"),
		).toBeNull();
	});

	test("a filled slot shows the destination name plus Edit and Remove", () => {
		const { ui, pick } = mount(
			makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }),
			[
				makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }),
				makePlanet({ number: 2, name: "Mars", x: 150, y: 100 }),
			],
			[
				{
					sourcePlanetNumber: 1,
					entries: [{ loadType: "COL", destinationPlanetNumber: 2 }],
				},
			],
		);
		expect(
			ui.getByTestId("inspector-planet-cargo-slot-col-destination"),
		).toHaveTextContent("Mars");
		expect(
			ui.getByTestId("inspector-planet-cargo-slot-col-edit"),
		).toBeInTheDocument();
		expect(
			ui.getByTestId("inspector-planet-cargo-slot-col-remove"),
		).toBeInTheDocument();
		expect(
			ui.queryByTestId("inspector-planet-cargo-slot-col-add"),
		).toBeNull();
	});

	test("Add opens pick mode with the reach-filtered set", async () => {
		// Reach = 40 * 2 = 80. Mars is 50 away (in reach), Pluto is
		// 200 away (out of reach).
		const { ui, pick } = mount(
			makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }),
			[
				makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }),
				makePlanet({ number: 2, name: "Mars", x: 150, y: 100 }),
				makePlanet({ number: 3, name: "Pluto", x: 300, y: 100 }),
			],
			[],
			2,
		);
		await fireEvent.click(ui.getByTestId("inspector-planet-cargo-slot-col-add"));
		await waitFor(() => expect(pick.invocations.length).toBe(1));
		const invocation = pick.invocations[0]!;
		expect(invocation.request.sourcePlanetNumber).toBe(1);
		expect(Array.from(invocation.request.reachableIds).sort()).toEqual([2]);
		expect(
			ui.getByTestId("inspector-planet-cargo-pick-prompt"),
		).toBeInTheDocument();
	});

	test("the reachable set spans every planet kind in range, not only own", async () => {
		// Reach = 40 * 1.5 = 60. Each candidate at distance 50 — in
		// reach. The picker must include the foreign-race planet,
		// the uninhabited rock, and the unidentified target so the
		// engine's "destinations may be any planet" rule is honoured
		// (route.go: only the source's ownership is enforced).
		const { ui, pick } = mount(
			makePlanet({
				number: 1,
				name: "Earth",
				x: 100,
				y: 100,
				kind: "local",
			}),
			[
				makePlanet({
					number: 1,
					name: "Earth",
					x: 100,
					y: 100,
					kind: "local",
				}),
				makePlanet({
					number: 2,
					name: "Alpha",
					x: 150,
					y: 100,
					kind: "other",
					owner: "Aliens",
				}),
				makePlanet({
					number: 3,
					name: "Rock",
					x: 100,
					y: 150,
					kind: "uninhabited",
				}),
				makePlanet({
					number: 4,
					name: "",
					x: 50,
					y: 100,
					kind: "unidentified",
				}),
			],
			[],
			1.5,
		);
		await fireEvent.click(ui.getByTestId("inspector-planet-cargo-slot-col-add"));
		await waitFor(() => expect(pick.invocations.length).toBe(1));
		expect(
			Array.from(pick.invocations[0]!.request.reachableIds).sort(),
		).toEqual([2, 3, 4]);
	});

	test("the picker accepts an unidentified destination", async () => {
		const { ui, pick } = mount(
			makePlanet({
				number: 1,
				name: "Earth",
				x: 100,
				y: 100,
				kind: "local",
			}),
			[
				makePlanet({
					number: 1,
					name: "Earth",
					x: 100,
					y: 100,
					kind: "local",
				}),
				makePlanet({
					number: 9,
					name: "",
					x: 130,
					y: 100,
					kind: "unidentified",
				}),
			],
			[],
			1.5,
		);
		await fireEvent.click(ui.getByTestId("inspector-planet-cargo-slot-mat-add"));
		await waitFor(() => expect(pick.invocations.length).toBe(1));
		pick.invocations[0]!.resolve(9);
		await waitFor(() => expect(draft.commands).toHaveLength(1));
		const cmd = draft.commands[0]!;
		expect(cmd.kind).toBe("setCargoRoute");
		if (cmd.kind !== "setCargoRoute") return;
		expect(cmd.destinationPlanetNumber).toBe(9);
		expect(cmd.loadType).toBe("MAT");
	});

	test("a successful pick emits setCargoRoute and closes the prompt", async () => {
		const { ui, pick } = mount(
			makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }),
			[
				makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }),
				makePlanet({ number: 2, name: "Mars", x: 150, y: 100 }),
			],
			[],
			2,
		);
		await fireEvent.click(ui.getByTestId("inspector-planet-cargo-slot-cap-add"));
		await waitFor(() => expect(pick.invocations.length).toBe(1));
		pick.invocations[0]!.resolve(2);
		await waitFor(() => expect(draft.commands).toHaveLength(1));
		const cmd = draft.commands[0]!;
		expect(cmd.kind).toBe("setCargoRoute");
		if (cmd.kind !== "setCargoRoute") return;
		expect(cmd.sourcePlanetNumber).toBe(1);
		expect(cmd.destinationPlanetNumber).toBe(2);
		expect(cmd.loadType).toBe("CAP");
		await waitFor(() =>
			expect(
				ui.queryByTestId("inspector-planet-cargo-pick-prompt"),
			).toBeNull(),
		);
	});

	test("cancel resolves null and emits no command", async () => {
		const { ui, pick } = mount(
			makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }),
			[
				makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }),
				makePlanet({ number: 2, name: "Mars", x: 150, y: 100 }),
			],
		);
		await fireEvent.click(ui.getByTestId("inspector-planet-cargo-slot-mat-add"));
		await waitFor(() => expect(pick.invocations.length).toBe(1));
		pick.invocations[0]!.resolve(null);
		await waitFor(() =>
			expect(
				ui.queryByTestId("inspector-planet-cargo-pick-prompt"),
			).toBeNull(),
		);
		expect(draft.commands).toHaveLength(0);
	});

	test("Remove emits removeCargoRoute for the slot", async () => {
		const { ui, pick } = mount(
			makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }),
			[
				makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }),
				makePlanet({ number: 2, name: "Mars", x: 150, y: 100 }),
			],
			[
				{
					sourcePlanetNumber: 1,
					entries: [{ loadType: "EMP", destinationPlanetNumber: 2 }],
				},
			],
		);
		await fireEvent.click(
			ui.getByTestId("inspector-planet-cargo-slot-emp-remove"),
		);
		await waitFor(() => expect(draft.commands).toHaveLength(1));
		const cmd = draft.commands[0]!;
		expect(cmd.kind).toBe("removeCargoRoute");
		if (cmd.kind !== "removeCargoRoute") return;
		expect(cmd.sourcePlanetNumber).toBe(1);
		expect(cmd.loadType).toBe("EMP");
	});

	test("Edit replaces the existing setCargoRoute via collapse rule", async () => {
		const { ui, pick } = mount(
			makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }),
			[
				makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }),
				makePlanet({ number: 2, name: "Mars", x: 150, y: 100 }),
				makePlanet({ number: 3, name: "Vesta", x: 100, y: 150 }),
			],
			[
				{
					sourcePlanetNumber: 1,
					entries: [{ loadType: "COL", destinationPlanetNumber: 2 }],
				},
			],
		);
		await fireEvent.click(
			ui.getByTestId("inspector-planet-cargo-slot-col-edit"),
		);
		await waitFor(() => expect(pick.invocations.length).toBe(1));
		pick.invocations[0]!.resolve(3);
		await waitFor(() => expect(draft.commands).toHaveLength(1));
		// Then a second edit to a different planet — collapse keeps a
		// single row.
		await fireEvent.click(
			ui.getByTestId("inspector-planet-cargo-slot-col-edit"),
		);
		await waitFor(() => expect(pick.invocations.length).toBe(2));
		pick.invocations[1]!.resolve(2);
		await waitFor(() => expect(draft.commands).toHaveLength(1));
		const cmd = draft.commands[0]!;
		expect(cmd.kind).toBe("setCargoRoute");
		if (cmd.kind !== "setCargoRoute") return;
		expect(cmd.destinationPlanetNumber).toBe(2);
	});

	test("different load-types coexist without collapsing each other", async () => {
		const { ui, pick } = mount(
			makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }),
			[
				makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }),
				makePlanet({ number: 2, name: "Mars", x: 150, y: 100 }),
			],
		);
		await fireEvent.click(ui.getByTestId("inspector-planet-cargo-slot-col-add"));
		await waitFor(() => expect(pick.invocations.length).toBe(1));
		pick.invocations[0]!.resolve(2);
		await waitFor(() => expect(draft.commands).toHaveLength(1));
		await fireEvent.click(ui.getByTestId("inspector-planet-cargo-slot-cap-add"));
		await waitFor(() => expect(pick.invocations.length).toBe(2));
		pick.invocations[1]!.resolve(2);
		await waitFor(() => expect(draft.commands).toHaveLength(2));
		const types = draft.commands
			.filter((c) => c.kind === "setCargoRoute")
			.map((c) => (c.kind === "setCargoRoute" ? c.loadType : ""))
			.sort();
		expect(types).toEqual(["CAP", "COL"]);
	});

	test("no_destinations message appears when reach is positive but every planet is out of range", () => {
		const { ui, pick } = mount(
			makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }),
			[
				makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }),
				makePlanet({ number: 2, name: "Pluto", x: 5000, y: 5000 }),
			],
			[],
			0.1, // reach 4 — far less than 5000 distance
		);
		expect(
			ui.getByTestId("inspector-planet-cargo-no-destinations"),
		).toBeInTheDocument();
	});
});
