"""
Tests for cv_graphic_detect.py — graphic element detection.

Lizenz: Apache 2.0
"""

import numpy as np
import pytest

import cv2

from ocr.detect.graphic_detect import detect_graphic_elements, GraphicElement, _dominant_color


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _white_image(width: int = 1200, height: int = 1800) -> np.ndarray:
    """Create a plain white BGR image."""
    return np.ones((height, width, 3), dtype=np.uint8) * 255


def _draw_colored_circle(img: np.ndarray, cx: int, cy: int, radius: int,
                         color_bgr: tuple) -> np.ndarray:
    """Draw a filled colored circle (simulates a balloon / graphic)."""
    cv2.circle(img, (cx, cy), radius, color_bgr, -1)
    return img


def _draw_colored_region(img: np.ndarray, x: int, y: int, w: int, h: int,
                         color_bgr: tuple) -> np.ndarray:
    """Draw a filled colored rectangle (simulates an image region)."""
    cv2.rectangle(img, (x, y), (x + w, y + h), color_bgr, -1)
    return img


def _draw_black_illustration(img: np.ndarray, x: int, y: int, w: int, h: int) -> np.ndarray:
    """Draw a large black filled shape (simulates a black-ink illustration)."""
    cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 0), -1)
    return img


def _word_box(left: int, top: int, width: int, height: int) -> dict:
    """Create a word box dict matching OCR output format."""
    return {"left": left, "top": top, "width": width, "height": height}


# ---------------------------------------------------------------------------
# _dominant_color tests
# ---------------------------------------------------------------------------

class TestDominantColor:
    """Tests for the _dominant_color helper."""

    def test_empty_array(self):
        hsv = np.array([], dtype=np.uint8).reshape(0, 3)
        name, hex_val = _dominant_color(hsv)
        assert name == "black"
        assert hex_val == "#000000"

    def test_low_saturation_returns_black(self):
        """Pixels with low saturation should be classified as black."""
        # HSV: H=90 (irrelevant), S=10 (low), V=200
        hsv = np.full((50, 50, 3), [90, 10, 200], dtype=np.uint8)
        name, _ = _dominant_color(hsv)
        assert name == "black"

    def test_red_hue(self):
        """Pixels with hue ~0-10 or ~170+ should be red."""
        hsv = np.full((50, 50, 3), [5, 200, 200], dtype=np.uint8)
        name, hex_val = _dominant_color(hsv)
        assert name == "red"
        assert hex_val == "#dc2626"

    def test_blue_hue(self):
        """Pixels with hue ~100 should be blue."""
        hsv = np.full((50, 50, 3), [110, 200, 200], dtype=np.uint8)
        name, hex_val = _dominant_color(hsv)
        assert name == "blue"
        assert hex_val == "#2563eb"

    def test_green_hue(self):
        """Pixels with hue ~60 should be green."""
        hsv = np.full((50, 50, 3), [60, 200, 200], dtype=np.uint8)
        name, hex_val = _dominant_color(hsv)
        assert name == "green"
        assert hex_val == "#16a34a"

    def test_yellow_hue(self):
        """Pixels with hue ~30 should be yellow."""
        hsv = np.full((50, 50, 3), [30, 200, 200], dtype=np.uint8)
        name, hex_val = _dominant_color(hsv)
        assert name == "yellow"

    def test_orange_hue(self):
        """Pixels with hue ~15 should be orange."""
        hsv = np.full((50, 50, 3), [15, 200, 200], dtype=np.uint8)
        name, hex_val = _dominant_color(hsv)
        assert name == "orange"

    def test_purple_hue(self):
        """Pixels with hue ~140 should be purple."""
        hsv = np.full((50, 50, 3), [140, 200, 200], dtype=np.uint8)
        name, hex_val = _dominant_color(hsv)
        assert name == "purple"


# ---------------------------------------------------------------------------
# detect_graphic_elements tests
# ---------------------------------------------------------------------------

class TestDetectGraphicElements:
    """Tests for the detect_graphic_elements() function."""

    def test_none_image_returns_empty(self):
        """None input should return empty list."""
        result = detect_graphic_elements(None, [])
        assert result == []

    def test_white_image_no_graphics(self):
        """A plain white image should produce no graphic elements."""
        img = _white_image()
        result = detect_graphic_elements(img, [])
        assert result == []

    def test_colored_region_detected_as_image(self):
        """A large colored rectangle should be detected as an image."""
        img = _white_image()
        # Draw a large red region (not text-like)
        _draw_colored_region(img, x=100, y=300, w=200, h=200, color_bgr=(0, 0, 220))

        result = detect_graphic_elements(img, word_boxes=[])

        assert len(result) >= 1
        graphic = result[0]
        assert isinstance(graphic, GraphicElement)
        assert graphic.shape == "image"
        assert graphic.color_name == "red"
        assert graphic.confidence > 0

    def test_colored_text_excluded_by_word_overlap(self):
        """Colored regions that overlap heavily with word boxes should be skipped."""
        img = _white_image()
        # Draw colored region
        _draw_colored_region(img, x=100, y=300, w=400, h=50, color_bgr=(0, 0, 220))

        # Word boxes covering >50% of the colored region
        words = [
            _word_box(100, 300, 200, 50),
            _word_box(300, 300, 200, 50),
        ]

        result = detect_graphic_elements(img, word_boxes=words)

        # Should be filtered out (word overlap > 50%)
        for g in result:
            # If anything is detected at that location, overlap check failed
            if g.x >= 90 and g.x <= 110 and g.y >= 290 and g.y <= 310:
                pytest.fail("Colored text region should be excluded by word overlap")

    def test_colored_graphic_with_low_word_overlap_kept(self):
        """A colored region with low word overlap should be kept."""
        img = _white_image()
        # Draw a large colored circle
        _draw_colored_circle(img, cx=300, cy=400, radius=80, color_bgr=(0, 200, 0))

        # One small word box overlapping only a tiny portion
        words = [_word_box(250, 390, 30, 20)]

        result = detect_graphic_elements(img, word_boxes=words)

        assert len(result) >= 1
        assert result[0].shape == "image"
        assert result[0].color_name == "green"

    def test_black_illustration_detected(self):
        """A large black filled area should be detected as illustration."""
        img = _white_image()
        # Draw a large black rectangle (simulating an illustration)
        _draw_black_illustration(img, x=200, y=400, w=300, h=300)

        result = detect_graphic_elements(img, word_boxes=[])

        assert len(result) >= 1
        illust = [g for g in result if g.shape == "illustration"]
        assert len(illust) >= 1
        assert illust[0].color_name == "black"

    def test_black_illustration_excluded_by_word_boxes(self):
        """Black ink in word regions should NOT be detected as illustration."""
        img = _white_image()
        # Draw black text-like region
        _draw_black_illustration(img, x=100, y=300, w=400, h=60)

        # Word boxes covering the same area
        words = [
            _word_box(100, 300, 200, 60),
            _word_box(300, 300, 200, 60),
        ]

        result = detect_graphic_elements(img, word_boxes=words)

        # Should be empty — the word exclusion mask covers the ink
        illust = [g for g in result if g.shape == "illustration"]
        assert len(illust) == 0

    def test_tiny_colored_region_filtered(self):
        """Very small colored regions (<200 colored pixels) should be filtered."""
        img = _white_image()
        # Draw a tiny colored dot (5x5 pixels)
        _draw_colored_region(img, x=500, y=500, w=5, h=5, color_bgr=(220, 0, 0))

        result = detect_graphic_elements(img, word_boxes=[])
        assert result == []

    def test_page_spanning_region_filtered(self):
        """Colored regions spanning >50% of width/height should be skipped."""
        img = _white_image(width=1200, height=1800)
        # Draw a region wider than 50% of the image
        _draw_colored_region(img, x=50, y=300, w=700, h=100, color_bgr=(0, 0, 220))

        result = detect_graphic_elements(img, word_boxes=[])

        # Should be filtered as page-spanning
        assert result == []

    def test_multiple_graphics_detected(self):
        """Multiple separate colored regions should all be detected."""
        img = _white_image()
        # Three separate colored circles
        _draw_colored_circle(img, cx=200, cy=300, radius=60, color_bgr=(0, 0, 220))
        _draw_colored_circle(img, cx=500, cy=300, radius=60, color_bgr=(0, 200, 0))
        _draw_colored_circle(img, cx=200, cy=600, radius=60, color_bgr=(220, 0, 0))

        result = detect_graphic_elements(img, word_boxes=[])

        # Should detect at least 2 (some may merge if dilation connects them)
        assert len(result) >= 2

    def test_results_sorted_by_area_descending(self):
        """Results should be sorted by area, largest first."""
        img = _white_image()
        # Small circle
        _draw_colored_circle(img, cx=200, cy=300, radius=30, color_bgr=(0, 0, 220))
        # Large circle
        _draw_colored_circle(img, cx=600, cy=800, radius=100, color_bgr=(0, 200, 0))

        result = detect_graphic_elements(img, word_boxes=[])

        if len(result) >= 2:
            assert result[0].area >= result[1].area

    def test_max_elements_limit(self):
        """Should respect max_elements parameter."""
        img = _white_image(width=2000, height=2000)
        # Draw many colored regions
        for i in range(10):
            _draw_colored_circle(img, cx=100 + i * 180, cy=300, radius=40,
                                 color_bgr=(0, 0, 220))

        result = detect_graphic_elements(img, word_boxes=[], max_elements=3)

        assert len(result) <= 3

    def test_detected_boxes_excluded_from_ink(self):
        """Detected box regions should be excluded from ink illustration detection."""
        img = _white_image()
        # Draw a black rectangle well inside the "box" area (8px inset is used)
        _draw_black_illustration(img, x=120, y=320, w=360, h=160)

        # Mark the outer box — the 8px inset still covers the drawn region
        detected_boxes = [{"x": 100, "y": 300, "w": 400, "h": 200}]

        result = detect_graphic_elements(img, word_boxes=[], detected_boxes=detected_boxes)

        illust = [g for g in result if g.shape == "illustration"]
        assert len(illust) == 0

    def test_deduplication_overlapping_regions(self):
        """Overlapping elements should be deduplicated."""
        img = _white_image()
        # Two overlapping colored regions
        _draw_colored_region(img, x=200, y=300, w=200, h=200, color_bgr=(0, 0, 220))
        _draw_colored_region(img, x=250, y=350, w=200, h=200, color_bgr=(0, 0, 220))

        result = detect_graphic_elements(img, word_boxes=[])

        # Should be merged/deduplicated into 1 element (heavy dilation merges them)
        assert len(result) <= 2

    def test_graphicelement_dataclass_fields(self):
        """GraphicElement should have all expected fields."""
        elem = GraphicElement(
            x=10, y=20, width=100, height=80,
            area=5000, shape="image",
            color_name="red", color_hex="#dc2626",
            confidence=0.85,
        )
        assert elem.x == 10
        assert elem.y == 20
        assert elem.width == 100
        assert elem.height == 80
        assert elem.area == 5000
        assert elem.shape == "image"
        assert elem.color_name == "red"
        assert elem.color_hex == "#dc2626"
        assert elem.confidence == 0.85
        assert elem.contour is None

    def test_small_ink_area_filtered(self):
        """Black ink areas smaller than 5000px should be filtered."""
        img = _white_image()
        # Small black mark (50x50 = 2500 area, below 5000 threshold)
        _draw_black_illustration(img, x=500, y=500, w=50, h=50)

        result = detect_graphic_elements(img, word_boxes=[])

        illust = [g for g in result if g.shape == "illustration"]
        assert len(illust) == 0
