package world

import (
	"errors"
	"fmt"
	"github.com/stretchr/testify/require"
	"image/color"
	"testing"
)

func newIndexedTestWorld() *World {
	w := NewWorld(10, 10)
	w.SetCircleRadiusScaleFp(SCALE)
	w.resetGrid(2 * SCALE) // 5x5 grid.
	return w
}

func cellHasOnlyID(t *testing.T, w *World, row, col int, want PrimitiveID) {
	t.Helper()

	cell := w.grid[row][col]
	if len(cell) != 1 {
		t.Fatalf("cell[%d][%d] len = %d, want 1", row, col, len(cell))
	}
	if got := cell[0].ID(); got != want {
		t.Fatalf("cell[%d][%d] item id = %v, want %v", row, col, got, want)
	}
}

func cellIsEmpty(t *testing.T, w *World, row, col int) {
	t.Helper()

	if got := len(w.grid[row][col]); got != 0 {
		t.Fatalf("cell[%d][%d] len = %d, want 0", row, col, got)
	}
}

func occupiedCellsByID(w *World, id PrimitiveID) map[[2]int]struct{} {
	result := make(map[[2]int]struct{})

	for row := range w.grid {
		for col := range w.grid[row] {
			for _, item := range w.grid[row][col] {
				if item.ID() == id {
					result[[2]int{row, col}] = struct{}{}
				}
			}
		}
	}

	return result
}

func assertOccupiedCells(t *testing.T, w *World, id PrimitiveID, want ...[2]int) {
	t.Helper()

	got := occupiedCellsByID(w, id)
	if len(got) != len(want) {
		t.Fatalf("occupied cell count = %d, want %d; got=%v want=%v", len(got), len(want), got, want)
	}

	for _, cell := range want {
		if _, ok := got[cell]; !ok {
			t.Fatalf("missing occupied cell row=%d col=%d; got=%v", cell[0], cell[1], got)
		}
	}
}

// TestNewWorld verifies new World.
func TestNewWorld(t *testing.T) {
	t.Parallel()

	w := NewWorld(12, 7)

	if w.W != 12*SCALE {
		t.Fatalf("W = %d, want %d", w.W, 12*SCALE)
	}
	if w.H != 7*SCALE {
		t.Fatalf("H = %d, want %d", w.H, 7*SCALE)
	}
	if w.cellSize != 1 {
		t.Fatalf("cellSize = %d, want 1", w.cellSize)
	}
	if w.objects == nil {
		t.Fatal("objects map is nil")
	}
}

// TestNewWorldPanicsOnInvalidSize verifies new World Panics On Invalid Size.
func TestNewWorldPanicsOnInvalidSize(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name   string
		width  int
		height int
	}{
		{name: "zero width", width: 0, height: 1},
		{name: "zero height", width: 1, height: 0},
		{name: "negative width", width: -1, height: 1},
		{name: "negative height", width: 1, height: -1},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			defer func() {
				if recover() == nil {
					t.Fatalf("NewWorld(%d, %d) did not panic", tt.width, tt.height)
				}
			}()

			_ = NewWorld(tt.width, tt.height)
		})
	}
}

// TestCheckCoordinate verifies check Coordinate.
func TestCheckCoordinate(t *testing.T) {
	t.Parallel()

	w := NewWorld(10, 10)

	tests := []struct {
		name string
		xf   int
		yf   int
		want bool
	}{
		{name: "origin", xf: 0, yf: 0, want: true},
		{name: "inside", xf: 5000, yf: 5000, want: true},
		{name: "last valid", xf: 9999, yf: 9999, want: true},
		{name: "x below", xf: -1, yf: 0, want: false},
		{name: "y below", xf: 0, yf: -1, want: false},
		{name: "x equal width", xf: 10000, yf: 0, want: false},
		{name: "y equal height", xf: 0, yf: 10000, want: false},
	}

	for _, tt := range tests {
		if got := w.checkCoordinate(tt.xf, tt.yf); got != tt.want {
			t.Fatalf("checkCoordinate(%d, %d) = %v, want %v", tt.xf, tt.yf, got, tt.want)
		}
	}
}

// TestAddPoint verifies add Point.
func TestAddPoint(t *testing.T) {
	t.Parallel()

	w := NewWorld(10, 10)

	id, err := w.AddPoint(1.25, 2.75)
	if err != nil {
		t.Fatalf("AddPoint returned error: %v", err)
	}

	item, ok := w.objects[id]
	if !ok {
		t.Fatalf("point with id %v was not stored", id)
	}

	p, ok := item.(Point)
	if !ok {
		t.Fatalf("stored item type = %T, want Point", item)
	}
	if p.X != 1250 || p.Y != 2750 {
		t.Fatalf("stored point = (%d, %d), want (1250, 2750)", p.X, p.Y)
	}
}

// TestAddPointRejectsOutOfBounds verifies add Point Rejects Out Of Bounds.
func TestAddPointRejectsOutOfBounds(t *testing.T) {
	t.Parallel()

	w := NewWorld(10, 10)

	tests := []struct {
		name string
		x    float64
		y    float64
	}{
		{name: "negative x", x: -0.001, y: 1},
		{name: "negative y", x: 1, y: -0.001},
		{name: "x rounds to width", x: 9.9995, y: 1},
		{name: "y rounds to height", x: 1, y: 9.9995},
		{name: "x clearly outside", x: 10, y: 1},
		{name: "y clearly outside", x: 1, y: 10},
	}

	for _, tt := range tests {
		_, err := w.AddPoint(tt.x, tt.y)
		if !errors.Is(err, errBadCoordinate) {
			t.Fatalf("%s: error = %v, want %v", tt.name, err, errBadCoordinate)
		}
	}
}

// TestAddPointAllowsLastRoundedInsideValue verifies add Point Allows Last Rounded Inside Value.
func TestAddPointAllowsLastRoundedInsideValue(t *testing.T) {
	t.Parallel()

	w := NewWorld(10, 10)

	id, err := w.AddPoint(9.9994, 9.9994)
	if err != nil {
		t.Fatalf("AddPoint returned error: %v", err)
	}

	p := w.objects[id].(Point)
	if p.X != 9999 || p.Y != 9999 {
		t.Fatalf("stored point = (%d, %d), want (9999, 9999)", p.X, p.Y)
	}
}

// TestAddCircle verifies add Circle.
func TestAddCircle(t *testing.T) {
	t.Parallel()

	w := NewWorld(10, 10)

	id, err := w.AddCircle(2.5, 3.5, 1.25)
	if err != nil {
		t.Fatalf("AddCircle returned error: %v", err)
	}

	item, ok := w.objects[id]
	if !ok {
		t.Fatalf("circle with id %v was not stored", id)
	}

	c, ok := item.(Circle)
	if !ok {
		t.Fatalf("stored item type = %T, want Circle", item)
	}
	if c.X != 2500 || c.Y != 3500 || c.Radius != 1250 {
		t.Fatalf("stored circle = (%d, %d, %d), want (2500, 3500, 1250)", c.X, c.Y, c.Radius)
	}
}

// TestAddCircleAllowsZeroRadius verifies add Circle Allows Zero Radius.
func TestAddCircleAllowsZeroRadius(t *testing.T) {
	t.Parallel()

	w := NewWorld(10, 10)

	id, err := w.AddCircle(2, 3, 0)
	if err != nil {
		t.Fatalf("AddCircle returned error: %v", err)
	}

	c := w.objects[id].(Circle)
	if c.Radius != 0 {
		t.Fatalf("radius = %d, want 0", c.Radius)
	}
}

// TestAddCircleRejectsInvalidInput verifies add Circle Rejects Invalid Input.
func TestAddCircleRejectsInvalidInput(t *testing.T) {
	t.Parallel()

	w := NewWorld(10, 10)

	if _, err := w.AddCircle(1, 1, -0.001); !errors.Is(err, errBadRadius) {
		t.Fatalf("negative radius error = %v, want %v", err, errBadRadius)
	}

	tests := []struct {
		name string
		x    float64
		y    float64
	}{
		{name: "negative x", x: -0.001, y: 1},
		{name: "negative y", x: 1, y: -0.001},
		{name: "x rounds to width", x: 9.9995, y: 1},
		{name: "y rounds to height", x: 1, y: 9.9995},
	}

	for _, tt := range tests {
		_, err := w.AddCircle(tt.x, tt.y, 1)
		if !errors.Is(err, errBadCoordinate) {
			t.Fatalf("%s: error = %v, want %v", tt.name, err, errBadCoordinate)
		}
	}
}

// TestAddLine verifies add Line.
func TestAddLine(t *testing.T) {
	t.Parallel()

	w := NewWorld(10, 10)

	id, err := w.AddLine(1.1, 2.2, 3.3, 4.4)
	if err != nil {
		t.Fatalf("AddLine returned error: %v", err)
	}

	item, ok := w.objects[id]
	if !ok {
		t.Fatalf("line with id %v was not stored", id)
	}

	l, ok := item.(Line)
	if !ok {
		t.Fatalf("stored item type = %T, want Line", item)
	}
	if l.X1 != 1100 || l.Y1 != 2200 || l.X2 != 3300 || l.Y2 != 4400 {
		t.Fatalf("stored line = (%d, %d) -> (%d, %d), want (1100, 2200) -> (3300, 4400)",
			l.X1, l.Y1, l.X2, l.Y2)
	}
}

// TestAddLineRejectsInvalidInput verifies add Line Rejects Invalid Input.
func TestAddLineRejectsInvalidInput(t *testing.T) {
	t.Parallel()

	w := NewWorld(10, 10)

	tests := []struct {
		name string
		x1   float64
		y1   float64
		x2   float64
		y2   float64
	}{
		{name: "first point x below", x1: -0.001, y1: 1, x2: 2, y2: 2},
		{name: "first point y below", x1: 1, y1: -0.001, x2: 2, y2: 2},
		{name: "second point x below", x1: 1, y1: 1, x2: -0.001, y2: 2},
		{name: "second point y below", x1: 1, y1: 1, x2: 2, y2: -0.001},
		{name: "first point x rounds to width", x1: 9.9995, y1: 1, x2: 2, y2: 2},
		{name: "second point y rounds to height", x1: 1, y1: 1, x2: 2, y2: 9.9995},
	}

	for _, tt := range tests {
		_, err := w.AddLine(tt.x1, tt.y1, tt.x2, tt.y2)
		if !errors.Is(err, errBadCoordinate) {
			t.Fatalf("%s: error = %v, want %v", tt.name, err, errBadCoordinate)
		}
	}
}

// TestResetGrid verifies reset Grid.
func TestResetGrid(t *testing.T) {
	t.Parallel()

	w := NewWorld(10, 6)
	w.resetGrid(2 * SCALE)

	if w.cellSize != 2*SCALE {
		t.Fatalf("cellSize = %d, want %d", w.cellSize, 2*SCALE)
	}
	if w.cols != 5 {
		t.Fatalf("cols = %d, want 5", w.cols)
	}
	if w.rows != 3 {
		t.Fatalf("rows = %d, want 3", w.rows)
	}
	if len(w.grid) != 3 {
		t.Fatalf("len(grid) = %d, want 3", len(w.grid))
	}
	for row := range w.grid {
		if len(w.grid[row]) != 5 {
			t.Fatalf("len(grid[%d]) = %d, want 5", row, len(w.grid[row]))
		}
	}
}

// TestWorldToCellXY verifies world To Cell XY.
func TestWorldToCellXY(t *testing.T) {
	t.Parallel()

	w := newIndexedTestWorld()

	if got := w.worldToCellX(2500); got != 1 {
		t.Fatalf("worldToCellX(2500) = %d, want 1", got)
	}
	if got := w.worldToCellY(4500); got != 2 {
		t.Fatalf("worldToCellY(4500) = %d, want 2", got)
	}
	if got := w.worldToCellX(-1); got != 4 {
		t.Fatalf("worldToCellX(-1) = %d, want 4", got)
	}
	if got := w.worldToCellY(10000); got != 0 {
		t.Fatalf("worldToCellY(10000) = %d, want 0", got)
	}
}

// TestIndexObjectPoint verifies index Object Point.
func TestIndexObjectPoint(t *testing.T) {
	t.Parallel()

	w := newIndexedTestWorld()
	id := PrimitiveID(1)

	p := Point{Id: id, X: 2500, Y: 4500}
	w.indexObject(p)

	cellHasOnlyID(t, w, 2, 1, id)
	cellIsEmpty(t, w, 0, 0)
	cellIsEmpty(t, w, 4, 4)
}

// TestIndexObjectCircleWithoutWrap verifies index Object Circle Without Wrap.
func TestIndexObjectCircleWithoutWrap(t *testing.T) {
	t.Parallel()

	w := newIndexedTestWorld()
	id := PrimitiveID(1)

	c := Circle{Id: id, X: 3000, Y: 2000, Radius: 900}
	w.indexObject(c)

	assertOccupiedCells(t, w, id,
		[2]int{0, 1},
		[2]int{1, 1},
	)
}

// TestIndexObjectCircleWrapsAcrossCorner verifies index Object Circle Wraps Across Corner.
func TestIndexObjectCircleWrapsAcrossCorner(t *testing.T) {
	t.Parallel()

	w := newIndexedTestWorld()
	id := PrimitiveID(1)

	c := Circle{Id: id, X: 500, Y: 500, Radius: 900}
	w.indexObject(c)

	assertOccupiedCells(t, w, id,
		[2]int{0, 0},
		[2]int{0, 4},
		[2]int{4, 0},
		[2]int{4, 4},
	)
}

// TestIndexObjectCircleCoversWholeWorld verifies index Object Circle Covers Whole World.
func TestIndexObjectCircleCoversWholeWorld(t *testing.T) {
	t.Parallel()

	w := newIndexedTestWorld()
	id := PrimitiveID(1)

	c := Circle{Id: id, X: 5000, Y: 5000, Radius: 6000}
	w.indexObject(c)

	want := make([][2]int, 0, 25)
	for row := 0; row < 5; row++ {
		for col := 0; col < 5; col++ {
			want = append(want, [2]int{row, col})
		}
	}

	assertOccupiedCells(t, w, id, want...)
}

// TestIndexObjectVerticalLineExpandsDegenerateX verifies index Object Vertical Line Expands Degenerate X.
func TestIndexObjectVerticalLineExpandsDegenerateX(t *testing.T) {
	t.Parallel()

	w := newIndexedTestWorld()
	id := PrimitiveID(1)

	l := Line{Id: id, X1: 3000, Y1: 1000, X2: 3000, Y2: 5000}
	w.indexObject(l)

	assertOccupiedCells(t, w, id,
		[2]int{0, 1},
		[2]int{1, 1},
		[2]int{2, 1},
	)
}

// TestIndexObjectHorizontalLineExpandsDegenerateY verifies index Object Horizontal Line Expands Degenerate Y.
func TestIndexObjectHorizontalLineExpandsDegenerateY(t *testing.T) {
	t.Parallel()

	w := newIndexedTestWorld()
	id := PrimitiveID(1)

	l := Line{Id: id, X1: 1000, Y1: 3000, X2: 5000, Y2: 3000}
	w.indexObject(l)

	assertOccupiedCells(t, w, id,
		[2]int{1, 0},
		[2]int{1, 1},
		[2]int{1, 2},
	)
}

// TestIndexObjectLineWrapsAcrossX verifies index Object Line Wraps Across X.
func TestIndexObjectLineWrapsAcrossX(t *testing.T) {
	t.Parallel()

	w := newIndexedTestWorld()
	id := PrimitiveID(1)

	l := Line{Id: id, X1: 9000, Y1: 3000, X2: 1000, Y2: 3000}
	w.indexObject(l)

	assertOccupiedCells(t, w, id,
		[2]int{1, 4},
		[2]int{1, 0},
	)
}

// TestIndexObjectLineWrapsAcrossY verifies index Object Line Wraps Across Y.
func TestIndexObjectLineWrapsAcrossY(t *testing.T) {
	t.Parallel()

	w := newIndexedTestWorld()
	id := PrimitiveID(1)

	l := Line{Id: id, X1: 3000, Y1: 9000, X2: 3000, Y2: 1000}
	w.indexObject(l)

	assertOccupiedCells(t, w, id,
		[2]int{4, 1},
		[2]int{0, 1},
	)
}

// TestIndexObjectLineTieCaseUsesDeterministicWrap verifies index Object Line Tie Case Uses Deterministic Wrap.
func TestIndexObjectLineTieCaseUsesDeterministicWrap(t *testing.T) {
	t.Parallel()

	w := newIndexedTestWorld()
	id := PrimitiveID(1)

	l := Line{Id: id, X1: 1000, Y1: 3000, X2: 6000, Y2: 3000}
	w.indexObject(l)

	assertOccupiedCells(t, w, id,
		[2]int{1, 3},
		[2]int{1, 4},
		[2]int{1, 0},
	)
}

type unknown struct {
	id PrimitiveID
}

func (u unknown) ID() PrimitiveID {
	return u.id
}

// TestIndexBBoxPanicsOnUnknownItemType verifies index B Box Panics On Unknown Item Type.
func TestIndexBBoxPanicsOnUnknownItemType(t *testing.T) {
	t.Parallel()

	w := newIndexedTestWorld()

	defer func() {
		if recover() == nil {
			t.Fatal("indexObject did not panic for unknown item type")
		}
	}()

	w.indexObject(unknown{id: PrimitiveID(1)})
}

// TestAddPoint_DefaultsPriorityAndStyle verifies add Point Defaults Priority And Style.
func TestAddPoint_DefaultsPriorityAndStyle(t *testing.T) {
	t.Parallel()

	w := NewWorld(10, 10)
	id, err := w.AddPoint(1, 1)
	require.NoError(t, err)

	obj := w.objects[id].(Point)
	require.Equal(t, DefaultPriorityPoint, obj.Priority)
	require.Equal(t, StyleIDDefaultPoint, obj.StyleID)
}

// TestAddCircle_DefaultsPriorityAndStyle verifies add Circle Defaults Priority And Style.
func TestAddCircle_DefaultsPriorityAndStyle(t *testing.T) {
	t.Parallel()

	w := NewWorld(10, 10)
	id, err := w.AddCircle(1, 1, 1)
	require.NoError(t, err)

	obj := w.objects[id].(Circle)
	require.Equal(t, DefaultPriorityCircle, obj.Priority)
	require.Equal(t, StyleIDDefaultCircle, obj.StyleID)
}

// TestAddLine_DefaultsPriorityAndStyle verifies add Line Defaults Priority And Style.
func TestAddLine_DefaultsPriorityAndStyle(t *testing.T) {
	t.Parallel()

	w := NewWorld(10, 10)
	id, err := w.AddLine(1, 1, 2, 2)
	require.NoError(t, err)

	obj := w.objects[id].(Line)
	require.Equal(t, DefaultPriorityLine, obj.Priority)
	require.Equal(t, StyleIDDefaultLine, obj.StyleID)
}

// TestAddStyleLine_ThenUseStyleID verifies add Style Line Then Use Style ID.
func TestAddStyleLine_ThenUseStyleID(t *testing.T) {
	t.Parallel()

	w := NewWorld(10, 10)

	width := 5.0
	ov := StyleOverride{StrokeWidthPx: &width}
	styleID := w.AddStyleLine(ov)

	id, err := w.AddLine(1, 1, 2, 2, LineWithStyleID(styleID), LineWithPriority(777))
	require.NoError(t, err)

	obj := w.objects[id].(Line)
	require.Equal(t, 777, obj.Priority)
	require.Equal(t, styleID, obj.StyleID)

	s, ok := w.styles.Get(styleID)
	require.True(t, ok)
	require.Equal(t, 5.0, s.StrokeWidthPx)
}

// TestAddPoint_WithOverride_CreatesDerivedStyle verifies add Point With Override Creates Derived Style.
func TestAddPoint_WithOverride_CreatesDerivedStyle(t *testing.T) {
	t.Parallel()

	w := NewWorld(10, 10)

	newRadius := 9.0
	ov := StyleOverride{PointRadiusPx: &newRadius}

	id, err := w.AddPoint(1, 1, PointWithStyleOverride(ov))
	require.NoError(t, err)

	obj := w.objects[id].(Point)
	require.NotEqual(t, StyleIDDefaultPoint, obj.StyleID)

	s, ok := w.styles.Get(obj.StyleID)
	require.True(t, ok)
	require.Equal(t, 9.0, s.PointRadiusPx)
}

// TestExplicitStyleID_WinsOverOverride verifies explicit Style ID Wins Over Override.
func TestExplicitStyleID_WinsOverOverride(t *testing.T) {
	t.Parallel()

	w := NewWorld(10, 10)

	red := color.RGBA{R: 255, A: 255}
	styleID := w.AddStyleCircle(StyleOverride{FillColor: red})

	// Try to override radius in options too; StyleID must win, override must be ignored.
	width := 123.0
	id, err := w.AddCircle(2, 2, 1,
		CircleWithStyleID(styleID),
		CircleWithStyleOverride(StyleOverride{StrokeWidthPx: &width}),
	)
	require.NoError(t, err)

	obj := w.objects[id].(Circle)
	require.Equal(t, styleID, obj.StyleID)

	s, ok := w.styles.Get(styleID)
	require.True(t, ok)
	require.Equal(t, red, s.FillColor)
	// width override must not affect styleID.
	require.NotEqual(t, 123.0, s.StrokeWidthPx)
}

// TestWorldPrimitiveID_ReusesFreedIDs verifies world Primitive ID Reuses Freed I Ds.
func TestWorldPrimitiveID_ReusesFreedIDs(t *testing.T) {
	t.Parallel()

	w := NewWorld(10, 10)

	id1, err := w.AddPoint(1, 1)
	require.NoError(t, err)

	id2, err := w.AddPoint(2, 2)
	require.NoError(t, err)

	require.NotEqual(t, id1, id2)

	require.NoError(t, w.Remove(id1))

	id3, err := w.AddPoint(3, 3)
	require.NoError(t, err)

	// LIFO free-list: id1 should be reused.
	require.Equal(t, id1, id3)
}

// TestWorldRemove_UnknownID verifies world Remove Unknown ID.
func TestWorldRemove_UnknownID(t *testing.T) {
	t.Parallel()

	w := NewWorld(10, 10)
	err := w.Remove(12345)
	require.ErrorIs(t, err, errNoSuchObject)
}

type gridCell struct {
	Row int
	Col int
}

func newTestWorld(wReal, hReal int) *World {
	w := NewWorld(wReal, hReal)
	w.SetCircleRadiusScaleFp(SCALE)
	return w
}

func countObjectInGrid(g *World, id PrimitiveID) int {
	count := 0
	for row := range g.grid {
		for col := range g.grid[row] {
			for _, item := range g.grid[row][col] {
				if item.ID() == id {
					count++
				}
			}
		}
	}
	return count
}

func hasObjectInCell(g *World, row, col int, id PrimitiveID) bool {
	for _, item := range g.grid[row][col] {
		if item.ID() == id {
			return true
		}
	}
	return false
}

// TestViewportPxToWorldFixed verifies viewport Px To World Fixed.
func TestViewportPxToWorldFixed(t *testing.T) {
	tests := []struct {
		name             string
		viewportWidthPx  int
		viewportHeightPx int
		cameraZoom       int
		wantWidth        int
		wantHeight       int
	}{
		{
			name:             "zoom 1.0",
			viewportWidthPx:  500,
			viewportHeightPx: 400,
			cameraZoom:       SCALE,
			wantWidth:        500 * SCALE,
			wantHeight:       400 * SCALE,
		},
		{
			name:             "zoom 2.0",
			viewportWidthPx:  500,
			viewportHeightPx: 400,
			cameraZoom:       2 * SCALE,
			wantWidth:        250 * SCALE,
			wantHeight:       200 * SCALE,
		},
		{
			name:             "zoom below 1.0",
			viewportWidthPx:  550,
			viewportHeightPx: 550,
			cameraZoom:       917,
			wantWidth:        599781,
			wantHeight:       599781,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gotW, gotH := viewportPxToWorldFixed(tt.viewportWidthPx, tt.viewportHeightPx, tt.cameraZoom)
			require.Equal(t, tt.wantWidth, gotW)
			require.Equal(t, tt.wantHeight, gotH)
		})
	}
}

// TestSplitByWrap_ZeroOrNegativeSizeReturnsNil verifies split By Wrap Zero Or Negative Size Returns Nil.
func TestSplitByWrap_ZeroOrNegativeSizeReturnsNil(t *testing.T) {
	tests := []struct {
		name       string
		minX, maxX int
		minY, maxY int
	}{
		{
			name: "zero width",
			minX: 100, maxX: 100,
			minY: 50, maxY: 100,
		},
		{
			name: "zero height",
			minX: 100, maxX: 200,
			minY: 50, maxY: 50,
		},
		{
			name: "negative width",
			minX: 200, maxX: 100,
			minY: 50, maxY: 100,
		},
		{
			name: "negative height",
			minX: 100, maxX: 200,
			minY: 100, maxY: 50,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			rects := splitByWrap(600, 400, tt.minX, tt.maxX, tt.minY, tt.maxY)
			require.Nil(t, rects)
		})
	}
}

// TestSplitByWrap_XWrapUsesWorldWidth verifies split By Wrap X Wrap Uses World Width.
func TestSplitByWrap_XWrapUsesWorldWidth(t *testing.T) {
	rects := splitByWrap(
		600, 400,
		500, 650,
		50, 100,
	)

	require.Len(t, rects, 2)
	require.Equal(t, Rect{minX: 500, maxX: 600, minY: 50, maxY: 100}, rects[0])
	require.Equal(t, Rect{minX: 0, maxX: 50, minY: 50, maxY: 100}, rects[1])
}

// TestSplitByWrap_YWrapUsesWorldHeight verifies split By Wrap Y Wrap Uses World Height.
func TestSplitByWrap_YWrapUsesWorldHeight(t *testing.T) {
	rects := splitByWrap(
		600, 400,
		50, 100,
		350, 450,
	)

	require.Len(t, rects, 2)
	require.Equal(t, Rect{minX: 50, maxX: 100, minY: 350, maxY: 400}, rects[0])
	require.Equal(t, Rect{minX: 50, maxX: 100, minY: 0, maxY: 50}, rects[1])
}

// TestSplitByWrap_XAndYWrap verifies split By Wrap X And Y Wrap.
func TestSplitByWrap_XAndYWrap(t *testing.T) {
	rects := splitByWrap(
		600, 400,
		550, 650,
		350, 450,
	)

	require.Len(t, rects, 4)
	require.ElementsMatch(t, []Rect{
		{minX: 550, maxX: 600, minY: 350, maxY: 400},
		{minX: 550, maxX: 600, minY: 0, maxY: 50},
		{minX: 0, maxX: 50, minY: 350, maxY: 400},
		{minX: 0, maxX: 50, minY: 0, maxY: 50},
	}, rects)
}

// TestSplitByWrap_NoWrapInsideWorld verifies split By Wrap No Wrap Inside World.
func TestSplitByWrap_NoWrapInsideWorld(t *testing.T) {
	rects := splitByWrap(
		600, 400,
		100, 200,
		50, 100,
	)

	require.Len(t, rects, 1)
	require.Equal(t, Rect{minX: 100, maxX: 200, minY: 50, maxY: 100}, rects[0])
}

// TestSplitByWrap_FullWorldCoverageOnEqualWidth verifies split By Wrap Full World Coverage On Equal Width.
func TestSplitByWrap_FullWorldCoverageOnEqualWidth(t *testing.T) {
	rects := splitByWrap(
		600, 400,
		0, 600,
		50, 100,
	)

	require.Len(t, rects, 1)
	require.Equal(t, Rect{minX: 0, maxX: 600, minY: 50, maxY: 100}, rects[0])
}

// TestSplitByWrap_FullWorldCoverageOnEqualHeight verifies split By Wrap Full World Coverage On Equal Height.
func TestSplitByWrap_FullWorldCoverageOnEqualHeight(t *testing.T) {
	rects := splitByWrap(
		600, 400,
		50, 100,
		0, 400,
	)

	require.Len(t, rects, 1)
	require.Equal(t, Rect{minX: 50, maxX: 100, minY: 0, maxY: 400}, rects[0])
}

// TestSplitByWrap_FullWorldCoverageOnBothAxes verifies split By Wrap Full World Coverage On Both Axes.
func TestSplitByWrap_FullWorldCoverageOnBothAxes(t *testing.T) {
	rects := splitByWrap(
		600, 400,
		0, 600,
		0, 400,
	)

	require.Len(t, rects, 1)
	require.Equal(t, Rect{minX: 0, maxX: 600, minY: 0, maxY: 400}, rects[0])
}

// TestWorldToCell verifies world To Cell.
func TestWorldToCell(t *testing.T) {
	tests := []struct {
		name      string
		value     int
		worldSize int
		cells     int
		cellSize  int
		want      int
	}{
		{
			name:      "simple inside world",
			value:     150,
			worldSize: 600,
			cells:     6,
			cellSize:  100,
			want:      1,
		},
		{
			name:      "negative wraps to last cell",
			value:     -1,
			worldSize: 600,
			cells:     6,
			cellSize:  100,
			want:      5,
		},
		{
			name:      "exact world size wraps to zero",
			value:     600,
			worldSize: 600,
			cells:     6,
			cellSize:  100,
			want:      0,
		},
		{
			name:      "large positive wraps correctly",
			value:     650,
			worldSize: 600,
			cells:     6,
			cellSize:  100,
			want:      0,
		},
		{
			name:      "last in-range value lands in last cell",
			value:     599,
			worldSize: 600,
			cells:     6,
			cellSize:  100,
			want:      5,
		},
		{name: "first cell", value: 0, worldSize: 10000, cells: 5, cellSize: 2000, want: 0},
		{name: "middle cell", value: 2500, worldSize: 10000, cells: 5, cellSize: 2000, want: 1},
		{name: "last exact world point wraps to zero", value: 10000, worldSize: 10000, cells: 5, cellSize: 2000, want: 0},
		{name: "negative wraps to last", value: -1, worldSize: 10000, cells: 5, cellSize: 2000, want: 4},
		{name: "partial last cell is clamped", value: 9999, worldSize: 10000, cells: 4, cellSize: 3000, want: 3},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := worldToCell(tt.value, tt.worldSize, tt.cells, tt.cellSize)
			require.Equal(t, tt.want, got)
		})
	}
}

// TestResetGrid_UsesWidthForColsAndHeightForRows verifies reset Grid Uses Width For Cols And Height For Rows.
func TestResetGrid_UsesWidthForColsAndHeightForRows(t *testing.T) {
	g := newTestWorld(600, 400)

	g.resetGrid(100 * SCALE)

	require.Equal(t, 6, g.cols)
	require.Equal(t, 4, g.rows)
	require.Len(t, g.grid, 4)
	require.Len(t, g.grid[0], 6)
}

// TestIndexPoint verifies index Point.
func TestIndexPoint(t *testing.T) {
	g := newTestWorld(600, 600)
	g.resetGrid(100 * SCALE)

	id := PrimitiveID(1)
	p := Point{
		Id: id,
		X:  150 * SCALE,
		Y:  250 * SCALE,
	}

	g.indexObject(p)

	require.True(t, hasObjectInCell(g, 2, 1, id))
	require.Equal(t, 1, countObjectInGrid(g, id))
}

// TestIndexPoint_WrapsNegativeCoordinates verifies index Point Wraps Negative Coordinates.
func TestIndexPoint_WrapsNegativeCoordinates(t *testing.T) {
	g := newTestWorld(600, 600)
	g.resetGrid(100 * SCALE)

	id := PrimitiveID(1)
	p := Point{
		Id: id,
		X:  -1,
		Y:  -1,
	}

	g.indexObject(p)

	require.True(t, hasObjectInCell(g, 5, 5, id))
	require.Equal(t, 1, countObjectInGrid(g, id))
}

// TestIndexCircle_WrapsAcrossLeftAndTopEdges verifies index Circle Wraps Across Left And Top Edges.
func TestIndexCircle_WrapsAcrossLeftAndTopEdges(t *testing.T) {
	g := newTestWorld(600, 600)
	g.resetGrid(100 * SCALE)

	id := PrimitiveID(1)
	c := Circle{
		Id:     id,
		X:      50 * SCALE,
		Y:      50 * SCALE,
		Radius: 75 * SCALE,
	}

	g.indexObject(c)

	// The circle spans [-25..125] on both axes.
	// It must appear both near zero and near the wrapped end.
	require.True(t, hasObjectInCell(g, 0, 0, id))
	require.True(t, hasObjectInCell(g, 0, 5, id))
	require.True(t, hasObjectInCell(g, 5, 0, id))
	require.True(t, hasObjectInCell(g, 5, 5, id))

	// It also extends into the next cells near the origin.
	require.True(t, hasObjectInCell(g, 0, 1, id))
	require.True(t, hasObjectInCell(g, 1, 0, id))
	require.True(t, hasObjectInCell(g, 1, 1, id))
}

// TestIndexCircle_NoWrap verifies index Circle No Wrap.
func TestIndexCircle_NoWrap(t *testing.T) {
	g := newTestWorld(600, 600)
	g.resetGrid(100 * SCALE)

	id := PrimitiveID(1)
	c := Circle{
		Id:     id,
		X:      300 * SCALE,
		Y:      300 * SCALE,
		Radius: 50 * SCALE,
	}

	g.indexObject(c)

	require.True(t, hasObjectInCell(g, 2, 2, id))
	require.True(t, hasObjectInCell(g, 2, 3, id))
	require.True(t, hasObjectInCell(g, 3, 2, id))
	require.True(t, hasObjectInCell(g, 3, 3, id))
}

// TestIndexCircle_CoversWholeWorldWhenLargerThanWorld verifies index Circle Covers Whole World When Larger Than World.
func TestIndexCircle_CoversWholeWorldWhenLargerThanWorld(t *testing.T) {
	g := newTestWorld(600, 600)
	g.resetGrid(100 * SCALE)

	id := PrimitiveID(1)
	c := Circle{
		Id:     id,
		X:      300 * SCALE,
		Y:      300 * SCALE,
		Radius: 400 * SCALE,
	}

	g.indexObject(c)

	for row := 0; row < g.rows; row++ {
		for col := 0; col < g.cols; col++ {
			require.Truef(t, hasObjectInCell(g, row, col, id), "missing object in row=%d col=%d", row, col)
		}
	}
}

// TestIndexLine_HorizontalWrap verifies index Line Horizontal Wrap.
func TestIndexLine_HorizontalWrap(t *testing.T) {
	g := newTestWorld(600, 600)
	g.resetGrid(100 * SCALE)

	id := PrimitiveID(1)
	l := Line{
		Id: id,
		X1: 590 * SCALE,
		Y1: 200 * SCALE,
		X2: 10 * SCALE,
		Y2: 200 * SCALE,
	}

	g.indexObject(l)

	// The shortest torus representation crosses the right/left border.
	require.True(t, hasObjectInCell(g, 2, 5, id))
	require.True(t, hasObjectInCell(g, 2, 0, id))
}

// TestIndexLine_VerticalWrap verifies index Line Vertical Wrap.
func TestIndexLine_VerticalWrap(t *testing.T) {
	g := newTestWorld(600, 600)
	g.resetGrid(100 * SCALE)

	id := PrimitiveID(1)
	l := Line{
		Id: id,
		X1: 200 * SCALE,
		Y1: 590 * SCALE,
		X2: 200 * SCALE,
		Y2: 10 * SCALE,
	}

	g.indexObject(l)

	require.True(t, hasObjectInCell(g, 5, 2, id))
	require.True(t, hasObjectInCell(g, 0, 2, id))
}

// TestIndexLine_DiagonalWrapBothAxes verifies index Line Diagonal Wrap Both Axes.
func TestIndexLine_DiagonalWrapBothAxes(t *testing.T) {
	g := newTestWorld(600, 600)
	g.resetGrid(100 * SCALE)

	id := PrimitiveID(1)
	l := Line{
		Id: id,
		X1: 590 * SCALE,
		Y1: 590 * SCALE,
		X2: 10 * SCALE,
		Y2: 10 * SCALE,
	}

	g.indexObject(l)

	require.True(t, hasObjectInCell(g, 5, 5, id))
	require.True(t, hasObjectInCell(g, 0, 0, id))
}

// TestIndexLine_HorizontalNoWrap_DegenerateBBoxStillIndexes verifies index Line Horizontal No Wrap Degenerate B Box Still Indexes.
func TestIndexLine_HorizontalNoWrap_DegenerateBBoxStillIndexes(t *testing.T) {
	g := newTestWorld(600, 600)
	g.resetGrid(100 * SCALE)

	id := PrimitiveID(1)
	l := Line{
		Id: id,
		X1: 100 * SCALE,
		Y1: 200 * SCALE,
		X2: 300 * SCALE,
		Y2: 200 * SCALE,
	}

	g.indexObject(l)

	// The indexed interval is half-open: [100,300).
	// Therefore it occupies columns 1 and 2, but not column 3.
	require.True(t, hasObjectInCell(g, 2, 1, id))
	require.True(t, hasObjectInCell(g, 2, 2, id))
	require.False(t, hasObjectInCell(g, 2, 3, id))
}

// TestIndexLine_VerticalNoWrap_DegenerateBBoxStillIndexes verifies index Line Vertical No Wrap Degenerate B Box Still Indexes.
func TestIndexLine_VerticalNoWrap_DegenerateBBoxStillIndexes(t *testing.T) {
	g := newTestWorld(600, 600)
	g.resetGrid(100 * SCALE)

	id := PrimitiveID(1)
	l := Line{
		Id: id,
		X1: 200 * SCALE,
		Y1: 100 * SCALE,
		X2: 200 * SCALE,
		Y2: 300 * SCALE,
	}

	g.indexObject(l)

	// The indexed interval is half-open: [100,300).
	// Therefore it occupies rows 1 and 2, but not row 3.
	require.True(t, hasObjectInCell(g, 1, 2, id))
	require.True(t, hasObjectInCell(g, 2, 2, id))
	require.False(t, hasObjectInCell(g, 3, 2, id))
}

// TestIndexLine_ZeroLengthIndexesSingleCell verifies index Line Zero Length Indexes Single Cell.
func TestIndexLine_ZeroLengthIndexesSingleCell(t *testing.T) {
	g := newTestWorld(600, 600)
	g.resetGrid(100 * SCALE)

	id := PrimitiveID(1)
	l := Line{
		Id: id,
		X1: 250 * SCALE,
		Y1: 350 * SCALE,
		X2: 250 * SCALE,
		Y2: 350 * SCALE,
	}

	g.indexObject(l)

	require.True(t, hasObjectInCell(g, 3, 2, id))
	require.Equal(t, 1, countObjectInGrid(g, id))
}

// TestIndexLine_ExactlyOnCellBoundaryUsesHalfOpenInterval verifies index Line Exactly On Cell Boundary Uses Half Open Interval.
func TestIndexLine_ExactlyOnCellBoundaryUsesHalfOpenInterval(t *testing.T) {
	g := newTestWorld(600, 600)
	g.resetGrid(100 * SCALE)

	id := PrimitiveID(1)
	l := Line{
		Id: id,
		X1: 200 * SCALE,
		Y1: 100 * SCALE,
		X2: 400 * SCALE,
		Y2: 100 * SCALE,
	}

	g.indexObject(l)

	// The indexed interval is [200,400), so it must occupy columns 2 and 3 only.
	require.True(t, hasObjectInCell(g, 1, 2, id))
	require.True(t, hasObjectInCell(g, 1, 3, id))
	require.False(t, hasObjectInCell(g, 1, 4, id))
}

func collectOccupiedCells(g *World, id PrimitiveID) []gridCell {
	var cells []gridCell
	for row := range g.grid {
		for col := range g.grid[row] {
			for _, item := range g.grid[row][col] {
				if item.ID() == id {
					cells = append(cells, gridCell{Row: row, Col: col})
					break
				}
			}
		}
	}
	return cells
}

func allGridCells(rows, cols int) []gridCell {
	cells := make([]gridCell, 0, rows*cols)
	for row := 0; row < rows; row++ {
		for col := 0; col < cols; col++ {
			cells = append(cells, gridCell{Row: row, Col: col})
		}
	}
	return cells
}

func requireIndexedExactlyInCells(t *testing.T, g *World, id PrimitiveID, want []gridCell) {
	t.Helper()

	got := collectOccupiedCells(g, id)

	require.ElementsMatchf(
		t,
		want,
		got,
		"unexpected indexed cells for object %d",
		id,
	)
}

// TestIndexObject_Point_TableDriven verifies index Object Point Table Driven.
func TestIndexObject_Point_TableDriven(t *testing.T) {
	tests := []struct {
		name      string
		worldW    int
		worldH    int
		cellSize  int
		item      Point
		wantCells []gridCell
	}{
		{
			name:     "point inside world",
			worldW:   600,
			worldH:   600,
			cellSize: 100 * SCALE,
			item: Point{
				Id: PrimitiveID(1),
				X:  150 * SCALE,
				Y:  250 * SCALE,
			},
			wantCells: []gridCell{
				{Row: 2, Col: 1},
			},
		},
		{
			name:     "point wraps from negative coordinates to last cell",
			worldW:   600,
			worldH:   600,
			cellSize: 100 * SCALE,
			item: Point{
				Id: PrimitiveID(1),
				X:  -1,
				Y:  -1,
			},
			wantCells: []gridCell{
				{Row: 5, Col: 5},
			},
		},
		{
			name:     "point exactly at world boundary wraps to zero cell",
			worldW:   600,
			worldH:   600,
			cellSize: 100 * SCALE,
			item: Point{
				Id: PrimitiveID(1),
				X:  600 * SCALE,
				Y:  600 * SCALE,
			},
			wantCells: []gridCell{
				{Row: 0, Col: 0},
			},
		},
		{
			name:     "point on cell boundary belongs to that cell",
			worldW:   600,
			worldH:   600,
			cellSize: 100 * SCALE,
			item: Point{
				Id: PrimitiveID(1),
				X:  200 * SCALE,
				Y:  300 * SCALE,
			},
			wantCells: []gridCell{
				{Row: 3, Col: 2},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := newTestWorld(tt.worldW, tt.worldH)
			g.resetGrid(tt.cellSize)

			g.indexObject(tt.item)

			requireIndexedExactlyInCells(t, g, tt.item.Id, tt.wantCells)
		})
	}
}

// TestIndexObject_Circle_TableDriven verifies index Object Circle Table Driven.
func TestIndexObject_Circle_TableDriven(t *testing.T) {
	tests := []struct {
		name      string
		worldW    int
		worldH    int
		cellSize  int
		item      Circle
		wantCells []gridCell
	}{
		{
			name:     "circle without wrap",
			worldW:   600,
			worldH:   600,
			cellSize: 100 * SCALE,
			item: Circle{
				Id:     PrimitiveID(1),
				X:      300 * SCALE,
				Y:      300 * SCALE,
				Radius: 50 * SCALE,
			},
			wantCells: []gridCell{
				{Row: 2, Col: 2},
				{Row: 2, Col: 3},
				{Row: 3, Col: 2},
				{Row: 3, Col: 3},
			},
		},
		{
			name:     "circle wraps across left and top edges",
			worldW:   600,
			worldH:   600,
			cellSize: 100 * SCALE,
			item: Circle{
				Id:     PrimitiveID(1),
				X:      50 * SCALE,
				Y:      50 * SCALE,
				Radius: 75 * SCALE,
			},
			wantCells: []gridCell{
				{Row: 5, Col: 5},
				{Row: 5, Col: 0},
				{Row: 5, Col: 1},
				{Row: 0, Col: 5},
				{Row: 0, Col: 0},
				{Row: 0, Col: 1},
				{Row: 1, Col: 5},
				{Row: 1, Col: 0},
				{Row: 1, Col: 1},
			},
		},
		{
			name:     "circle wraps across right edge only",
			worldW:   600,
			worldH:   600,
			cellSize: 100 * SCALE,
			item: Circle{
				Id:     PrimitiveID(1),
				X:      575 * SCALE,
				Y:      300 * SCALE,
				Radius: 50 * SCALE,
			},
			wantCells: []gridCell{
				{Row: 2, Col: 5},
				{Row: 2, Col: 0},
				{Row: 3, Col: 5},
				{Row: 3, Col: 0},
			},
		},
		{
			name:     "circle wraps across bottom edge only",
			worldW:   600,
			worldH:   600,
			cellSize: 100 * SCALE,
			item: Circle{
				Id:     PrimitiveID(1),
				X:      300 * SCALE,
				Y:      575 * SCALE,
				Radius: 50 * SCALE,
			},
			wantCells: []gridCell{
				{Row: 5, Col: 2},
				{Row: 5, Col: 3},
				{Row: 0, Col: 2},
				{Row: 0, Col: 3},
			},
		},
		{
			name:     "circle larger than world covers the whole grid",
			worldW:   600,
			worldH:   600,
			cellSize: 100 * SCALE,
			item: Circle{
				Id:     PrimitiveID(1),
				X:      300 * SCALE,
				Y:      300 * SCALE,
				Radius: 400 * SCALE,
			},
			wantCells: allGridCells(6, 6),
		},
		{
			name:     "circle touching boundaries exactly uses half-open indexing",
			worldW:   600,
			worldH:   600,
			cellSize: 100 * SCALE,
			item: Circle{
				Id:     PrimitiveID(1),
				X:      300 * SCALE,
				Y:      300 * SCALE,
				Radius: 100 * SCALE, // bbox [200, 400) x [200, 400)
			},
			wantCells: []gridCell{
				{Row: 2, Col: 2},
				{Row: 2, Col: 3},
				{Row: 3, Col: 2},
				{Row: 3, Col: 3},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := newTestWorld(tt.worldW, tt.worldH)
			g.resetGrid(tt.cellSize)

			g.indexObject(tt.item)

			requireIndexedExactlyInCells(t, g, tt.item.Id, tt.wantCells)
		})
	}
}

// TestIndexObject_Line_TableDriven verifies index Object Line Table Driven.
func TestIndexObject_Line_TableDriven(t *testing.T) {
	tests := []struct {
		name      string
		worldW    int
		worldH    int
		cellSize  int
		item      Line
		wantCells []gridCell
	}{
		{
			name:     "horizontal line without wrap",
			worldW:   600,
			worldH:   600,
			cellSize: 100 * SCALE,
			item: Line{
				Id: PrimitiveID(1),
				X1: 100 * SCALE,
				Y1: 200 * SCALE,
				X2: 300 * SCALE,
				Y2: 200 * SCALE,
			},
			// Half-open interval [100,300), so only cols 1 and 2.
			wantCells: []gridCell{
				{Row: 2, Col: 1},
				{Row: 2, Col: 2},
			},
		},
		{
			name:     "vertical line without wrap",
			worldW:   600,
			worldH:   600,
			cellSize: 100 * SCALE,
			item: Line{
				Id: PrimitiveID(1),
				X1: 200 * SCALE,
				Y1: 100 * SCALE,
				X2: 200 * SCALE,
				Y2: 300 * SCALE,
			},
			// Half-open interval [100,300), so only rows 1 and 2.
			wantCells: []gridCell{
				{Row: 1, Col: 2},
				{Row: 2, Col: 2},
			},
		},
		{
			name:     "horizontal line wraps across left right border",
			worldW:   600,
			worldH:   600,
			cellSize: 100 * SCALE,
			item: Line{
				Id: PrimitiveID(1),
				X1: 590 * SCALE,
				Y1: 200 * SCALE,
				X2: 10 * SCALE,
				Y2: 200 * SCALE,
			},
			wantCells: []gridCell{
				{Row: 2, Col: 5},
				{Row: 2, Col: 0},
			},
		},
		{
			name:     "vertical line wraps across top bottom border",
			worldW:   600,
			worldH:   600,
			cellSize: 100 * SCALE,
			item: Line{
				Id: PrimitiveID(1),
				X1: 200 * SCALE,
				Y1: 590 * SCALE,
				X2: 200 * SCALE,
				Y2: 10 * SCALE,
			},
			wantCells: []gridCell{
				{Row: 5, Col: 2},
				{Row: 0, Col: 2},
			},
		},
		{
			name:     "diagonal line wraps across both axes",
			worldW:   600,
			worldH:   600,
			cellSize: 100 * SCALE,
			item: Line{
				Id: PrimitiveID(1),
				X1: 590 * SCALE,
				Y1: 590 * SCALE,
				X2: 10 * SCALE,
				Y2: 10 * SCALE,
			},
			wantCells: []gridCell{
				{Row: 5, Col: 5},
				{Row: 5, Col: 0},
				{Row: 0, Col: 5},
				{Row: 0, Col: 0},
			},
		},
		{
			name:     "zero length line indexes a single cell",
			worldW:   600,
			worldH:   600,
			cellSize: 100 * SCALE,
			item: Line{
				Id: PrimitiveID(1),
				X1: 250 * SCALE,
				Y1: 350 * SCALE,
				X2: 250 * SCALE,
				Y2: 350 * SCALE,
			},
			wantCells: []gridCell{
				{Row: 3, Col: 2},
			},
		},
		{
			name:     "line exactly on cell boundaries follows half-open interval",
			worldW:   600,
			worldH:   600,
			cellSize: 100 * SCALE,
			item: Line{
				Id: PrimitiveID(1),
				X1: 200 * SCALE,
				Y1: 100 * SCALE,
				X2: 400 * SCALE,
				Y2: 100 * SCALE,
			},
			// [200,400) => cols 2 and 3 only.
			wantCells: []gridCell{
				{Row: 1, Col: 2},
				{Row: 1, Col: 3},
			},
		},
		{
			name:     "diagonal line without wrap indexes its full bbox footprint",
			worldW:   600,
			worldH:   600,
			cellSize: 100 * SCALE,
			item: Line{
				Id: PrimitiveID(1),
				X1: 100 * SCALE,
				Y1: 100 * SCALE,
				X2: 300 * SCALE,
				Y2: 300 * SCALE,
			},
			// Indexing is bbox-based, not raster-based.
			// The bbox is [100,300) x [100,300), so four cells.
			wantCells: []gridCell{
				{Row: 1, Col: 1},
				{Row: 1, Col: 2},
				{Row: 2, Col: 1},
				{Row: 2, Col: 2},
			},
		},
		{
			name:     "horizontal wrap exactly on borders still indexes both edge cells",
			worldW:   600,
			worldH:   600,
			cellSize: 100 * SCALE,
			item: Line{
				Id: PrimitiveID(1),
				X1: 600 * SCALE,
				Y1: 100 * SCALE,
				X2: 0,
				Y2: 100 * SCALE,
			},
			// After wrapping both endpoints are equivalent to zero-width on the edge.
			// The degenerate bbox expansion should still index the first cell only.
			wantCells: []gridCell{
				{Row: 1, Col: 0},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			g := newTestWorld(tt.worldW, tt.worldH)
			g.resetGrid(tt.cellSize)

			g.indexObject(tt.item)

			requireIndexedExactlyInCells(t, g, tt.item.Id, tt.wantCells)
		})
	}
}

// TestIndexOnViewportChange_RebuildsGridAndIndexesObjects verifies index On Viewport Change Rebuilds Grid And Indexes Objects.
func TestIndexOnViewportChange_RebuildsGridAndIndexesObjects(t *testing.T) {
	g := newTestWorld(600, 400)

	pID := PrimitiveID(1)
	cID := PrimitiveID(2)
	lID := PrimitiveID(3)

	g.objects[pID] = Point{
		Id: pID,
		X:  50 * SCALE,
		Y:  50 * SCALE,
	}
	g.objects[cID] = Circle{
		Id:     cID,
		X:      300 * SCALE,
		Y:      200 * SCALE,
		Radius: 50 * SCALE,
	}
	g.objects[lID] = Line{
		Id: lID,
		X1: 590 * SCALE,
		Y1: 100 * SCALE,
		X2: 10 * SCALE,
		Y2: 100 * SCALE,
	}

	g.IndexOnViewportChange(500, 300, 1.)

	require.Greater(t, g.cellSize, 0)
	require.Equal(t, ceilDiv(g.W, g.cellSize), g.cols)
	require.Equal(t, ceilDiv(g.H, g.cellSize), g.rows)

	require.Greaterf(t, countObjectInGrid(g, pID), 0, "point %s was not indexed", pID)
	require.Greaterf(t, countObjectInGrid(g, cID), 0, "circle %s was not indexed", cID)
	require.Greaterf(t, countObjectInGrid(g, lID), 0, "line %s was not indexed", lID)
}

// TestIndexOnViewportChange_RebuildsGridShapeForNonSquareWorld verifies index On Viewport Change Rebuilds Grid Shape For Non Square World.
func TestIndexOnViewportChange_RebuildsGridShapeForNonSquareWorld(t *testing.T) {
	g := newTestWorld(600, 400)
	g.IndexOnViewportChange(500, 300, 1.)

	require.Equal(t, ceilDiv(g.W, g.cellSize), g.cols)
	require.Equal(t, ceilDiv(g.H, g.cellSize), g.rows)
	require.Len(t, g.grid, g.rows)
	require.Len(t, g.grid[0], g.cols)
}

// TestIndexOnViewportChange_ReindexesAfterCellSizeChange verifies index On Viewport Change Reindexes After Cell Size Change.
func TestIndexOnViewportChange_ReindexesAfterCellSizeChange(t *testing.T) {
	g := newTestWorld(600, 600)

	id := PrimitiveID(1)
	g.objects[id] = Circle{
		Id:     id,
		X:      300 * SCALE,
		Y:      300 * SCALE,
		Radius: 50 * SCALE,
	}

	g.IndexOnViewportChange(500, 500, 1.)
	firstCellSize := g.cellSize
	firstCount := countObjectInGrid(g, id)

	g.IndexOnViewportChange(200, 200, 1.)
	secondCellSize := g.cellSize
	secondCount := countObjectInGrid(g, id)

	require.NotEqual(t, firstCellSize, secondCellSize)
	require.Greater(t, firstCount, 0)
	require.Greater(t, secondCount, 0)

	if firstCellSize != secondCellSize && firstCount == secondCount {
		t.Logf(
			"cell size changed from %d to %d, but the indexed cell count happened to stay equal (%d)",
			firstCellSize,
			secondCellSize,
			firstCount,
		)
	}
}

// TestPrimitiveIndexing_ErrorMessagesStayReadable verifies primitive Indexing Error Messages Stay Readable.
func TestPrimitiveIndexing_ErrorMessagesStayReadable(t *testing.T) {
	g := newTestWorld(600, 600)
	g.resetGrid(100 * SCALE)

	id := PrimitiveID(1)
	p := Point{
		Id: id,
		X:  100 * SCALE,
		Y:  100 * SCALE,
	}

	g.indexObject(p)

	got := collectOccupiedCells(g, id)
	require.NotEmpty(t, got, fmt.Sprintf("object %d should occupy at least one cell", id))
}

// TestPrimitiveIDs verifies primitive I Ds.
func TestPrimitiveIDs(t *testing.T) {
	t.Parallel()

	id1 := PrimitiveID(1)
	id2 := PrimitiveID(2)
	id3 := PrimitiveID(3)

	p := Point{Id: id1}
	l := Line{Id: id2}
	c := Circle{Id: id3}

	if got := p.ID(); got != id1 {
		t.Fatalf("Point.ID() = %v, want %v", got, id1)
	}
	if got := l.ID(); got != id2 {
		t.Fatalf("Line.ID() = %v, want %v", got, id2)
	}
	if got := c.ID(); got != id3 {
		t.Fatalf("Circle.ID() = %v, want %v", got, id3)
	}
}

// TestLineMinMax verifies line Min Max.
func TestLineMinMax(t *testing.T) {
	t.Parallel()

	l := Line{
		X1: 7000, Y1: 2000,
		X2: 1000, Y2: 9000,
	}

	if got := l.MinX(); got != 1000 {
		t.Fatalf("Line.MinX() = %d, want 1000", got)
	}
	if got := l.MaxX(); got != 7000 {
		t.Fatalf("Line.MaxX() = %d, want 7000", got)
	}
	if got := l.MinY(); got != 2000 {
		t.Fatalf("Line.MinY() = %d, want 2000", got)
	}
	if got := l.MaxY(); got != 9000 {
		t.Fatalf("Line.MaxY() = %d, want 9000", got)
	}
}

// TestCircleBounds verifies circle Bounds.
func TestCircleBounds(t *testing.T) {
	t.Parallel()

	c := Circle{
		X:      4000,
		Y:      7000,
		Radius: 1500,
	}

	if got := c.MinX(); got != 2500 {
		t.Fatalf("Circle.MinX() = %d, want 2500", got)
	}
	if got := c.MaxX(); got != 5500 {
		t.Fatalf("Circle.MaxX() = %d, want 5500", got)
	}
	if got := c.MinY(); got != 5500 {
		t.Fatalf("Circle.MinY() = %d, want 5500", got)
	}
	if got := c.MaxY(); got != 8500 {
		t.Fatalf("Circle.MaxY() = %d, want 8500", got)
	}
}

// TestRender_CircleRadiusScale_AffectsRenderedRadiusPx verifies render Circle Radius Scale Affects Rendered Radius Px.
func TestRender_CircleRadiusScale_AffectsRenderedRadiusPx(t *testing.T) {
	t.Parallel()

	w := NewWorld(10, 10)
	w.resetGrid(2 * SCALE)

	// Ensure index state is initialized so Add triggers rebuild if needed.
	w.IndexOnViewportChange(10, 10, 1.0)

	_, err := w.AddCircle(5, 5, 2) // raw radius = 2 units
	require.NoError(t, err)

	// scale = 2.0
	require.NoError(t, w.SetCircleRadiusScaleFp(2*SCALE))

	// Reindex explicitly (safe).
	w.Reindex()

	params := RenderParams{
		ViewportWidthPx:  10,
		ViewportHeightPx: 10,
		MarginXPx:        0,
		MarginYPx:        0,
		CameraXWorldFp:   5 * SCALE,
		CameraYWorldFp:   5 * SCALE,
		CameraZoom:       1.0,
		Options: &RenderOptions{
			BackgroundColor: color.RGBA{A: 255},
		},
	}

	d := &fakePrimitiveDrawer{}
	require.NoError(t, w.Render(d, params))

	circles := d.CommandsByName("AddCircle")
	require.NotEmpty(t, circles)

	// AddCircle args: cx, cy, rPx
	rPx := circles[0].Args[2]
	require.Equal(t, float64(4), rPx, "raw radius=2 with scale=2 => eff radius=4 => rPx=4 at zoom=1")
}
