"""Tests for geometry/bbox.py — pure bbox operations.""" from __future__ import annotations import pytest from src.app.geometry import bbox class TestBasicProperties: def test_x2(self) -> None: assert bbox.x2((10, 20, 100, 50)) == 110 def test_y2(self) -> None: assert bbox.y2((10, 20, 100, 50)) == 70 def test_area(self) -> None: assert bbox.area((0, 0, 10, 20)) == 200 def test_center(self) -> None: cx, cy = bbox.center((100, 200, 300, 50)) assert cx == 250.0 assert cy == 225.0 class TestContains: def test_fully_inside(self) -> None: outer = (0, 0, 100, 100) inner = (10, 10, 20, 20) assert bbox.contains(outer, inner) is True def test_same_box(self) -> None: b = (10, 20, 30, 40) assert bbox.contains(b, b) is True def test_exceeds_right(self) -> None: outer = (0, 0, 100, 100) inner = (80, 10, 30, 20) # x2 = 110 > 100 assert bbox.contains(outer, inner) is False def test_exceeds_bottom(self) -> None: outer = (0, 0, 100, 100) inner = (10, 85, 20, 20) # y2 = 105 > 100 assert bbox.contains(outer, inner) is False def test_tolerance_allows_small_overflow(self) -> None: outer = (0, 0, 100, 100) inner = (0, 0, 103, 100) # 3px overflow on right assert bbox.contains(outer, inner, tolerance=5) is True assert bbox.contains(outer, inner, tolerance=2) is False def test_tolerance_on_all_sides(self) -> None: outer = (10, 10, 80, 80) inner = (7, 7, 86, 86) # 3px overflow on each side assert bbox.contains(outer, inner, tolerance=3) is True assert bbox.contains(outer, inner, tolerance=2) is False class TestIntersects: def test_overlapping(self) -> None: a = (0, 0, 50, 50) b = (25, 25, 50, 50) assert bbox.intersects(a, b) is True def test_not_overlapping(self) -> None: a = (0, 0, 10, 10) b = (20, 20, 10, 10) assert bbox.intersects(a, b) is False def test_touching_edge_not_overlapping(self) -> None: a = (0, 0, 10, 10) b = (10, 0, 10, 10) # touching at x=10 assert bbox.intersects(a, b) is False def test_contained(self) -> None: outer = (0, 0, 100, 100) inner = (10, 10, 20, 20) assert bbox.intersects(outer, inner) is True class TestIntersection: def test_overlap(self) -> None: a = (0, 0, 50, 50) b = (25, 25, 50, 50) result = bbox.intersection(a, b) assert result == (25, 25, 25, 25) def test_no_overlap(self) -> None: a = (0, 0, 10, 10) b = (20, 20, 10, 10) assert bbox.intersection(a, b) is None def test_contained(self) -> None: outer = (0, 0, 100, 100) inner = (10, 10, 20, 20) result = bbox.intersection(outer, inner) assert result == inner class TestUnion: def test_adjacent(self) -> None: a = (0, 0, 10, 10) b = (10, 0, 10, 10) result = bbox.union(a, b) assert result == (0, 0, 20, 10) def test_overlapping(self) -> None: a = (0, 0, 50, 50) b = (25, 25, 50, 50) result = bbox.union(a, b) assert result == (0, 0, 75, 75) def test_contained(self) -> None: outer = (0, 0, 100, 100) inner = (10, 10, 20, 20) assert bbox.union(outer, inner) == outer class TestUnionAll: def test_multiple(self) -> None: bboxes = [(0, 0, 10, 10), (50, 50, 10, 10), (25, 25, 10, 10)] result = bbox.union_all(bboxes) assert result == (0, 0, 60, 60) def test_single(self) -> None: assert bbox.union_all([(5, 5, 10, 10)]) == (5, 5, 10, 10) def test_empty_raises(self) -> None: with pytest.raises(ValueError, match="empty"): bbox.union_all([]) class TestExpand: def test_normal(self) -> None: result = bbox.expand((10, 20, 100, 50), 5) assert result == (5, 15, 110, 60) def test_clamp_to_zero(self) -> None: result = bbox.expand((2, 3, 10, 10), 10) assert result[0] == 0 assert result[1] == 0 def test_min_dimension(self) -> None: result = bbox.expand((10, 10, 10, 10), -100) assert result[2] >= 1 assert result[3] >= 1 class TestIou: def test_perfect_overlap(self) -> None: b = (0, 0, 10, 10) assert bbox.iou(b, b) == pytest.approx(1.0) def test_no_overlap(self) -> None: a = (0, 0, 10, 10) b = (20, 20, 10, 10) assert bbox.iou(a, b) == 0.0 def test_partial_overlap(self) -> None: a = (0, 0, 10, 10) b = (5, 5, 10, 10) # intersection = (5,5,5,5) area=25, union = 100+100-25=175 assert bbox.iou(a, b) == pytest.approx(25 / 175) class TestOverlapRatio: def test_fully_inside(self) -> None: inner = (10, 10, 20, 20) outer = (0, 0, 100, 100) assert bbox.overlap_ratio(inner, outer) == pytest.approx(1.0) def test_no_overlap(self) -> None: a = (0, 0, 10, 10) b = (20, 20, 10, 10) assert bbox.overlap_ratio(a, b) == 0.0 def test_half_overlap(self) -> None: a = (0, 0, 10, 10) b = (5, 0, 10, 10) # a ∩ b = (5,0,5,10) area=50, a area=100 assert bbox.overlap_ratio(a, b) == pytest.approx(0.5)