// Vitest coverage for `lib/util/ship-class-validation.ts`. The
// validator is a TS port of `pkg/calc/validator.go` plus the
// `validateEntityName` rules and a UX-only duplicate-name check.
// Each branch is exercised explicitly so a future engine
// validator change cannot drift silently.

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

import {
	validateShipClass,
	type ShipClassDraft,
} from "../src/lib/util/ship-class-validation";

function draft(overrides: Partial<ShipClassDraft>): ShipClassDraft {
	return {
		name: "Scout",
		drive: 1,
		armament: 0,
		weapons: 0,
		shields: 0,
		cargo: 0,
		...overrides,
	};
}

describe("validateShipClass", () => {
	test("accepts a minimal valid drone-style class", () => {
		const result = validateShipClass(
			draft({ name: "Drone", drive: 1, armament: 0, weapons: 0 }),
		);
		expect(result.ok).toBe(true);
		if (!result.ok) return;
		expect(result.value.name).toBe("Drone");
	});

	test("trims surrounding whitespace from the name", () => {
		const result = validateShipClass(draft({ name: "  Scout  " }));
		expect(result.ok).toBe(true);
		if (!result.ok) return;
		expect(result.value.name).toBe("Scout");
	});

	test("rejects empty name", () => {
		const result = validateShipClass(draft({ name: "" }));
		expect(result.ok).toBe(false);
		if (result.ok) return;
		expect(result.reason).toBe("empty");
	});

	test("rejects name longer than 30 runes", () => {
		const result = validateShipClass(draft({ name: "a".repeat(31) }));
		expect(result.ok).toBe(false);
		if (result.ok) return;
		expect(result.reason).toBe("too_long");
	});

	test("rejects name with whitespace inside", () => {
		const result = validateShipClass(draft({ name: "Big Ship" }));
		expect(result.ok).toBe(false);
		if (result.ok) return;
		expect(result.reason).toBe("whitespace");
	});

	test.each([
		{ value: 0.5, reason: "drive_value" as const, field: "drive" as const },
		{ value: -1, reason: "drive_value" as const, field: "drive" as const },
		{
			value: Number.POSITIVE_INFINITY,
			reason: "drive_value" as const,
			field: "drive" as const,
		},
		{ value: 0.5, reason: "weapons_value" as const, field: "weapons" as const },
		{ value: 0.5, reason: "shields_value" as const, field: "shields" as const },
		{ value: 0.5, reason: "cargo_value" as const, field: "cargo" as const },
	])(
		"rejects $field = $value with reason $reason",
		({ value, reason, field }) => {
			// Make sure both armament/weapons stay coupled (both nonzero or both zero) so we
			// trip the per-field rule before the pair rule kicks in.
			const overrides: Partial<ShipClassDraft> = {
				drive: 1,
				armament: 0,
				weapons: 0,
				shields: 0,
				cargo: 0,
			};
			if (field === "weapons") {
				overrides.armament = 1;
			}
			(overrides as Record<typeof field, number>)[field] = value;
			const result = validateShipClass(draft(overrides));
			expect(result.ok).toBe(false);
			if (result.ok) return;
			expect(result.reason).toBe(reason);
		},
	);

	test("rejects negative armament", () => {
		const result = validateShipClass(draft({ armament: -1 }));
		expect(result.ok).toBe(false);
		if (result.ok) return;
		expect(result.reason).toBe("armament_value");
	});

	test("rejects fractional armament", () => {
		const result = validateShipClass(draft({ armament: 1.5, weapons: 1 }));
		expect(result.ok).toBe(false);
		if (result.ok) return;
		expect(result.reason).toBe("armament_not_integer");
	});

	test("rejects nonzero armament with zero weapons", () => {
		const result = validateShipClass(draft({ armament: 2, weapons: 0 }));
		expect(result.ok).toBe(false);
		if (result.ok) return;
		expect(result.reason).toBe("armament_weapons_pair");
	});

	test("rejects zero armament with nonzero weapons", () => {
		const result = validateShipClass(draft({ armament: 0, weapons: 5 }));
		expect(result.ok).toBe(false);
		if (result.ok) return;
		expect(result.reason).toBe("armament_weapons_pair");
	});

	test("rejects all-zero values", () => {
		const result = validateShipClass(
			draft({
				drive: 0,
				armament: 0,
				weapons: 0,
				shields: 0,
				cargo: 0,
			}),
		);
		expect(result.ok).toBe(false);
		if (result.ok) return;
		expect(result.reason).toBe("all_zero");
	});

	test("accepts the canonical Cruiser fixture from rules.txt", () => {
		const result = validateShipClass(
			draft({
				name: "Cruiser",
				drive: 15,
				armament: 1,
				weapons: 15,
				shields: 15,
				cargo: 0,
			}),
		);
		expect(result.ok).toBe(true);
	});

	test("accepts the canonical Megafreighter fixture", () => {
		const result = validateShipClass(
			draft({
				name: "Megafreighter",
				drive: 80,
				armament: 2,
				weapons: 2,
				shields: 30,
				cargo: 100,
			}),
		);
		expect(result.ok).toBe(true);
	});

	test("flags duplicate names against existingNames", () => {
		const result = validateShipClass(draft({ name: "Scout" }), {
			existingNames: ["Scout", "Destroyer"],
		});
		expect(result.ok).toBe(false);
		if (result.ok) return;
		expect(result.reason).toBe("duplicate_name");
	});

	test("compares duplicate names after trimming", () => {
		const result = validateShipClass(draft({ name: "  Scout  " }), {
			existingNames: ["Scout"],
		});
		expect(result.ok).toBe(false);
		if (result.ok) return;
		expect(result.reason).toBe("duplicate_name");
	});

	test("does not flag duplicates when existingNames is empty", () => {
		const result = validateShipClass(draft({ name: "Scout" }), {
			existingNames: [],
		});
		expect(result.ok).toBe(true);
	});
});
