// Unit tests for the no-wrap camera helpers in src/map/no-wrap.ts.
//
// The bounded-plane mode has three invariants that the helpers must
// uphold together:
//
//   1. The visible viewport stays inside the world rectangle, except
//      when the visible viewport span exceeds the world span on an
//      axis — in that case the camera centres on that axis.
//   2. minScaleNoWrap is the smallest scale at which the visible
//      viewport fits the world along both axes.
//   3. pivotZoom keeps the world point under the cursor stable
//      across a scale change.
//
// Each invariant is tested in isolation here; the renderer composes
// them in render.ts.

import { describe, expect, test } from "vitest";
import { screenToWorld } from "../src/map/math";
import { clampCameraNoWrap, minScaleNoWrap, pivotZoom } from "../src/map/no-wrap";
import { World } from "../src/map/world";

const world = new World(1000, 800);
const viewport = { widthPx: 400, heightPx: 300 };

describe("clampCameraNoWrap", () => {
	test("leaves the camera unchanged when the viewport sits inside the world", () => {
		const c = clampCameraNoWrap({ centerX: 500, centerY: 400, scale: 1 }, viewport, world);
		expect(c.centerX).toBe(500);
		expect(c.centerY).toBe(400);
	});
	test("clamps the camera to the left edge", () => {
		const c = clampCameraNoWrap({ centerX: 0, centerY: 400, scale: 1 }, viewport, world);
		expect(c.centerX).toBe(viewport.widthPx / 2);
	});
	test("clamps the camera to the right edge", () => {
		const c = clampCameraNoWrap({ centerX: 9999, centerY: 400, scale: 1 }, viewport, world);
		expect(c.centerX).toBe(world.width - viewport.widthPx / 2);
	});
	test("clamps the camera to the top edge", () => {
		const c = clampCameraNoWrap({ centerX: 500, centerY: -50, scale: 1 }, viewport, world);
		expect(c.centerY).toBe(viewport.heightPx / 2);
	});
	test("clamps the camera to the bottom edge", () => {
		const c = clampCameraNoWrap({ centerX: 500, centerY: 9999, scale: 1 }, viewport, world);
		expect(c.centerY).toBe(world.height - viewport.heightPx / 2);
	});
	test("centres the camera on an axis when the viewport span exceeds world span", () => {
		// At scale=0.1, viewport.widthPx/scale = 4000 world units > world.width=1000.
		const c = clampCameraNoWrap({ centerX: 999, centerY: 999, scale: 0.1 }, viewport, world);
		expect(c.centerX).toBe(world.width / 2);
		expect(c.centerY).toBe(world.height / 2);
	});
	test("does not modify scale", () => {
		const c = clampCameraNoWrap({ centerX: 999, centerY: 999, scale: 0.5 }, viewport, world);
		expect(c.scale).toBe(0.5);
	});
});

describe("minScaleNoWrap", () => {
	test("equals the larger axis ratio (width-bound)", () => {
		// world 1000×800, viewport 400×300:
		// width ratio = 0.4, height ratio = 0.375 — width wins.
		expect(minScaleNoWrap(viewport, world)).toBeCloseTo(0.4, 12);
	});
	test("equals the larger axis ratio (height-bound)", () => {
		// world 100×100, viewport 200×400: height ratio = 4 wins over width = 2.
		expect(minScaleNoWrap({ widthPx: 200, heightPx: 400 }, new World(100, 100))).toBeCloseTo(
			4,
			12,
		);
	});
});

describe("pivotZoom", () => {
	const camera = { centerX: 500, centerY: 400, scale: 1 };

	test("keeps the world point under the cursor stable", () => {
		const cursor = { x: 100, y: 250 };
		const before = screenToWorld(cursor, camera, viewport);
		const newCam = pivotZoom(camera, viewport, cursor, 2.5);
		const after = screenToWorld(cursor, newCam, viewport);
		expect(after.x).toBeCloseTo(before.x, 9);
		expect(after.y).toBeCloseTo(before.y, 9);
		expect(newCam.scale).toBe(2.5);
	});

	test("invariant holds when the cursor sits at the viewport centre", () => {
		const cursor = { x: viewport.widthPx / 2, y: viewport.heightPx / 2 };
		const before = screenToWorld(cursor, camera, viewport);
		const newCam = pivotZoom(camera, viewport, cursor, 0.4);
		const after = screenToWorld(cursor, newCam, viewport);
		expect(after.x).toBeCloseTo(before.x, 9);
		expect(after.y).toBeCloseTo(before.y, 9);
	});

	test("invariant holds at the viewport corner", () => {
		const cursor = { x: 0, y: 0 };
		const before = screenToWorld(cursor, camera, viewport);
		const newCam = pivotZoom(camera, viewport, cursor, 7.7);
		const after = screenToWorld(cursor, newCam, viewport);
		expect(after.x).toBeCloseTo(before.x, 9);
		expect(after.y).toBeCloseTo(before.y, 9);
	});

	test("rejects non-positive scale", () => {
		expect(() => pivotZoom(camera, viewport, { x: 0, y: 0 }, 0)).toThrow();
		expect(() => pivotZoom(camera, viewport, { x: 0, y: 0 }, -1)).toThrow();
	});
});
