"""Tests for OCR detection functionality."""

import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch

import numpy as np
import pytest
from PIL import Image

from video_lens.analysis.ocr_detector import (
    OCRDetector,
    OCRResult,
    TextRegion,
    create_ocr_detector,
)
from video_lens.core.exceptions import ErrorCode, VideoProcessingError
from video_lens.utils.config import VideoLensConfig, VisualAnalysisConfig


@pytest.fixture
def mock_config():
    """Create mock configuration for testing."""
    config = VideoLensConfig(
        visual_analysis=VisualAnalysisConfig(
            enable_ocr=True,
            ocr_engine="tesseract",
            ocr_languages=["eng"],
            ocr_confidence_threshold=60.0,
            ocr_text_min_length=3,
            detect_slide_text=True,
            detect_ui_text=False,
        )
    )
    return config


@pytest.fixture
def mock_config_disabled():
    """Create mock configuration with OCR disabled."""
    config = VideoLensConfig(
        visual_analysis=VisualAnalysisConfig(
            enable_ocr=False,
            ocr_engine="tesseract",
            ocr_languages=["eng"],
        )
    )
    return config


@pytest.fixture
def mock_config_easyocr():
    """Create mock configuration for EasyOCR."""
    config = VideoLensConfig(
        visual_analysis=VisualAnalysisConfig(
            enable_ocr=True,
            ocr_engine="easyocr",
            ocr_languages=["eng"],
            ocr_confidence_threshold=60.0,
            ocr_text_min_length=3,
        )
    )
    return config


@pytest.fixture
def sample_image():
    """Create sample PIL Image for testing."""
    # Create a simple test image with text-like patterns
    image_array = np.ones((200, 400, 3), dtype=np.uint8) * 255  # White background

    # Add some text-like rectangles (simulate text)
    image_array[50:70, 50:200] = [0, 0, 0]  # Black rectangle (title area)
    image_array[100:120, 50:350] = [0, 0, 0]  # Black rectangle (content area)
    image_array[150:170, 50:150] = [0, 0, 0]  # Black rectangle (smaller text)

    return Image.fromarray(image_array)


@pytest.fixture
def sample_image_array():
    """Create sample numpy array for testing."""
    # Create a simple test image with text patterns
    image_array = np.ones((150, 300, 3), dtype=np.uint8) * 255  # White background

    # Add text-like patterns
    image_array[30:50, 30:150] = [0, 0, 0]  # Header text
    image_array[80:100, 30:250] = [0, 0, 0]  # Body text

    return image_array


class TestTextRegion:
    """Test TextRegion model."""

    def test_text_region_creation(self):
        """Test creating TextRegion object."""
        region = TextRegion(
            text="Sample text content",
            confidence=85.5,
            bbox=(10, 20, 100, 30),
            language="eng",
            font_size_estimate=1.2,
            is_title=True,
            is_slide_number=False,
        )

        assert region.text == "Sample text content"
        assert region.confidence == 85.5
        assert region.bbox == (10, 20, 100, 30)
        assert region.language == "eng"
        assert region.font_size_estimate == 1.2
        assert region.is_title is True
        assert region.is_slide_number is False


class TestOCRResult:
    """Test OCRResult model."""

    def test_ocr_result_creation(self):
        """Test creating OCRResult object."""
        text_region = TextRegion(
            text="Test text",
            confidence=75.0,
            bbox=(0, 0, 50, 20),
        )

        result = OCRResult(
            text_regions=[text_region],
            full_text="Test text",
            processing_time=1.5,
            engine_used="tesseract",
            languages_detected=["eng"],
            total_text_regions=1,
            high_confidence_regions=1,
            average_confidence=75.0,
        )

        assert len(result.text_regions) == 1
        assert result.full_text == "Test text"
        assert result.processing_time == 1.5
        assert result.engine_used == "tesseract"
        assert result.languages_detected == ["eng"]
        assert result.total_text_regions == 1
        assert result.high_confidence_regions == 1
        assert result.average_confidence == 75.0


class TestOCRDetector:
    """Test OCRDetector class."""

    @patch("video_lens.analysis.ocr_detector._tesseract_available", True)
    @patch("video_lens.analysis.ocr_detector.pytesseract")
    def test_initialization_tesseract(self, mock_pytesseract, mock_config):
        """Test OCRDetector initialization with Tesseract."""
        mock_pytesseract.get_tesseract_version.return_value = "5.0.0"

        detector = OCRDetector(config=mock_config)

        assert detector.config == mock_config
        assert detector.engine == "tesseract"
        assert detector.languages == ["eng"]
        assert detector.confidence_threshold == 60.0
        mock_pytesseract.get_tesseract_version.assert_called_once()

    @patch("video_lens.analysis.ocr_detector._easyocr_available", True)
    @patch("video_lens.analysis.ocr_detector.easyocr")
    def test_initialization_easyocr(self, mock_easyocr, mock_config_easyocr):
        """Test OCRDetector initialization with EasyOCR."""
        mock_reader = MagicMock()
        mock_easyocr.Reader.return_value = mock_reader

        detector = OCRDetector(config=mock_config_easyocr)

        assert detector.config == mock_config_easyocr
        assert detector.engine == "easyocr"
        assert detector.languages == ["eng"]
        mock_easyocr.Reader.assert_called_once_with(["en"], gpu=False)

    def test_initialization_default_config(self):
        """Test OCRDetector initialization with default config."""
        with patch("video_lens.analysis.ocr_detector.get_config") as mock_get_config:
            mock_config = MagicMock()
            mock_config.visual_analysis.ocr_engine = "tesseract"
            mock_config.visual_analysis.ocr_languages = ["eng"]
            mock_config.visual_analysis.ocr_confidence_threshold = 60.0
            mock_config.visual_analysis.ocr_text_min_length = 3
            mock_get_config.return_value = mock_config

            with (
                patch("video_lens.analysis.ocr_detector._tesseract_available", True),
                patch(
                    "video_lens.analysis.ocr_detector.pytesseract"
                ) as mock_pytesseract,
            ):
                mock_pytesseract.get_tesseract_version.return_value = "5.0.0"

                detector = OCRDetector()

                assert detector.config == mock_config
                assert detector.engine == "tesseract"
                mock_get_config.assert_called_once()

    @patch("video_lens.analysis.ocr_detector._tesseract_available", False)
    def test_initialization_missing_tesseract(self, mock_config):
        """Test OCRDetector initialization with missing Tesseract."""
        with pytest.raises(VideoProcessingError) as exc_info:
            OCRDetector(config=mock_config)

        assert exc_info.value.error_code == ErrorCode.MISSING_DEPENDENCY
        assert "Tesseract OCR not available" in str(exc_info.value)

    @patch("video_lens.analysis.ocr_detector._easyocr_available", False)
    def test_initialization_missing_easyocr(self, mock_config_easyocr):
        """Test OCRDetector initialization with missing EasyOCR."""
        with pytest.raises(VideoProcessingError) as exc_info:
            OCRDetector(config=mock_config_easyocr)

        assert exc_info.value.error_code == ErrorCode.MISSING_DEPENDENCY
        assert "EasyOCR not available" in str(exc_info.value)

    @patch("video_lens.analysis.ocr_detector._tesseract_available", True)
    @patch("video_lens.analysis.ocr_detector.pytesseract")
    def test_detect_text_disabled(self, mock_pytesseract, mock_config_disabled):
        """Test text detection when OCR is disabled."""
        mock_pytesseract.get_tesseract_version.return_value = "5.0.0"
        detector = OCRDetector(config=mock_config_disabled)

        result = detector.detect_text(pil_image=Image.new("RGB", (100, 100)))

        assert result.full_text == ""
        assert result.engine_used == "none"
        assert len(result.text_regions) == 0

    def test_detect_text_no_input(self):
        """Test text detection with no input provided."""
        with (
            patch("video_lens.analysis.ocr_detector._tesseract_available", True),
            patch("video_lens.analysis.ocr_detector.pytesseract") as mock_pytesseract,
        ):
            mock_pytesseract.get_tesseract_version.return_value = "5.0.0"
            detector = OCRDetector()

            with pytest.raises(ValueError) as exc_info:
                detector.detect_text()

            assert "Must provide one of" in str(exc_info.value)

    @patch("video_lens.analysis.ocr_detector._tesseract_available", True)
    @patch("video_lens.analysis.ocr_detector.pytesseract")
    def test_detect_text_pil_success(self, mock_pytesseract, mock_config, sample_image):
        """Test successful text detection with PIL Image."""
        mock_pytesseract.get_tesseract_version.return_value = "5.0.0"

        # Mock Tesseract OCR results
        mock_pytesseract.image_to_data.return_value = {
            "level": [1, 2, 3],
            "conf": [85.0, 75.0, 90.0],
            "text": ["Title Text", "Body content", "Footer"],
            "left": [50, 50, 50],
            "top": [50, 100, 150],
            "width": [150, 300, 100],
            "height": [20, 20, 20],
        }

        detector = OCRDetector(config=mock_config)
        result = detector.detect_text(pil_image=sample_image)

        assert isinstance(result, OCRResult)
        assert len(result.text_regions) == 3
        assert result.engine_used == "tesseract"
        assert "Title Text" in result.full_text
        assert result.processing_time > 0

    @patch("video_lens.analysis.ocr_detector._tesseract_available", True)
    @patch("video_lens.analysis.ocr_detector.pytesseract")
    def test_detect_text_array_success(
        self, mock_pytesseract, mock_config, sample_image_array
    ):
        """Test successful text detection with numpy array."""
        mock_pytesseract.get_tesseract_version.return_value = "5.0.0"

        # Mock Tesseract OCR results
        mock_pytesseract.image_to_data.return_value = {
            "level": [1, 2],
            "conf": [80.0, 70.0],
            "text": ["Header", "Content"],
            "left": [30, 30],
            "top": [30, 80],
            "width": [120, 220],
            "height": [20, 20],
        }

        detector = OCRDetector(config=mock_config)
        result = detector.detect_text(image_array=sample_image_array)

        assert isinstance(result, OCRResult)
        assert len(result.text_regions) == 2
        assert "Header Content" in result.full_text

    @patch("video_lens.analysis.ocr_detector._tesseract_available", True)
    @patch("video_lens.analysis.ocr_detector.pytesseract")
    def test_detect_text_path_not_found(self, mock_pytesseract, mock_config):
        """Test text detection with non-existent file."""
        mock_pytesseract.get_tesseract_version.return_value = "5.0.0"
        detector = OCRDetector(config=mock_config)

        non_existent_path = Path("/nonexistent/image.jpg")

        with pytest.raises(VideoProcessingError) as exc_info:
            detector.detect_text(image_path=non_existent_path)

        assert exc_info.value.error_code == ErrorCode.FILE_NOT_FOUND
        assert "Image file not found" in str(exc_info.value)

    @patch("video_lens.analysis.ocr_detector._tesseract_available", True)
    @patch("video_lens.analysis.ocr_detector.pytesseract")
    def test_detect_text_path_success(
        self, mock_pytesseract, mock_config, sample_image
    ):
        """Test successful text detection with file path."""
        mock_pytesseract.get_tesseract_version.return_value = "5.0.0"

        # Create temporary image file
        with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as temp_file:
            sample_image.save(temp_file.name)
            image_path = Path(temp_file.name)

        try:
            # Mock Tesseract OCR results
            mock_pytesseract.image_to_data.return_value = {
                "level": [1],
                "conf": [85.0],
                "text": ["Saved image text"],
                "left": [50],
                "top": [50],
                "width": [150],
                "height": [20],
            }

            detector = OCRDetector(config=mock_config)
            result = detector.detect_text(image_path=image_path)

            assert isinstance(result, OCRResult)
            assert "Saved image text" in result.full_text

        finally:
            # Clean up
            if image_path.exists():
                image_path.unlink()

    @patch("video_lens.analysis.ocr_detector._easyocr_available", True)
    @patch("video_lens.analysis.ocr_detector.easyocr")
    def test_detect_text_easyocr_success(
        self, mock_easyocr, mock_config_easyocr, sample_image
    ):
        """Test successful text detection with EasyOCR."""
        # Mock EasyOCR
        mock_reader = MagicMock()
        mock_easyocr.Reader.return_value = mock_reader

        # Mock EasyOCR results format: [bbox_points, text, confidence]
        mock_reader.readtext.return_value = [
            ([[10, 10], [110, 10], [110, 30], [10, 30]], "Sample Text", 0.85),
            ([[10, 50], [200, 50], [200, 70], [10, 70]], "Another Line", 0.75),
        ]

        detector = OCRDetector(config=mock_config_easyocr)
        result = detector.detect_text(pil_image=sample_image)

        assert isinstance(result, OCRResult)
        assert len(result.text_regions) == 2
        assert "Sample Text Another Line" in result.full_text
        assert result.engine_used == "easyocr"

    @patch("video_lens.analysis.ocr_detector._tesseract_available", True)
    @patch("video_lens.analysis.ocr_detector.pytesseract")
    def test_detect_text_failure(self, mock_pytesseract, mock_config, sample_image):
        """Test text detection with OCR failure."""
        mock_pytesseract.get_tesseract_version.return_value = "5.0.0"

        # Mock _detect_text_tesseract to raise an exception
        detector = OCRDetector(config=mock_config)

        with patch.object(
            detector,
            "_detect_text_tesseract",
            side_effect=Exception("OCR processing error"),
        ):
            with pytest.raises(VideoProcessingError) as exc_info:
                detector.detect_text(pil_image=sample_image)

            assert exc_info.value.error_code == ErrorCode.FRAME_EXTRACTION_FAILED
            assert "OCR detection failed" in str(exc_info.value)

    @patch("video_lens.analysis.ocr_detector._tesseract_available", True)
    @patch("video_lens.analysis.ocr_detector.pytesseract")
    def test_filter_text_regions(self, mock_pytesseract, mock_config):
        """Test text region filtering by confidence and length."""
        mock_pytesseract.get_tesseract_version.return_value = "5.0.0"
        detector = OCRDetector(config=mock_config)

        # Create regions with various confidence and lengths
        regions = [
            TextRegion(text="Good text", confidence=75.0, bbox=(0, 0, 50, 20)),  # Keep
            TextRegion(
                text="Low confidence", confidence=30.0, bbox=(0, 0, 50, 20)
            ),  # Filter out
            TextRegion(
                text="Ok", confidence=70.0, bbox=(0, 0, 50, 20)
            ),  # Filter out (too short)
            TextRegion(
                text="Another good text", confidence=80.0, bbox=(0, 0, 50, 20)
            ),  # Keep
            TextRegion(
                text="   ", confidence=90.0, bbox=(0, 0, 50, 20)
            ),  # Filter out (whitespace)
        ]

        filtered = detector._filter_text_regions(regions)

        assert len(filtered) == 2
        assert filtered[0].text == "Good text"
        assert filtered[1].text == "Another good text"

    @patch("video_lens.analysis.ocr_detector._tesseract_available", True)
    @patch("video_lens.analysis.ocr_detector.pytesseract")
    def test_analyze_text_regions(self, mock_pytesseract, mock_config):
        """Test text region analysis for semantic information."""
        mock_pytesseract.get_tesseract_version.return_value = "5.0.0"
        detector = OCRDetector(config=mock_config)

        # Create regions with different characteristics
        regions = [
            TextRegion(
                text="PRESENTATION TITLE", confidence=85.0, bbox=(50, 10, 200, 40)
            ),  # Large, top
            TextRegion(
                text="Body content here", confidence=75.0, bbox=(50, 100, 300, 20)
            ),  # Normal
            TextRegion(
                text="Slide 5", confidence=80.0, bbox=(350, 450, 50, 15)
            ),  # Small, bottom-right
        ]

        analyzed = detector._analyze_text_regions(regions)

        assert len(analyzed) == 3
        assert analyzed[0].is_title is True  # Large font at top
        assert analyzed[1].is_title is False  # Normal content
        assert analyzed[2].is_slide_number is True  # Small, numbers, bottom area

    @patch("video_lens.analysis.ocr_detector._tesseract_available", True)
    @patch("video_lens.analysis.ocr_detector.pytesseract")
    def test_detect_text_batch_success(
        self, mock_pytesseract, mock_config, sample_image
    ):
        """Test batch text detection functionality."""
        mock_pytesseract.get_tesseract_version.return_value = "5.0.0"

        # Mock different results for each call
        mock_pytesseract.image_to_data.side_effect = [
            {
                "level": [1],
                "conf": [85.0],
                "text": ["First image"],
                "left": [50],
                "top": [50],
                "width": [100],
                "height": [20],
            },
            {
                "level": [1],
                "conf": [75.0],
                "text": ["Second image"],
                "left": [50],
                "top": [50],
                "width": [100],
                "height": [20],
            },
        ]

        images = [sample_image, sample_image]

        detector = OCRDetector(config=mock_config)
        results = detector.detect_text_batch(images)

        assert len(results) == 2
        assert "First image" in results[0].full_text
        assert "Second image" in results[1].full_text

    @patch("video_lens.analysis.ocr_detector._tesseract_available", True)
    @patch("video_lens.analysis.ocr_detector.pytesseract")
    def test_detect_text_batch_partial_failure(
        self, mock_pytesseract, mock_config, sample_image
    ):
        """Test batch text detection with some failures."""
        mock_pytesseract.get_tesseract_version.return_value = "5.0.0"

        detector = OCRDetector(config=mock_config)

        # Mock detect_text to succeed for first image, fail for second
        def mock_detect_text(*_args, **_kwargs):
            if not hasattr(mock_detect_text, "call_count"):
                mock_detect_text.call_count = 0
            mock_detect_text.call_count += 1

            if mock_detect_text.call_count == 1:
                # First call succeeds
                return OCRResult(
                    text_regions=[
                        TextRegion(
                            text="Success", confidence=85.0, bbox=(50, 50, 100, 20)
                        )
                    ],
                    full_text="Success",
                    processing_time=0.1,
                    engine_used="tesseract",
                    languages_detected=["eng"],
                    total_text_regions=1,
                    high_confidence_regions=1,
                    average_confidence=85.0,
                )
            else:
                # Second call fails
                raise Exception("OCR failed")

        with patch.object(detector, "detect_text", side_effect=mock_detect_text):
            images = [sample_image, sample_image]
            results = detector.detect_text_batch(images)

            assert len(results) == 2
            assert "Success" in results[0].full_text
            assert "OCR failed" in results[1].full_text
            assert results[1].total_text_regions == 0

    @patch("video_lens.analysis.ocr_detector._tesseract_available", True)
    @patch("video_lens.analysis.ocr_detector.pytesseract")
    def test_get_supported_languages_tesseract(self, mock_pytesseract, mock_config):
        """Test getting supported languages for Tesseract."""
        mock_pytesseract.get_tesseract_version.return_value = "5.0.0"
        mock_pytesseract.get_languages.return_value = ["eng", "spa", "fra"]

        detector = OCRDetector(config=mock_config)
        languages = detector.get_supported_languages()

        assert "eng" in languages
        assert "spa" in languages
        assert "fra" in languages

    @patch("video_lens.analysis.ocr_detector._easyocr_available", True)
    @patch("video_lens.analysis.ocr_detector.easyocr")
    def test_get_supported_languages_easyocr(self, mock_easyocr, mock_config_easyocr):
        """Test getting supported languages for EasyOCR."""
        mock_reader = MagicMock()
        mock_easyocr.Reader.return_value = mock_reader

        detector = OCRDetector(config=mock_config_easyocr)
        languages = detector.get_supported_languages()

        assert "en" in languages
        assert "es" in languages
        assert "fr" in languages

    @patch("video_lens.analysis.ocr_detector._tesseract_available", True)
    @patch("video_lens.analysis.ocr_detector.pytesseract")
    def test_preprocess_image(self, mock_pytesseract, mock_config, sample_image):
        """Test image preprocessing for OCR."""
        mock_pytesseract.get_tesseract_version.return_value = "5.0.0"
        detector = OCRDetector(config=mock_config)

        # Test preprocessing
        processed = detector._preprocess_image(sample_image)

        assert isinstance(processed, Image.Image)
        # Processed image should be grayscale (mode 'L')
        assert processed.mode == "L"

    @patch("video_lens.analysis.ocr_detector._easyocr_available", True)
    @patch("video_lens.analysis.ocr_detector.easyocr")
    def test_cleanup_easyocr(self, mock_easyocr, mock_config_easyocr):
        """Test OCR detector cleanup with EasyOCR."""
        mock_reader = MagicMock()
        mock_easyocr.Reader.return_value = mock_reader

        detector = OCRDetector(config=mock_config_easyocr)
        detector.cleanup()

        assert detector.easyocr_reader is None


class TestOCRDetectorFactory:
    """Test OCRDetector factory function."""

    def test_create_ocr_detector_no_config(self):
        """Test creating OCRDetector without config."""
        with patch("video_lens.analysis.ocr_detector.get_config") as mock_get_config:
            mock_config = MagicMock()
            mock_config.visual_analysis.ocr_engine = "tesseract"
            mock_config.visual_analysis.ocr_languages = ["eng"]
            mock_config.visual_analysis.ocr_confidence_threshold = 60.0
            mock_config.visual_analysis.ocr_text_min_length = 3
            mock_get_config.return_value = mock_config

            with (
                patch("video_lens.analysis.ocr_detector._tesseract_available", True),
                patch(
                    "video_lens.analysis.ocr_detector.pytesseract"
                ) as mock_pytesseract,
            ):
                mock_pytesseract.get_tesseract_version.return_value = "5.0.0"

                detector = create_ocr_detector()

                assert isinstance(detector, OCRDetector)
                assert detector.config == mock_config

    @patch("video_lens.analysis.ocr_detector._tesseract_available", True)
    @patch("video_lens.analysis.ocr_detector.pytesseract")
    def test_create_ocr_detector_with_config(self, mock_pytesseract, mock_config):
        """Test creating OCRDetector with config."""
        mock_pytesseract.get_tesseract_version.return_value = "5.0.0"
        detector = create_ocr_detector(config=mock_config)

        assert isinstance(detector, OCRDetector)
        assert detector.config == mock_config


class TestOCRDetectorIntegration:
    """Integration tests for OCR detection workflow."""

    @patch("video_lens.analysis.ocr_detector._tesseract_available", True)
    @patch("video_lens.analysis.ocr_detector.pytesseract")
    def test_full_ocr_workflow(self, mock_pytesseract, mock_config, sample_image):
        """Test complete OCR detection workflow."""
        mock_pytesseract.get_tesseract_version.return_value = "5.0.0"

        # Mock realistic OCR results
        mock_pytesseract.image_to_data.return_value = {
            "level": [1, 2, 3, 4],
            "conf": [90.0, 85.0, 75.0, 40.0],  # Last one will be filtered out
            "text": ["TITLE TEXT", "Subtitle here", "Body content with details", "low"],
            "left": [50, 50, 50, 300],
            "top": [20, 60, 120, 400],
            "width": [200, 150, 300, 30],
            "height": [25, 20, 18, 12],
        }

        detector = OCRDetector(config=mock_config)
        result = detector.detect_text(pil_image=sample_image, preprocess=True)

        # Verify overall results
        assert isinstance(result, OCRResult)
        assert result.processing_time > 0
        assert result.engine_used == "tesseract"

        # Should have filtered out low-confidence text
        assert result.total_text_regions == 3  # Filtered from 4 to 3
        assert result.high_confidence_regions == 3

        # Verify semantic analysis (check that analysis was performed)
        # Note: title detection depends on bbox height relative to average
        # In our mock data, the first region has height=25, others have 20,18
        # So it should be detected as title since 25 > avg(20.75) * 1.5 = 31.125 is false
        # Let's just verify the analysis was performed
        assert all(
            hasattr(region, "font_size_estimate") for region in result.text_regions
        )
        assert all(hasattr(region, "is_title") for region in result.text_regions)
        assert all(hasattr(region, "is_slide_number") for region in result.text_regions)

        # Verify full text concatenation
        assert "TITLE TEXT" in result.full_text
        assert "Body content" in result.full_text
