package world

import (
	"github.com/stretchr/testify/require"
	"math"
	"testing"
)

// TestWrap verifies wrap.
func TestWrap(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name  string
		value int
		size  int
		want  int
	}{
		{name: "zero", value: 0, size: 10, want: 0},
		{name: "inside range", value: 7, size: 10, want: 7},
		{name: "equal to size", value: 10, size: 10, want: 0},
		{name: "greater than size", value: 27, size: 10, want: 7},
		{name: "negative one", value: -1, size: 10, want: 9},
		{name: "negative many", value: -23, size: 10, want: 7},
	}

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

			if got := wrap(tt.value, tt.size); got != tt.want {
				t.Fatalf("wrap(%d, %d) = %d, want %d", tt.value, tt.size, got, tt.want)
			}
		})
	}
}

// TestClamp verifies clamp.
func TestClamp(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name     string
		value    int
		minValue int
		maxValue int
		want     int
	}{
		{name: "below range", value: -5, minValue: 0, maxValue: 10, want: 0},
		{name: "inside range", value: 5, minValue: 0, maxValue: 10, want: 5},
		{name: "above range", value: 15, minValue: 0, maxValue: 10, want: 10},
		{name: "equal min", value: 0, minValue: 0, maxValue: 10, want: 0},
		{name: "equal max", value: 10, minValue: 0, maxValue: 10, want: 10},
	}

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

			if got := clamp(tt.value, tt.minValue, tt.maxValue); got != tt.want {
				t.Fatalf("clamp(%d, %d, %d) = %d, want %d", tt.value, tt.minValue, tt.maxValue, got, tt.want)
			}
		})
	}
}

// TestCeilDiv verifies ceil Div.
func TestCeilDiv(t *testing.T) {
	t.Parallel()

	tests := []struct {
		a    int
		b    int
		want int
	}{
		{a: 1, b: 1, want: 1},
		{a: 1, b: 2, want: 1},
		{a: 2, b: 2, want: 1},
		{a: 3, b: 2, want: 2},
		{a: 10, b: 3, want: 4},
		{a: 16, b: 8, want: 2},
		{a: 17, b: 8, want: 3},
	}

	for _, tt := range tests {
		if got := ceilDiv(tt.a, tt.b); got != tt.want {
			t.Fatalf("ceilDiv(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
		}
	}
}

// TestFloorDiv verifies floor Div.
func TestFloorDiv(t *testing.T) {
	t.Parallel()

	require.Equal(t, 0, floorDiv(0, 10))
	require.Equal(t, 0, floorDiv(1, 10))
	require.Equal(t, 0, floorDiv(9, 10))
	require.Equal(t, 1, floorDiv(10, 10))
	require.Equal(t, 1, floorDiv(19, 10))

	require.Equal(t, -1, floorDiv(-1, 10))
	require.Equal(t, -1, floorDiv(-9, 10))
	require.Equal(t, -1, floorDiv(-10, 10))
	require.Equal(t, -2, floorDiv(-11, 10))

	require.Panics(t, func() { _ = floorDiv(1, 0) })
	require.Panics(t, func() { _ = floorDiv(1, -1) })
}

// TestFixedPoint verifies fixed Point.
func TestFixedPoint(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name string
		v    float64
		want int
	}{
		{name: "zero", v: 0, want: 0},
		{name: "integer", v: 3, want: 3000},
		{name: "fraction", v: 1.234, want: 1234},
		{name: "round down", v: 1.2344, want: 1234},
		{name: "round up", v: 1.2345, want: 1235},
		{name: "negative", v: -1.2345, want: -1235},
	}

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

			if got := fixedPoint(tt.v); got != tt.want {
				t.Fatalf("fixedPoint(%f) = %d, want %d", tt.v, got, tt.want)
			}
		})
	}
}

// TestAbs verifies abs.
func TestAbs(t *testing.T) {
	t.Parallel()

	tests := []struct {
		v    int
		want int
	}{
		{v: 0, want: 0},
		{v: 7, want: 7},
		{v: -7, want: 7},
	}

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

// TestPixelSpanToWorldFixed verifies pixel Span To World Fixed.
func TestPixelSpanToWorldFixed(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name   string
		spanPx int
		zoomFp int
		want   int
	}{
		{name: "1x zoom", spanPx: 100, zoomFp: SCALE, want: 100 * SCALE},
		{name: "2x zoom", spanPx: 100, zoomFp: 2 * SCALE, want: 50 * SCALE},
		{name: "half zoom", spanPx: 100, zoomFp: SCALE / 2, want: 200 * SCALE},
		{name: "fractional result truncation", spanPx: 1, zoomFp: 3 * SCALE, want: 333},
	}

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

			if got := PixelSpanToWorldFixed(tt.spanPx, tt.zoomFp); got != tt.want {
				t.Fatalf("PixelSpanToWorldFixed(%d, %d) = %d, want %d", tt.spanPx, tt.zoomFp, got, tt.want)
			}
		})
	}
}

// TestWorldToCellPanicsOnInvalidGrid verifies world To Cell Panics On Invalid Grid.
func TestWorldToCellPanicsOnInvalidGrid(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name     string
		cells    int
		cellSize int
	}{
		{name: "zero cells", cells: 0, cellSize: 1},
		{name: "negative cells", cells: -1, cellSize: 1},
		{name: "zero cell size", cells: 1, cellSize: 0},
		{name: "negative cell size", cells: 1, cellSize: -1},
	}

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

			defer func() {
				if recover() == nil {
					t.Fatalf("worldToCell did not panic for cells=%d cellSize=%d", tt.cells, tt.cellSize)
				}
			}()

			_ = worldToCell(0, 1000, tt.cells, tt.cellSize)
		})
	}
}

// TestShortestWrappedDelta verifies shortest Wrapped Delta.
func TestShortestWrappedDelta(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name  string
		from  int
		to    int
		size  int
		wantA int
		wantB int
	}{
		{name: "no wrap forward", from: 1000, to: 3000, size: 10000, wantA: 1000, wantB: 3000},
		{name: "no wrap backward", from: 3000, to: 1000, size: 10000, wantA: 3000, wantB: 1000},
		{name: "wrap forward over half", from: 1000, to: 7000, size: 10000, wantA: 11000, wantB: 7000},
		{name: "wrap backward over half", from: 7000, to: 1000, size: 10000, wantA: 7000, wantB: 11000},
		{name: "tie positive half wraps", from: 1000, to: 6000, size: 10000, wantA: 11000, wantB: 6000},
		{name: "tie negative half stays", from: 6000, to: 1000, size: 10000, wantA: 6000, wantB: 1000},
		{name: "just below positive half does not wrap", from: 1000, to: 5999, size: 10000, wantA: 1000, wantB: 5999},
		{name: "just beyond negative half wraps", from: 6001, to: 1000, size: 10000, wantA: 6001, wantB: 11000},
	}

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

			gotA, gotB := shortestWrappedDelta(tt.from, tt.to, tt.size)
			if gotA != tt.wantA || gotB != tt.wantB {
				t.Fatalf("shortestWrappedDelta(%d, %d, %d) = (%d, %d), want (%d, %d)",
					tt.from, tt.to, tt.size, gotA, gotB, tt.wantA, tt.wantB)
			}

			delta := gotB - gotA
			half := tt.size / 2
			if delta < -half || delta >= half {
				t.Fatalf("normalized delta %d is outside [-%d, %d)", delta, half, half)
			}
		})
	}
}

// TestCameraZoomToWorldFixed verifies camera Zoom To World Fixed.
func TestCameraZoomToWorldFixed(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name       string
		cameraZoom float64
		want       int
	}{
		{
			name:       "neutral zoom",
			cameraZoom: 1.0,
			want:       SCALE,
		},
		{
			name:       "integer zoom",
			cameraZoom: 2.0,
			want:       2 * SCALE,
		},
		{
			name:       "fractional zoom",
			cameraZoom: 1.25,
			want:       1250,
		},
		{
			name:       "minimum configured zoom value shape",
			cameraZoom: 0.25,
			want:       SCALE / 4,
		},
		{
			name:       "round down",
			cameraZoom: 1.2344,
			want:       1234,
		},
		{
			name:       "round up",
			cameraZoom: 1.2345,
			want:       1235,
		},
		{
			name:       "very small but still positive after rounding",
			cameraZoom: 0.0006,
			want:       1,
		},
	}

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

			got, err := CameraZoomToWorldFixed(tt.cameraZoom)
			require.NoError(t, err)
			require.Equal(t, tt.want, got)
		})
	}
}

// TestCameraZoomToWorldFixedReturnsError verifies camera Zoom To World Fixed Returns Error.
func TestCameraZoomToWorldFixedReturnsError(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name       string
		cameraZoom float64
	}{
		{
			name:       "zero",
			cameraZoom: 0,
		},
		{
			name:       "negative",
			cameraZoom: -1,
		},
		{
			name:       "nan",
			cameraZoom: math.NaN(),
		},
		{
			name:       "positive infinity",
			cameraZoom: math.Inf(1),
		},
		{
			name:       "negative infinity",
			cameraZoom: math.Inf(-1),
		},
		{
			name:       "positive but rounds to zero",
			cameraZoom: 0.0004,
		},
	}

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

			got, err := CameraZoomToWorldFixed(tt.cameraZoom)
			require.ErrorIs(t, err, errInvalidCameraZoom)
			require.Zero(t, got)
		})
	}
}

// TestMustCameraZoomToWorldFixed verifies must Camera Zoom To World Fixed.
func TestMustCameraZoomToWorldFixed(t *testing.T) {
	t.Parallel()

	require.Equal(t, 1250, mustCameraZoomToWorldFixed(1.25))

	require.Panics(t, func() {
		_ = mustCameraZoomToWorldFixed(0)
	})
}

// TestWorldFixedToCameraZoom verifies world Fixed To Camera Zoom.
func TestWorldFixedToCameraZoom(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name   string
		zoomFp int
		want   float64
	}{
		{name: "zero", zoomFp: 0, want: 0},
		{name: "neutral", zoomFp: SCALE, want: 1.0},
		{name: "fractional", zoomFp: 1250, want: 1.25},
		{name: "integer multiple", zoomFp: 3 * SCALE, want: 3.0},
	}

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

			got := worldFixedToCameraZoom(tt.zoomFp)
			require.Equal(t, tt.want, got)
		})
	}
}

// TestRequiredZoomToFitWorld verifies required Zoom To Fit World.
func TestRequiredZoomToFitWorld(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name           string
		viewportSpanPx int
		worldSpanFp    int
		want           int
	}{
		{
			name:           "zero viewport span",
			viewportSpanPx: 0,
			worldSpanFp:    10 * SCALE,
			want:           0,
		},
		{
			name:           "exact neutral fit",
			viewportSpanPx: 10,
			worldSpanFp:    10 * SCALE,
			want:           SCALE,
		},
		{
			name:           "exact 2x fit",
			viewportSpanPx: 20,
			worldSpanFp:    10 * SCALE,
			want:           2 * SCALE,
		},
		{
			name:           "fractional fit rounded up",
			viewportSpanPx: 11,
			worldSpanFp:    10 * SCALE,
			want:           1100,
		},
		{
			name:           "small world requires larger zoom",
			viewportSpanPx: 320,
			worldSpanFp:    80 * SCALE,
			want:           4 * SCALE,
		},
	}

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

			got := requiredZoomToFitWorld(tt.viewportSpanPx, tt.worldSpanFp)
			require.Equal(t, tt.want, got)
		})
	}
}

// TestRequiredZoomToFitWorldPanics verifies required Zoom To Fit World Panics.
func TestRequiredZoomToFitWorldPanics(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name           string
		viewportSpanPx int
		worldSpanFp    int
	}{
		{
			name:           "negative viewport span",
			viewportSpanPx: -1,
			worldSpanFp:    10 * SCALE,
		},
		{
			name:           "zero world span",
			viewportSpanPx: 10,
			worldSpanFp:    0,
		},
		{
			name:           "negative world span",
			viewportSpanPx: 10,
			worldSpanFp:    -1,
		},
	}

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

			require.Panics(t, func() {
				_ = requiredZoomToFitWorld(tt.viewportSpanPx, tt.worldSpanFp)
			})
		})
	}
}

// TestCorrectCameraZoomFpReturnsCurrentWhenNoCorrectionNeeded verifies correct Camera Zoom Fp Returns Current When No Correction Needed.
func TestCorrectCameraZoomFpReturnsCurrentWhenNoCorrectionNeeded(t *testing.T) {
	t.Parallel()

	got := correctCameraZoomFp(
		2*SCALE,
		40, 30,
		100*SCALE, 100*SCALE,
		MIN_ZOOM, MAX_ZOOM,
	)

	require.Equal(t, 2*SCALE, got)
}

// TestCorrectCameraZoomFpRaisesZoomToFitWorldWidth verifies correct Camera Zoom Fp Raises Zoom To Fit World Width.
func TestCorrectCameraZoomFpRaisesZoomToFitWorldWidth(t *testing.T) {
	t.Parallel()

	got := correctCameraZoomFp(
		SCALE,
		120, 20,
		100*SCALE, 100*SCALE,
		0, 0,
	)

	require.Equal(t, 1200, got)
}

// TestCorrectCameraZoomFpRaisesZoomToFitWorldHeight verifies correct Camera Zoom Fp Raises Zoom To Fit World Height.
func TestCorrectCameraZoomFpRaisesZoomToFitWorldHeight(t *testing.T) {
	t.Parallel()

	got := correctCameraZoomFp(
		SCALE,
		20, 150,
		100*SCALE, 100*SCALE,
		0, 0,
	)

	require.Equal(t, 1500, got)
}

// TestCorrectCameraZoomFpUsesMaxFitAcrossAxes verifies correct Camera Zoom Fp Uses Max Fit Across Axes.
func TestCorrectCameraZoomFpUsesMaxFitAcrossAxes(t *testing.T) {
	t.Parallel()

	got := correctCameraZoomFp(
		SCALE,
		120, 150,
		100*SCALE, 100*SCALE,
		0, 0,
	)

	require.Equal(t, 1500, got)
}

// TestCorrectCameraZoomFpAppliesMinZoomWhenLargerThanCurrentAndFit verifies correct Camera Zoom Fp Applies Min Zoom When Larger Than Current And Fit.
func TestCorrectCameraZoomFpAppliesMinZoomWhenLargerThanCurrentAndFit(t *testing.T) {
	t.Parallel()

	got := correctCameraZoomFp(
		SCALE,
		20, 20,
		100*SCALE, 100*SCALE,
		1500, 0,
	)

	require.Equal(t, 1500, got)
}

// TestCorrectCameraZoomFpAppliesMaxZoomWhenNoFitConflict verifies correct Camera Zoom Fp Applies Max Zoom When No Fit Conflict.
func TestCorrectCameraZoomFpAppliesMaxZoomWhenNoFitConflict(t *testing.T) {
	t.Parallel()

	got := correctCameraZoomFp(
		4*SCALE,
		20, 20,
		100*SCALE, 100*SCALE,
		0, 3*SCALE,
	)

	require.Equal(t, 3*SCALE, got)
}

// TestCorrectCameraZoomFpIgnoresMaxZoomWhenFitNeedsMore verifies correct Camera Zoom Fp Ignores Max Zoom When Fit Needs More.
func TestCorrectCameraZoomFpIgnoresMaxZoomWhenFitNeedsMore(t *testing.T) {
	t.Parallel()

	got := correctCameraZoomFp(
		SCALE,
		200, 20,
		100*SCALE, 100*SCALE,
		0, 1500,
	)

	require.Equal(t, 2*SCALE, got)
}

// TestCorrectCameraZoomFpAppliesMinThenMaxWhenBothValid verifies correct Camera Zoom Fp Applies Min Then Max When Both Valid.
func TestCorrectCameraZoomFpAppliesMinThenMaxWhenBothValid(t *testing.T) {
	t.Parallel()

	got := correctCameraZoomFp(
		SCALE,
		20, 20,
		100*SCALE, 100*SCALE,
		1500, 1600,
	)

	require.Equal(t, 1500, got)
}

// TestCorrectCameraZoomFpCurrentAboveMaxGetsClamped verifies correct Camera Zoom Fp Current Above Max Gets Clamped.
func TestCorrectCameraZoomFpCurrentAboveMaxGetsClamped(t *testing.T) {
	t.Parallel()

	got := correctCameraZoomFp(
		5*SCALE,
		20, 20,
		100*SCALE, 100*SCALE,
		0, 3*SCALE,
	)

	require.Equal(t, 3*SCALE, got)
}

// TestCorrectCameraZoomFpZeroViewportUsesOnlyBounds verifies correct Camera Zoom Fp Zero Viewport Uses Only Bounds.
func TestCorrectCameraZoomFpZeroViewportUsesOnlyBounds(t *testing.T) {
	t.Parallel()

	got := correctCameraZoomFp(
		SCALE,
		0, 0,
		100*SCALE, 100*SCALE,
		1500, 0,
	)

	require.Equal(t, 1500, got)
}

// TestCorrectCameraZoomFpZeroBoundsAreIgnored verifies correct Camera Zoom Fp Zero Bounds Are Ignored.
func TestCorrectCameraZoomFpZeroBoundsAreIgnored(t *testing.T) {
	t.Parallel()

	got := correctCameraZoomFp(
		1250,
		20, 20,
		100*SCALE, 100*SCALE,
		0, 0,
	)

	require.Equal(t, 1250, got)
}

// TestCorrectCameraZoomFpPanics verifies correct Camera Zoom Fp Panics.
func TestCorrectCameraZoomFpPanics(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name string
		fn   func()
	}{
		{
			name: "non-positive current zoom",
			fn: func() {
				_ = correctCameraZoomFp(0, 10, 10, 100*SCALE, 100*SCALE, 0, 0)
			},
		},
		{
			name: "negative viewport width",
			fn: func() {
				_ = correctCameraZoomFp(SCALE, -1, 10, 100*SCALE, 100*SCALE, 0, 0)
			},
		},
		{
			name: "negative viewport height",
			fn: func() {
				_ = correctCameraZoomFp(SCALE, 10, -1, 100*SCALE, 100*SCALE, 0, 0)
			},
		},
		{
			name: "non-positive world width",
			fn: func() {
				_ = correctCameraZoomFp(SCALE, 10, 10, 0, 100*SCALE, 0, 0)
			},
		},
		{
			name: "non-positive world height",
			fn: func() {
				_ = correctCameraZoomFp(SCALE, 10, 10, 100*SCALE, 0, 0, 0)
			},
		},
		{
			name: "negative min zoom",
			fn: func() {
				_ = correctCameraZoomFp(SCALE, 10, 10, 100*SCALE, 100*SCALE, -1, 0)
			},
		},
		{
			name: "negative max zoom",
			fn: func() {
				_ = correctCameraZoomFp(SCALE, 10, 10, 100*SCALE, 100*SCALE, 0, -1)
			},
		},
		{
			name: "min greater than max",
			fn: func() {
				_ = correctCameraZoomFp(SCALE, 10, 10, 100*SCALE, 100*SCALE, 2000, 1500)
			},
		},
	}

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

			require.Panics(t, tt.fn)
		})
	}
}

// TestWorldCorrectCameraZoomReturnsFloatValue verifies world Correct Camera Zoom Returns Float Value.
func TestWorldCorrectCameraZoomReturnsFloatValue(t *testing.T) {
	t.Parallel()

	w := NewWorld(100, 100)

	got := w.CorrectCameraZoom(1.0, 120, 20)

	require.Equal(t, 1.2, got)
}

// TestWorldCorrectCameraZoomAppliesDefaultBounds verifies world Correct Camera Zoom Applies Default Bounds.
func TestWorldCorrectCameraZoomAppliesDefaultBounds(t *testing.T) {
	t.Parallel()

	w := NewWorld(100, 100)

	got := w.CorrectCameraZoom(100.0, 20, 20)

	require.Equal(t, worldFixedToCameraZoom(MAX_ZOOM), got)
}

// TestWorldCorrectCameraZoomFitBeatsDefaultMaxBound verifies world Correct Camera Zoom Fit Beats Default Max Bound.
func TestWorldCorrectCameraZoomFitBeatsDefaultMaxBound(t *testing.T) {
	t.Parallel()

	w := NewWorld(1, 100)

	got := w.CorrectCameraZoom(1.0, 40, 10)

	require.Equal(t, 40.0, got)
}

// TestCorrectCameraZoomFp_DoesNotLowerZoomWhenViewportIsSmallerThanWorld verifies correct Camera Zoom Fp Does Not Lower Zoom When Viewport Is Smaller Than World.
func TestCorrectCameraZoomFp_DoesNotLowerZoomWhenViewportIsSmallerThanWorld(t *testing.T) {
	t.Parallel()

	got := correctCameraZoomFp(
		SCALE,  // currentZoomFp = 1.0x
		80, 80, // viewport px
		100*SCALE, 100*SCALE, // world fp
		0, 0,
	)

	// No anti-wrap needed, and we do not auto-fit by lowering zoom.
	require.Equal(t, SCALE, got)
}

// TestCorrectCameraZoomFp_RaisesZoomToPreventWrapWhenViewportIsLarger verifies correct Camera Zoom Fp Raises Zoom To Prevent Wrap When Viewport Is Larger.
func TestCorrectCameraZoomFp_RaisesZoomToPreventWrapWhenViewportIsLarger(t *testing.T) {
	t.Parallel()

	// World width = 100 units, viewport width = 120 px, at zoom=1 visible span = 120 units => too large.
	got := correctCameraZoomFp(
		SCALE,
		120, 20,
		100*SCALE, 100*SCALE,
		0, 0,
	)

	require.Equal(t, 1200, got) // 1.2x
}

// TestCorrectCameraZoomFp_AppliesMaxZoomWhenNoWrapConflict verifies correct Camera Zoom Fp Applies Max Zoom When No Wrap Conflict.
func TestCorrectCameraZoomFp_AppliesMaxZoomWhenNoWrapConflict(t *testing.T) {
	t.Parallel()

	got := correctCameraZoomFp(
		4*SCALE, // user wants 4x
		20, 20,
		100*SCALE, 100*SCALE,
		0, 3*SCALE, // max 3x
	)

	require.Equal(t, 3*SCALE, got)
}

// TestCorrectCameraZoomFp_AntiWrapBeatsMaxZoom verifies correct Camera Zoom Fp Anti Wrap Beats Max Zoom.
func TestCorrectCameraZoomFp_AntiWrapBeatsMaxZoom(t *testing.T) {
	t.Parallel()

	// requiredFit = 2x, but max is 1.5x => must return 2x.
	got := correctCameraZoomFp(
		SCALE,
		200, 20,
		100*SCALE, 100*SCALE,
		0, 1500,
	)

	require.Equal(t, 2*SCALE, got)
}

// TestCorrectCameraZoomFp_AppliesMinZoom verifies correct Camera Zoom Fp Applies Min Zoom.
func TestCorrectCameraZoomFp_AppliesMinZoom(t *testing.T) {
	t.Parallel()

	got := correctCameraZoomFp(
		800, // 0.8x
		20, 20,
		100*SCALE, 100*SCALE,
		SCALE, 0, // min 1.0x
	)

	require.Equal(t, SCALE, got)
}

// TestCorrectCameraZoomFp_ZeroViewportUsesOnlyBounds verifies correct Camera Zoom Fp Zero Viewport Uses Only Bounds.
func TestCorrectCameraZoomFp_ZeroViewportUsesOnlyBounds(t *testing.T) {
	t.Parallel()

	got := correctCameraZoomFp(
		SCALE,
		0, 0,
		100*SCALE, 100*SCALE,
		1500, 0,
	)

	require.Equal(t, 1500, got)
}

// TestClampCameraNoWrapViewport_ClampsToKeepViewportInsideWorld verifies clamp Camera No Wrap Viewport Clamps To Keep Viewport Inside World.
func TestClampCameraNoWrapViewport_ClampsToKeepViewportInsideWorld(t *testing.T) {
	t.Parallel()

	worldW := 100 * SCALE
	worldH := 100 * SCALE
	zoomFp := SCALE // 1.0x

	// viewport 40px => span 40 units => half 20.
	viewportW, viewportH := 40, 40

	// Too far left/up => clamp to minCam=20
	cx, cy := ClampCameraNoWrapViewport(
		0, 0,
		viewportW, viewportH,
		zoomFp,
		worldW, worldH,
	)
	require.Equal(t, 20*SCALE, cx)
	require.Equal(t, 20*SCALE, cy)

	// Too far right/down => clamp to maxCam=world-half=80
	cx, cy = ClampCameraNoWrapViewport(
		99*SCALE, 99*SCALE,
		viewportW, viewportH,
		zoomFp,
		worldW, worldH,
	)
	require.Equal(t, 80*SCALE, cx)
	require.Equal(t, 80*SCALE, cy)

	// Inside range => unchanged
	cx, cy = ClampCameraNoWrapViewport(
		50*SCALE, 60*SCALE,
		viewportW, viewportH,
		zoomFp,
		worldW, worldH,
	)
	require.Equal(t, 50*SCALE, cx)
	require.Equal(t, 60*SCALE, cy)
}

// TestClampCameraNoWrapViewport_WhenViewportLargerThanWorld_ForcesCenter verifies clamp Camera No Wrap Viewport When Viewport Larger Than World Forces Center.
func TestClampCameraNoWrapViewport_WhenViewportLargerThanWorld_ForcesCenter(t *testing.T) {
	t.Parallel()

	worldW := 50 * SCALE
	worldH := 50 * SCALE
	zoomFp := SCALE

	// viewport 60px => span 60 units > world 50
	viewportW, viewportH := 60, 60

	cx, cy := ClampCameraNoWrapViewport(
		0, 0,
		viewportW, viewportH,
		zoomFp,
		worldW, worldH,
	)

	require.Equal(t, worldW/2, cx)
	require.Equal(t, worldH/2, cy)
}

// TestWorldClampRenderParamsNoWrap_UsesViewportClamp verifies world Clamp Render Params No Wrap Uses Viewport Clamp.
func TestWorldClampRenderParamsNoWrap_UsesViewportClamp(t *testing.T) {
	t.Parallel()

	w := NewWorld(100, 100)

	p := RenderParams{
		ViewportWidthPx:  40,
		ViewportHeightPx: 40,
		MarginXPx:        10,
		MarginYPx:        10,
		CameraZoom:       1.0,
		CameraXWorldFp:   0,
		CameraYWorldFp:   0,
		Options:          &RenderOptions{DisableWrapScroll: true},
	}

	w.ClampRenderParamsNoWrap(&p)

	// viewport half is 20, not 30 (margins ignored)
	require.Equal(t, 20*SCALE, p.CameraXWorldFp)
	require.Equal(t, 20*SCALE, p.CameraYWorldFp)
}

// TestPivotZoom_CursorAtCenter_KeepsCamera verifies pivot Zoom Cursor At Center Keeps Camera.
func TestPivotZoom_CursorAtCenter_KeepsCamera(t *testing.T) {
	t.Parallel()

	cx, cy := PivotZoomCameraNoWrap(
		50*SCALE, 60*SCALE,
		100, 80,
		50, 40, // cursor at center
		SCALE, 2*SCALE,
	)
	require.Equal(t, 50*SCALE, cx)
	require.Equal(t, 60*SCALE, cy)
}

// TestPivotZoom_RightEdge_ZoomInMovesCameraRight verifies pivot Zoom Right Edge Zoom In Moves Camera Right.
func TestPivotZoom_RightEdge_ZoomInMovesCameraRight(t *testing.T) {
	t.Parallel()

	// viewport 100px, cursor at x=100 (right edge), center is 50 => offX=50px
	// zoom 1->2 halves worldPerPx, so camera must move towards the cursor to keep the same world point.
	cx0 := 50 * SCALE

	cx, _ := PivotZoomCameraNoWrap(
		cx0, 50*SCALE,
		100, 100,
		100, 50,
		SCALE, 2*SCALE,
	)
	require.Greater(t, cx, cx0)
}

// TestPivotZoom_LeftEdge_ZoomInMovesCameraLeft verifies pivot Zoom Left Edge Zoom In Moves Camera Left.
func TestPivotZoom_LeftEdge_ZoomInMovesCameraLeft(t *testing.T) {
	t.Parallel()

	cx0 := 50 * SCALE

	cx, _ := PivotZoomCameraNoWrap(
		cx0, 50*SCALE,
		100, 100,
		0, 50,
		SCALE, 2*SCALE,
	)
	require.Less(t, cx, cx0)
}
