"""
Tests for expand_bbox function.

Tests verify that bbox expansion works correctly with center-point scaling,
directional compensation, max padding clamping, and image boundary handling.
"""

import pytest

from shared.bbox import (
    expand_bbox,
    ScaleStrategy,
    FIELD_SCALE_STRATEGIES,
    DEFAULT_STRATEGY,
)


class TestExpandBboxCenterScaling:
    """Tests for center-point based scaling."""

    def test_center_scaling_expands_symmetrically(self):
        """Verify bbox expands symmetrically around center when no extra ratios."""
        # 100x50 bbox at (100, 200)
        bbox = (100, 200, 200, 250)
        strategy = ScaleStrategy(
            scale_x=1.2,  # 20% wider
            scale_y=1.4,  # 40% taller
            max_pad_x=1000,  # Large to avoid clamping
            max_pad_y=1000,
        )

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="test_field",
            strategies={"test_field": strategy},
        )

        # Original: width=100, height=50
        # New: width=120, height=70
        # Center: (150, 225)
        # Expected: x0=150-60=90, x1=150+60=210, y0=225-35=190, y1=225+35=260
        assert result[0] == 90  # x0
        assert result[1] == 190  # y0
        assert result[2] == 210  # x1
        assert result[3] == 260  # y1

    def test_no_scaling_returns_original(self):
        """Verify scale=1.0 with no extras returns original bbox."""
        bbox = (100, 200, 200, 250)
        strategy = ScaleStrategy(
            scale_x=1.0,
            scale_y=1.0,
            max_pad_x=1000,
            max_pad_y=1000,
        )

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="test_field",
            strategies={"test_field": strategy},
        )

        assert result == (100, 200, 200, 250)


class TestExpandBboxDirectionalCompensation:
    """Tests for directional compensation (extra ratios)."""

    def test_extra_top_expands_upward(self):
        """Verify extra_top_ratio adds expansion toward top."""
        bbox = (100, 200, 200, 250)  # width=100, height=50
        strategy = ScaleStrategy(
            scale_x=1.0,
            scale_y=1.0,
            extra_top_ratio=0.5,  # Add 50% of height to top
            max_pad_x=1000,
            max_pad_y=1000,
        )

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="test_field",
            strategies={"test_field": strategy},
        )

        # extra_top = 50 * 0.5 = 25
        assert result[0] == 100  # x0 unchanged
        assert result[1] == 175  # y0 = 200 - 25
        assert result[2] == 200  # x1 unchanged
        assert result[3] == 250  # y1 unchanged

    def test_extra_left_expands_leftward(self):
        """Verify extra_left_ratio adds expansion toward left."""
        bbox = (100, 200, 200, 250)  # width=100
        strategy = ScaleStrategy(
            scale_x=1.0,
            scale_y=1.0,
            extra_left_ratio=0.8,  # Add 80% of width to left
            max_pad_x=1000,
            max_pad_y=1000,
        )

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="test_field",
            strategies={"test_field": strategy},
        )

        # extra_left = 100 * 0.8 = 80
        assert result[0] == 20  # x0 = 100 - 80
        assert result[1] == 200  # y0 unchanged
        assert result[2] == 200  # x1 unchanged
        assert result[3] == 250  # y1 unchanged

    def test_extra_right_expands_rightward(self):
        """Verify extra_right_ratio adds expansion toward right."""
        bbox = (100, 200, 200, 250)  # width=100
        strategy = ScaleStrategy(
            scale_x=1.0,
            scale_y=1.0,
            extra_right_ratio=0.3,  # Add 30% of width to right
            max_pad_x=1000,
            max_pad_y=1000,
        )

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="test_field",
            strategies={"test_field": strategy},
        )

        # extra_right = 100 * 0.3 = 30
        assert result[0] == 100  # x0 unchanged
        assert result[1] == 200  # y0 unchanged
        assert result[2] == 230  # x1 = 200 + 30
        assert result[3] == 250  # y1 unchanged

    def test_extra_bottom_expands_downward(self):
        """Verify extra_bottom_ratio adds expansion toward bottom."""
        bbox = (100, 200, 200, 250)  # height=50
        strategy = ScaleStrategy(
            scale_x=1.0,
            scale_y=1.0,
            extra_bottom_ratio=0.4,  # Add 40% of height to bottom
            max_pad_x=1000,
            max_pad_y=1000,
        )

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="test_field",
            strategies={"test_field": strategy},
        )

        # extra_bottom = 50 * 0.4 = 20
        assert result[0] == 100  # x0 unchanged
        assert result[1] == 200  # y0 unchanged
        assert result[2] == 200  # x1 unchanged
        assert result[3] == 270  # y1 = 250 + 20

    def test_combined_scaling_and_directional(self):
        """Verify scale + directional compensation work together."""
        bbox = (100, 200, 200, 250)  # width=100, height=50
        strategy = ScaleStrategy(
            scale_x=1.2,  # 20% wider -> 120 width
            scale_y=1.0,  # no height change
            extra_left_ratio=0.5,  # Add 50% of width to left
            max_pad_x=1000,
            max_pad_y=1000,
        )

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="test_field",
            strategies={"test_field": strategy},
        )

        # Center: x=150
        # After scale: width=120 -> x0=150-60=90, x1=150+60=210
        # After extra_left: x0 = 90 - (100 * 0.5) = 40
        assert result[0] == 40  # x0
        assert result[2] == 210  # x1


class TestExpandBboxMaxPadClamping:
    """Tests for max padding clamping."""

    def test_max_pad_x_limits_horizontal_expansion(self):
        """Verify max_pad_x limits expansion on left and right."""
        bbox = (100, 200, 200, 250)  # width=100
        strategy = ScaleStrategy(
            scale_x=2.0,  # Double width (would add 50 each side)
            scale_y=1.0,
            max_pad_x=30,  # Limit to 30 pixels each side
            max_pad_y=1000,
        )

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="test_field",
            strategies={"test_field": strategy},
        )

        # Scale would make: x0=100, x1=200 -> x0=50, x1=250 (50px each side)
        # But max_pad_x=30 limits to: x0=70, x1=230
        assert result[0] == 70  # x0 = 100 - 30
        assert result[2] == 230  # x1 = 200 + 30

    def test_max_pad_y_limits_vertical_expansion(self):
        """Verify max_pad_y limits expansion on top and bottom."""
        bbox = (100, 200, 200, 250)  # height=50
        strategy = ScaleStrategy(
            scale_x=1.0,
            scale_y=3.0,  # Triple height (would add 50 each side)
            max_pad_x=1000,
            max_pad_y=20,  # Limit to 20 pixels each side
        )

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="test_field",
            strategies={"test_field": strategy},
        )

        # Scale would make: y0=175, y1=275 (50px each side)
        # But max_pad_y=20 limits to: y0=180, y1=270
        assert result[1] == 180  # y0 = 200 - 20
        assert result[3] == 270  # y1 = 250 + 20

    def test_max_pad_preserves_asymmetry(self):
        """Verify max_pad clamping preserves asymmetric expansion."""
        bbox = (100, 200, 200, 250)  # width=100
        strategy = ScaleStrategy(
            scale_x=1.0,
            scale_y=1.0,
            extra_left_ratio=1.0,  # 100px left expansion
            extra_right_ratio=0.0,  # No right expansion
            max_pad_x=50,  # Limit to 50 pixels
            max_pad_y=1000,
        )

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="test_field",
            strategies={"test_field": strategy},
        )

        # Left would expand 100, clamped to 50
        # Right stays at 0
        assert result[0] == 50  # x0 = 100 - 50
        assert result[2] == 200  # x1 unchanged


class TestExpandBboxImageBoundaryClamping:
    """Tests for image boundary clamping."""

    def test_clamps_to_left_boundary(self):
        """Verify x0 is clamped to 0."""
        bbox = (10, 200, 110, 250)  # Close to left edge
        strategy = ScaleStrategy(
            scale_x=1.0,
            scale_y=1.0,
            extra_left_ratio=0.5,  # Would push x0 below 0
            max_pad_x=1000,
            max_pad_y=1000,
        )

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="test_field",
            strategies={"test_field": strategy},
        )

        assert result[0] == 0  # Clamped to 0

    def test_clamps_to_top_boundary(self):
        """Verify y0 is clamped to 0."""
        bbox = (100, 10, 200, 60)  # Close to top edge
        strategy = ScaleStrategy(
            scale_x=1.0,
            scale_y=1.0,
            extra_top_ratio=0.5,  # Would push y0 below 0
            max_pad_x=1000,
            max_pad_y=1000,
        )

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="test_field",
            strategies={"test_field": strategy},
        )

        assert result[1] == 0  # Clamped to 0

    def test_clamps_to_right_boundary(self):
        """Verify x1 is clamped to image_width."""
        bbox = (900, 200, 990, 250)  # Close to right edge
        strategy = ScaleStrategy(
            scale_x=1.0,
            scale_y=1.0,
            extra_right_ratio=0.5,  # Would push x1 beyond image_width
            max_pad_x=1000,
            max_pad_y=1000,
        )

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="test_field",
            strategies={"test_field": strategy},
        )

        assert result[2] == 1000  # Clamped to image_width

    def test_clamps_to_bottom_boundary(self):
        """Verify y1 is clamped to image_height."""
        bbox = (100, 940, 200, 990)  # Close to bottom edge
        strategy = ScaleStrategy(
            scale_x=1.0,
            scale_y=1.0,
            extra_bottom_ratio=0.5,  # Would push y1 beyond image_height
            max_pad_x=1000,
            max_pad_y=1000,
        )

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="test_field",
            strategies={"test_field": strategy},
        )

        assert result[3] == 1000  # Clamped to image_height


class TestExpandBboxUnknownField:
    """Tests for unknown field handling."""

    def test_unknown_field_uses_default_strategy(self):
        """Verify unknown field types use DEFAULT_STRATEGY."""
        bbox = (100, 200, 200, 250)

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="unknown_field_xyz",
        )

        # DEFAULT_STRATEGY: scale_x=1.15, scale_y=1.15
        # Original: width=100, height=50
        # New: width=115, height=57.5
        # Center: (150, 225)
        # x0 = 150 - 57.5 = 92.5 -> 92
        # x1 = 150 + 57.5 = 207.5 -> 207
        # y0 = 225 - 28.75 = 196.25 -> 196
        # y1 = 225 + 28.75 = 253.75 -> 253
        # But max_pad_x=50 may clamp...
        # Left pad = 100 - 92.5 = 7.5 (< 50, ok)
        # Right pad = 207.5 - 200 = 7.5 (< 50, ok)
        assert result[0] == 92
        assert result[2] == 207


class TestExpandBboxWithRealStrategies:
    """Tests using actual FIELD_SCALE_STRATEGIES."""

    def test_ocr_number_expands_significantly_upward(self):
        """Verify ocr_number field gets significant upward expansion."""
        bbox = (100, 200, 200, 230)  # Small height=30

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="ocr_number",
        )

        # extra_top_ratio=0.60 -> 30 * 0.6 = 18 extra top
        # y0 should decrease significantly
        assert result[1] < 200 - 10  # At least 10px upward expansion

    def test_bankgiro_expands_significantly_leftward(self):
        """Verify bankgiro field gets significant leftward expansion."""
        bbox = (200, 200, 300, 230)  # width=100

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="bankgiro",
        )

        # extra_left_ratio=0.80 -> 100 * 0.8 = 80 extra left
        # x0 should decrease significantly
        assert result[0] < 200 - 30  # At least 30px leftward expansion

    def test_amount_expands_rightward(self):
        """Verify amount field gets rightward expansion for currency."""
        bbox = (100, 200, 200, 230)  # width=100

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="amount",
        )

        # extra_right_ratio=0.30 -> 100 * 0.3 = 30 extra right
        # x1 should increase
        assert result[2] > 200 + 10  # At least 10px rightward expansion


class TestExpandBboxReturnType:
    """Tests for return type and value format."""

    def test_returns_tuple_of_four_ints(self):
        """Verify return type is tuple of 4 integers."""
        bbox = (100.5, 200.3, 200.7, 250.9)

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="invoice_number",
        )

        assert isinstance(result, tuple)
        assert len(result) == 4
        assert all(isinstance(v, int) for v in result)

    def test_returns_valid_bbox_format(self):
        """Verify returned bbox has x0 < x1 and y0 < y1."""
        bbox = (100, 200, 200, 250)

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="invoice_number",
        )

        x0, y0, x1, y1 = result
        assert x0 < x1, "x0 should be less than x1"
        assert y0 < y1, "y0 should be less than y1"


class TestManualLabelMode:
    """Tests for manual_mode parameter."""

    def test_manual_mode_uses_minimal_padding(self):
        """Verify manual_mode uses MANUAL_LABEL_STRATEGY with minimal padding."""
        bbox = (100, 200, 200, 250)  # width=100, height=50

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="bankgiro",  # Would normally expand left significantly
            manual_mode=True,
        )

        # MANUAL_LABEL_STRATEGY: scale=1.0, max_pad=10
        # Should only add 10px padding each side (but scale=1.0 means no scaling)
        # Actually with scale=1.0, no extra ratios, we get 0 expansion from scaling
        # Only max_pad=10 applies as a limit, but there's no expansion to limit
        # So result should be same as original
        assert result == (100, 200, 200, 250)

    def test_manual_mode_ignores_field_type(self):
        """Verify manual_mode ignores field-specific strategies."""
        bbox = (100, 200, 200, 250)

        # Different fields should give same result in manual_mode
        result_bankgiro = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="bankgiro",
            manual_mode=True,
        )

        result_ocr = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="ocr_number",
            manual_mode=True,
        )

        assert result_bankgiro == result_ocr

    def test_manual_mode_vs_auto_mode_different(self):
        """Verify manual_mode produces different results than auto mode."""
        bbox = (100, 200, 200, 250)

        auto_result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="bankgiro",  # Has extra_left_ratio=0.80
            manual_mode=False,
        )

        manual_result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="bankgiro",
            manual_mode=True,
        )

        # Auto mode should expand more (especially to the left for bankgiro)
        assert auto_result[0] < manual_result[0]  # Auto x0 is more left

    def test_manual_mode_clamps_to_image_bounds(self):
        """Verify manual_mode still respects image boundaries."""
        bbox = (5, 5, 50, 50)  # Close to top-left corner

        result = expand_bbox(
            bbox=bbox,
            image_width=1000,
            image_height=1000,
            field_type="test",
            manual_mode=True,
        )

        # Should clamp to 0
        assert result[0] >= 0
        assert result[1] >= 0
