// Phase 29 pure helpers in `src/map/visibility.ts`. The tests exercise
// `computeHiddenPlanetNumbers`, `computeHiddenIds`, `computeFogCircles`,
// and `isCategoryVisible` directly so the map view can stay a thin
// wiring layer.

import { describe, expect, test } from "vitest";

import type { GameReport, ReportPlanet } from "../src/api/game-state";
import { DEFAULT_MAP_TOGGLES, type MapToggles } from "../src/lib/game-state.svelte";
import type { MapCategory } from "../src/map/state-binding";
import {
	FLIGHT_DISTANCE_PER_DRIVE,
	VISIBILITY_DISTANCE_PER_DRIVE,
	computeFogCircles,
	computeHiddenIds,
	computeHiddenPlanetNumbers,
	fingerprintHiddenPlanets,
	isCategoryVisible,
} from "../src/map/visibility";
import type { PrimitiveID } from "../src/map/world";
import { EMPTY_SHIP_GROUPS } from "./helpers/empty-ship-groups";

function makeReport(overrides: Partial<GameReport> = {}): GameReport {
	return {
		turn: 1,
		mapWidth: 4000,
		mapHeight: 4000,
		planetCount: 0,
		planets: [],
		race: "",
		localShipClass: [],
		routes: [],
		localPlayerDrive: 0,
		localPlayerWeapons: 0,
		localPlayerShields: 0,
		localPlayerCargo: 0,
		...EMPTY_SHIP_GROUPS,
		...overrides,
	};
}

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

function toggles(overrides: Partial<MapToggles> = {}): MapToggles {
	return { ...DEFAULT_MAP_TOGGLES, ...overrides };
}

describe("isCategoryVisible", () => {
	test("local planets are always visible regardless of toggles", () => {
		expect(
			isCategoryVisible("planet-local", toggles({ foreignPlanets: false })),
		).toBe(true);
	});

	test("each kind toggle controls its planet category", () => {
		const t = toggles({
			foreignPlanets: false,
			uninhabitedPlanets: false,
			unidentifiedPlanets: false,
		});
		expect(isCategoryVisible("planet-foreign", t)).toBe(false);
		expect(isCategoryVisible("planet-uninhabited", t)).toBe(false);
		expect(isCategoryVisible("planet-unidentified", t)).toBe(false);
	});

	test("battle and bombing markers have independent toggles", () => {
		const t = toggles({ battleMarkers: false, bombingMarkers: true });
		expect(isCategoryVisible("battleMarker", t)).toBe(false);
		expect(isCategoryVisible("bombingMarker", t)).toBe(true);
	});
});

describe("computeHiddenPlanetNumbers", () => {
	test("returns an empty set when defaults are in effect", () => {
		const report = makeReport({
			localPlayerDrive: 10,
			planets: [
				makePlanet({ number: 1, kind: "local", x: 100, y: 100 }),
				makePlanet({ number: 2, kind: "other", x: 200, y: 100 }),
			],
		});
		expect(computeHiddenPlanetNumbers(report, toggles())).toEqual(new Set());
	});

	test("kind-toggle off hides every planet of that kind", () => {
		const report = makeReport({
			localPlayerDrive: 10,
			planets: [
				makePlanet({ number: 1, kind: "local", x: 100, y: 100 }),
				makePlanet({ number: 2, kind: "other", x: 200, y: 100 }),
				makePlanet({ number: 3, kind: "uninhabited", x: 300, y: 100 }),
				makePlanet({ number: 4, kind: "unidentified", x: 400, y: 100 }),
			],
		});
		const hidden = computeHiddenPlanetNumbers(
			report,
			toggles({ foreignPlanets: false, unidentifiedPlanets: false }),
		);
		expect(hidden).toEqual(new Set([2, 4]));
	});

	test("unreachablePlanets=off hides planets beyond FlightDistance", () => {
		const report = makeReport({
			localPlayerDrive: 10,
			mapWidth: 4000,
			mapHeight: 4000,
			planets: [
				makePlanet({ number: 1, kind: "local", x: 100, y: 100 }),
				// Foreign within reach: distance ≈ 100 < 400.
				makePlanet({ number: 2, kind: "other", x: 200, y: 100 }),
				// Foreign beyond reach: distance ≈ 500 > 400.
				makePlanet({ number: 3, kind: "other", x: 600, y: 100 }),
			],
		});
		const reachLimit = 10 * FLIGHT_DISTANCE_PER_DRIVE;
		expect(reachLimit).toBe(400);
		const hidden = computeHiddenPlanetNumbers(
			report,
			toggles({ unreachablePlanets: false }),
		);
		expect(hidden).toEqual(new Set([3]));
	});

	test("torus wrap shortens reach distance across the seam", () => {
		const report = makeReport({
			localPlayerDrive: 10,
			mapWidth: 1000,
			mapHeight: 1000,
			planets: [
				makePlanet({ number: 1, kind: "local", x: 50, y: 500 }),
				// Wrap distance is 100 (50 → 950 via the left seam), well
				// inside the 400-unit reach. Without the torus metric this
				// would resolve to 900 and the planet would hide.
				makePlanet({ number: 2, kind: "other", x: 950, y: 500 }),
			],
		});
		const hidden = computeHiddenPlanetNumbers(
			report,
			toggles({ unreachablePlanets: false }),
		);
		expect(hidden).toEqual(new Set());
	});

	test("localPlayerDrive=0 hides every non-local planet when reach filter is on", () => {
		const report = makeReport({
			localPlayerDrive: 0,
			planets: [
				makePlanet({ number: 1, kind: "local", x: 100, y: 100 }),
				makePlanet({ number: 2, kind: "other", x: 101, y: 100 }),
			],
		});
		const hidden = computeHiddenPlanetNumbers(
			report,
			toggles({ unreachablePlanets: false }),
		);
		expect(hidden).toEqual(new Set([2]));
	});

	test("a report with no LOCAL planets keeps everything visible (no reach anchor)", () => {
		const report = makeReport({
			localPlayerDrive: 10,
			planets: [makePlanet({ number: 9, kind: "other", x: 9000, y: 9000 })],
		});
		const hidden = computeHiddenPlanetNumbers(
			report,
			toggles({ unreachablePlanets: false }),
		);
		expect(hidden).toEqual(new Set());
	});

	test("LOCAL planets are never hidden", () => {
		const report = makeReport({
			localPlayerDrive: 10,
			planets: [makePlanet({ number: 1, kind: "local", x: 1, y: 1 })],
		});
		expect(
			computeHiddenPlanetNumbers(
				report,
				toggles({ foreignPlanets: false, unreachablePlanets: false }),
			),
		).toEqual(new Set());
	});
});

describe("computeHiddenIds", () => {
	const categories: Map<PrimitiveID, MapCategory> = new Map<
		PrimitiveID,
		MapCategory
	>([
		[1, "planet-local"],
		[2, "planet-foreign"],
		[100, "hyperspaceGroup"],
		[150, "hyperspaceGroup"],
		[200, "incomingGroup"],
		[300, "battleMarker"],
		[400, "bombingMarker"],
	]);
	const planetDependents = new Map<number, ReadonlySet<PrimitiveID>>([
		[1, new Set([1])],
		[2, new Set([2, 100, 150, 200, 300, 400])],
	]);

	test("category-toggle off hides every primitive in that category", () => {
		const hidden = computeHiddenIds(
			categories,
			planetDependents,
			new Set(),
			toggles({ hyperspaceGroups: false }),
		);
		expect(hidden.has(100)).toBe(true);
		expect(hidden.has(150)).toBe(true);
		expect(hidden.has(200)).toBe(false);
		expect(hidden.has(2)).toBe(false);
	});

	test("hiding a planet cascades onto its dependent primitives", () => {
		const hidden = computeHiddenIds(
			categories,
			planetDependents,
			new Set([2]),
			toggles(),
		);
		expect(hidden).toEqual(new Set([2, 100, 150, 200, 300, 400]));
	});

	test("battle / bombing markers have independent toggles", () => {
		const hidden = computeHiddenIds(
			categories,
			planetDependents,
			new Set(),
			toggles({ battleMarkers: false }),
		);
		expect(hidden.has(300)).toBe(true);
		expect(hidden.has(400)).toBe(false);
	});

	test("planet cascade and category toggle compose without duplicates", () => {
		const hidden = computeHiddenIds(
			categories,
			planetDependents,
			new Set([2]),
			toggles({ battleMarkers: false }),
		);
		// 300 is already present from the cascade; the category toggle
		// re-adds it but Set semantics dedupe.
		expect(hidden).toEqual(new Set([2, 100, 150, 200, 300, 400]));
	});
});

describe("computeFogCircles", () => {
	test("disabled toggle returns an empty list", () => {
		const report = makeReport({
			localPlayerDrive: 10,
			planets: [makePlanet({ number: 1, kind: "local", x: 100, y: 100 })],
		});
		expect(
			computeFogCircles(report, toggles({ visibleHyperspace: false })),
		).toEqual([]);
	});

	test("zero drive returns an empty list (radius would be zero)", () => {
		const report = makeReport({
			localPlayerDrive: 0,
			planets: [makePlanet({ number: 1, kind: "local", x: 100, y: 100 })],
		});
		expect(computeFogCircles(report, toggles())).toEqual([]);
	});

	test("emits one circle per LOCAL planet at VisibilityDistance", () => {
		const report = makeReport({
			localPlayerDrive: 10,
			planets: [
				makePlanet({ number: 1, kind: "local", x: 100, y: 100 }),
				makePlanet({ number: 2, kind: "local", x: 300, y: 200 }),
				makePlanet({ number: 3, kind: "other", x: 500, y: 500 }),
			],
		});
		const radius = 10 * VISIBILITY_DISTANCE_PER_DRIVE;
		expect(radius).toBe(300);
		expect(computeFogCircles(report, toggles())).toEqual([
			{ x: 100, y: 100, radius },
			{ x: 300, y: 200, radius },
		]);
	});
});

describe("fingerprintHiddenPlanets", () => {
	test("sorts numerically for deterministic fingerprint", () => {
		expect(fingerprintHiddenPlanets(new Set([3, 1, 2]))).toBe("1,2,3");
	});

	test("empty set returns an empty string", () => {
		expect(fingerprintHiddenPlanets(new Set())).toBe("");
	});
});
