"""
Unit test suite for Viewport Frustum Culling Utilities.
Verifies AABB-Frustum intersection math, isometric projection, and entity culling.
"""

import pytest

from tools.asset_pipeline.viewport_culling_utils import (
    AABB,
    EntityBounds,
    Frustum2D,
    benchmark_culling,
    cull_entities,
    intersects,
    project_iso,
)


class TestAABBOperations:
    def test_aabb_properties(self) -> None:
        box = AABB(min_x=10.0, min_y=20.0, max_x=50.0, max_y=80.0)
        assert box.width == 40.0
        assert box.height == 60.0

    def test_aabb_contains_point(self) -> None:
        box = AABB(min_x=0.0, min_y=0.0, max_x=100.0, max_y=100.0)
        assert box.contains_point(50.0, 50.0) is True
        assert box.contains_point(0.0, 0.0) is True
        assert box.contains_point(100.0, 100.0) is True
        assert box.contains_point(-1.0, 50.0) is False
        assert box.contains_point(50.0, 101.0) is False

    def test_aabb_intersects(self) -> None:
        b1 = AABB(min_x=0.0, min_y=0.0, max_x=50.0, max_y=50.0)
        b2 = AABB(min_x=25.0, min_y=25.0, max_x=75.0, max_y=75.0)
        b3 = AABB(min_x=60.0, min_y=60.0, max_x=100.0, max_y=100.0)

        assert b1.intersects(b2) is True
        assert b2.intersects(b1) is True
        assert b1.intersects(b3) is False
        assert b3.intersects(b1) is False

    def test_aabb_expand(self) -> None:
        box = AABB(min_x=10.0, min_y=10.0, max_x=20.0, max_y=20.0)
        padded = box.expand(5.0)
        assert padded.min_x == 5.0
        assert padded.min_y == 5.0
        assert padded.max_x == 25.0
        assert padded.max_y == 25.0


class TestFrustumIntersection:
    def test_frustum_to_aabb(self) -> None:
        frustum = Frustum2D(min_x=0.0, min_y=0.0, max_x=100.0, max_y=100.0, padding=10.0)
        box = frustum.to_aabb()
        assert box.min_x == -10.0
        assert box.min_y == -10.0
        assert box.max_x == 110.0
        assert box.max_y == 110.0

    def test_frustum_intersects_box(self) -> None:
        frustum = Frustum2D(min_x=0.0, min_y=0.0, max_x=100.0, max_y=100.0, padding=0.0)
        # Inside
        assert intersects(frustum, AABB(20.0, 20.0, 40.0, 40.0)) is True
        # Partial overlap on edge
        assert intersects(frustum, AABB(90.0, 90.0, 120.0, 120.0)) is True
        # Completely outside right
        assert intersects(frustum, AABB(101.0, 50.0, 150.0, 80.0)) is False
        # Completely outside top
        assert intersects(frustum, AABB(50.0, 101.0, 80.0, 150.0)) is False

    def test_frustum_padding_inclusion(self) -> None:
        # Box is just outside unpadded frustum, but inside when padding=20.0
        frustum_no_pad = Frustum2D(0.0, 0.0, 100.0, 100.0, padding=0.0)
        frustum_padded = Frustum2D(0.0, 0.0, 100.0, 100.0, padding=20.0)
        box = AABB(105.0, 50.0, 115.0, 70.0)

        assert intersects(frustum_no_pad, box) is False
        assert intersects(frustum_padded, box) is True


class TestIsometricProjection:
    def test_project_iso_origin(self) -> None:
        iso_x, iso_y = project_iso(0.0, 0.0, 0.0)
        assert iso_x == 0.0
        assert iso_y == 0.0

    def test_project_iso_elevation_offset(self) -> None:
        _, y_ground = project_iso(5.0, 5.0, 0.0, z_scale=24.0)
        _, y_elevated = project_iso(5.0, 5.0, 2.0, z_scale=24.0)
        # Higher elevation shifts screen y upwards (negative delta)
        assert y_elevated == y_ground - 48.0


class TestCullEntities:
    def test_cull_entities_filter(self) -> None:
        entities = [
            EntityBounds("center_mob", world_x=0.0, world_y=0.0),
            EntityBounds("nearby_mob", world_x=2.0, world_y=-1.0),
            EntityBounds("far_mob_culled", world_x=50.0, world_y=50.0),
            EntityBounds("negative_far_culled", world_x=-50.0, world_y=-50.0),
        ]

        visible = cull_entities(
            camera_pos=(0.0, 0.0),
            viewport_size=(800.0, 600.0),
            entities=entities,
            padding=32.0,
        )

        vis_ids = {e.entity_id for e in visible}
        assert "center_mob" in vis_ids
        assert "nearby_mob" in vis_ids
        assert "far_mob_culled" not in vis_ids
        assert "negative_far_culled" not in vis_ids

    def test_cull_entities_empty_list(self) -> None:
        visible = cull_entities((0.0, 0.0), (800.0, 600.0), [])
        assert visible == []

    def test_benchmark_culling_synthetic(self) -> None:
        res = benchmark_culling(num_entities=200, viewport_size=(1280.0, 720.0))
        assert res["total_entities"] == 200
        assert res["visible_entities"] > 0
        assert res["culled_entities"] > 0
        assert res["elapsed_ms"] < 50.0
