// Unit tests for the geometry primitives in src/map/math.ts.
//
// These functions are the foundation for hit-test and the no-wrap
// camera helpers; they run far more often than their callers and any
// regression here ripples everywhere. Each test asserts a single
// algebraic property; the cases together cover the contract of the
// functions described in ui/docs/renderer.md.

import { describe, expect, test } from "vitest";
import {
	clamp,
	distSqPointToSegment,
	screenToWorld,
	torusShortestDelta,
	worldToScreen,
} from "../src/map/math";

describe("clamp", () => {
	test("returns the value when inside the bounds", () => {
		expect(clamp(5, 0, 10)).toBe(5);
		expect(clamp(0, 0, 10)).toBe(0);
		expect(clamp(10, 0, 10)).toBe(10);
	});
	test("clamps to the lower bound", () => {
		expect(clamp(-3, 0, 10)).toBe(0);
	});
	test("clamps to the upper bound", () => {
		expect(clamp(13, 0, 10)).toBe(10);
	});
});

describe("torusShortestDelta", () => {
	test("returns zero for equal inputs", () => {
		expect(torusShortestDelta(50, 50, 100)).toBe(0);
	});
	test("returns the direct delta when no wrap is shorter", () => {
		expect(torusShortestDelta(10, 30, 100)).toBe(20);
		expect(torusShortestDelta(30, 10, 100)).toBe(-20);
	});
	test("wraps to the shorter direction near the seam", () => {
		// from=10, to=90: direct=+80, wrap=-20 — wrap wins.
		expect(torusShortestDelta(10, 90, 100)).toBe(-20);
		// from=90, to=10: direct=-80, wrap=+20 — wrap wins.
		expect(torusShortestDelta(90, 10, 100)).toBe(20);
	});
	test("normalises inputs outside [0, size)", () => {
		expect(torusShortestDelta(-10, 10, 100)).toBe(20);
		expect(torusShortestDelta(110, 10, 100)).toBe(-100 + 100); // wraps to 0
	});
	test("at exactly size/2 picks the positive direction deterministically", () => {
		// from=0, to=50, size=100 — both directions are equal.
		// The contract: returns +size/2.
		expect(torusShortestDelta(0, 50, 100)).toBe(50);
	});
	test("rejects non-positive size", () => {
		expect(() => torusShortestDelta(0, 0, 0)).toThrow();
		expect(() => torusShortestDelta(0, 0, -1)).toThrow();
	});
});

describe("distSqPointToSegment", () => {
	test("zero distance when the point is on the segment", () => {
		expect(distSqPointToSegment(5, 0, 0, 0, 10, 0)).toBe(0);
		expect(distSqPointToSegment(0, 0, 0, 0, 10, 0)).toBe(0);
		expect(distSqPointToSegment(10, 0, 0, 0, 10, 0)).toBe(0);
	});
	test("perpendicular foot inside the segment", () => {
		// segment along the x-axis from (0,0) to (10,0); point at (5,3).
		// foot is (5,0), distance is 3, distSq is 9.
		expect(distSqPointToSegment(5, 3, 0, 0, 10, 0)).toBeCloseTo(9, 12);
	});
	test("foot beyond the start endpoint clamps to start", () => {
		expect(distSqPointToSegment(-2, 0, 0, 0, 10, 0)).toBeCloseTo(4, 12);
	});
	test("foot beyond the end endpoint clamps to end", () => {
		expect(distSqPointToSegment(15, 0, 0, 0, 10, 0)).toBeCloseTo(25, 12);
	});
	test("zero-length segment falls back to point distance", () => {
		expect(distSqPointToSegment(3, 4, 0, 0, 0, 0)).toBeCloseTo(25, 12);
	});
});

describe("screenToWorld and worldToScreen", () => {
	const viewport = { widthPx: 800, heightPx: 600 };
	const camera = { centerX: 1000, centerY: 500, scale: 2 };

	test("centre of viewport maps to camera centre in world space", () => {
		const w = screenToWorld({ x: 400, y: 300 }, camera, viewport);
		expect(w.x).toBeCloseTo(1000, 12);
		expect(w.y).toBeCloseTo(500, 12);
	});

	test("worldToScreen is the inverse of screenToWorld", () => {
		const screenIn = { x: 123.5, y: 456.25 };
		const world = screenToWorld(screenIn, camera, viewport);
		const screenOut = worldToScreen(world, camera, viewport);
		expect(screenOut.x).toBeCloseTo(screenIn.x, 9);
		expect(screenOut.y).toBeCloseTo(screenIn.y, 9);
	});

	test("scale propagates: 2px on screen = 1 world unit at scale=2", () => {
		const w0 = screenToWorld({ x: 400, y: 300 }, camera, viewport);
		const w1 = screenToWorld({ x: 402, y: 300 }, camera, viewport);
		expect(w1.x - w0.x).toBeCloseTo(1, 12);
	});
});
