// Phase 29 unit coverage for the visible-hyperspace overlay's paint
// ops. `fogPaintOps` lives in `src/map/render.ts` next to its sole
// consumer (`RendererHandle.setVisibilityFog`): the renderer draws
// the rectangle ops into a `fogLayer` container (below every
// primitive copy) and feeds the circle ops into an inverse stencil
// mask that cuts the visibility holes out of the fog. `fogPaintOps`
// only produces the ordered op list — rect(s) first, then one circle
// per visibility circle — which is what these tests pin; earlier
// renderer implementations used Pixi `cut()` (disconnected arcs) and
// then opaque overpaint (a fill-rate cliff under Safari's WebGPU).
//
// Coordinates returned by `fogPaintOps` are in world space because
// `fogLayer` and the mask have no transform — wraps for torus mode
// are baked into the ops directly.

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

import { FOG_COLOR, fogPaintOps } from "../src/map/render";

const BG_COLOR = 0x0a0e1a;
const WORLD = { width: 1000, height: 800 };

describe("fogPaintOps — no-wrap mode", () => {
	test("empty input returns no ops", () => {
		expect(fogPaintOps(WORLD, [], FOG_COLOR, BG_COLOR, "no-wrap")).toEqual([]);
	});

	test("single circle emits a single fog rect + one bg circle", () => {
		const ops = fogPaintOps(
			WORLD,
			[{ x: 100, y: 200, radius: 50 }],
			FOG_COLOR,
			BG_COLOR,
			"no-wrap",
		);
		expect(ops).toEqual([
			{
				kind: "fillRect",
				x: 0,
				y: 0,
				width: 1000,
				height: 800,
				color: FOG_COLOR,
				alpha: 1,
			},
			{
				kind: "fillCircle",
				x: 100,
				y: 200,
				radius: 50,
				color: BG_COLOR,
				alpha: 1,
			},
		]);
	});

	test("multiple circles produce one fog rect followed by N bg circles", () => {
		const ops = fogPaintOps(
			WORLD,
			[
				{ x: 100, y: 100, radius: 50 },
				{ x: 300, y: 200, radius: 80 },
				{ x: 500, y: 600, radius: 30 },
			],
			FOG_COLOR,
			BG_COLOR,
			"no-wrap",
		);
		expect(ops.length).toBe(4);
		expect(ops[0].kind).toBe("fillRect");
		for (let i = 1; i < ops.length; i++) {
			expect(ops[i].kind).toBe("fillCircle");
			const op = ops[i];
			if (op.kind === "fillCircle") {
				expect(op.color).toBe(BG_COLOR);
				expect(op.alpha).toBe(1);
			}
		}
	});

	test("zero or negative world dimensions return no ops", () => {
		expect(
			fogPaintOps(
				{ width: 0, height: 800 },
				[{ x: 0, y: 0, radius: 10 }],
				FOG_COLOR,
				BG_COLOR,
				"no-wrap",
			),
		).toEqual([]);
		expect(
			fogPaintOps(
				{ width: 1000, height: -1 },
				[{ x: 0, y: 0, radius: 10 }],
				FOG_COLOR,
				BG_COLOR,
				"no-wrap",
			),
		).toEqual([]);
	});
});

describe("fogPaintOps — torus mode", () => {
	test("single circle expands to 9 fog rects + 9 bg circles in world space", () => {
		const ops = fogPaintOps(
			WORLD,
			[{ x: 100, y: 200, radius: 50 }],
			FOG_COLOR,
			BG_COLOR,
			"torus",
		);
		// 9 fog rects + 9 wrapped circles.
		expect(ops.length).toBe(18);
		// The first 9 ops are fog rects, one per neighbour tile.
		const rectPositions = ops
			.slice(0, 9)
			.map((op) =>
				op.kind === "fillRect" ? `${op.x},${op.y}` : "non-rect",
			)
			.sort();
		const expectedRectPositions: string[] = [];
		for (const dx of [-1, 0, 1]) {
			for (const dy of [-1, 0, 1]) {
				expectedRectPositions.push(`${dx * 1000},${dy * 800}`);
			}
		}
		expectedRectPositions.sort();
		expect(rectPositions).toEqual(expectedRectPositions);
		// The next 9 ops are bg circles at every wrapped planet position.
		const circlePositions = ops
			.slice(9)
			.map((op) =>
				op.kind === "fillCircle" ? `${op.x},${op.y}` : "non-circle",
			)
			.sort();
		const expectedCirclePositions: string[] = [];
		for (const dx of [-1, 0, 1]) {
			for (const dy of [-1, 0, 1]) {
				expectedCirclePositions.push(
					`${100 + dx * 1000},${200 + dy * 800}`,
				);
			}
		}
		expectedCirclePositions.sort();
		expect(circlePositions).toEqual(expectedCirclePositions);
	});

	test("multiple circles produce 9 fog rects + 9N bg circles", () => {
		const ops = fogPaintOps(
			WORLD,
			[
				{ x: 100, y: 100, radius: 50 },
				{ x: 700, y: 600, radius: 30 },
			],
			FOG_COLOR,
			BG_COLOR,
			"torus",
		);
		// 9 fog rects + (9 wraps × 2 circles) = 27 ops.
		expect(ops.length).toBe(27);
		expect(
			ops.slice(0, 9).every((op) => op.kind === "fillRect"),
		).toBe(true);
		expect(
			ops.slice(9).every((op) => op.kind === "fillCircle"),
		).toBe(true);
		const radii = ops
			.slice(9)
			.map((op) => (op.kind === "fillCircle" ? op.radius : 0));
		expect(radii.filter((r) => r === 50).length).toBe(9);
		expect(radii.filter((r) => r === 30).length).toBe(9);
	});

	test("a circle near the right edge produces a wrapped copy past the seam", () => {
		// Planet at (950, 400) with radius 300 — the painted area
		// extends to x = 1250 in the central tile. In torus mode the
		// renderer also draws wrapped circles at (-50, 400) and
		// (1950, 400) so the circle stays continuous across the seam
		// instead of appearing as a sector clipped by the neighbour
		// tile's fog rectangle.
		const ops = fogPaintOps(
			WORLD,
			[{ x: 950, y: 400, radius: 300 }],
			FOG_COLOR,
			BG_COLOR,
			"torus",
		);
		const circleXs = ops
			.filter((op) => op.kind === "fillCircle")
			.map((op) => (op.kind === "fillCircle" ? op.x : 0));
		expect(circleXs).toContain(-50);
		expect(circleXs).toContain(950);
		expect(circleXs).toContain(1950);
	});

	test("empty input still returns no ops in torus mode", () => {
		expect(fogPaintOps(WORLD, [], FOG_COLOR, BG_COLOR, "torus")).toEqual([]);
	});
});
