"""
Tests for custom exceptions.
"""

import pytest
import sys
from pathlib import Path

# Add project root to path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))

from shared.exceptions import (
    InvoiceExtractionError,
    PDFProcessingError,
    OCRError,
    ModelInferenceError,
    FieldValidationError,
    DatabaseError,
    ConfigurationError,
    PaymentLineParseError,
    CustomerNumberParseError,
)


class TestExceptionHierarchy:
    """Test exception inheritance and hierarchy."""

    def test_all_exceptions_inherit_from_base(self):
        """Test that all custom exceptions inherit from InvoiceExtractionError."""
        exceptions = [
            PDFProcessingError,
            OCRError,
            ModelInferenceError,
            FieldValidationError,
            DatabaseError,
            ConfigurationError,
            PaymentLineParseError,
            CustomerNumberParseError,
        ]

        for exc_class in exceptions:
            assert issubclass(exc_class, InvoiceExtractionError)
            assert issubclass(exc_class, Exception)

    def test_base_exception_with_message(self):
        """Test base exception with simple message."""
        error = InvoiceExtractionError("Something went wrong")
        assert str(error) == "Something went wrong"
        assert error.message == "Something went wrong"
        assert error.details == {}

    def test_base_exception_with_details(self):
        """Test base exception with additional details."""
        error = InvoiceExtractionError(
            "Processing failed",
            details={"doc_id": "123", "page": 1}
        )
        assert "Processing failed" in str(error)
        assert "doc_id=123" in str(error)
        assert "page=1" in str(error)
        assert error.details["doc_id"] == "123"


class TestSpecificExceptions:
    """Test specific exception types."""

    def test_pdf_processing_error(self):
        """Test PDFProcessingError."""
        error = PDFProcessingError("Failed to convert PDF", {"path": "/tmp/test.pdf"})
        assert isinstance(error, InvoiceExtractionError)
        assert "Failed to convert PDF" in str(error)

    def test_ocr_error(self):
        """Test OCRError."""
        error = OCRError("OCR engine failed", {"engine": "PaddleOCR"})
        assert isinstance(error, InvoiceExtractionError)
        assert "OCR engine failed" in str(error)

    def test_model_inference_error(self):
        """Test ModelInferenceError."""
        error = ModelInferenceError("YOLO detection failed")
        assert isinstance(error, InvoiceExtractionError)
        assert "YOLO detection failed" in str(error)

    def test_field_validation_error(self):
        """Test FieldValidationError with specific attributes."""
        error = FieldValidationError(
            field_name="amount",
            value="invalid",
            reason="Not a valid number"
        )

        assert isinstance(error, InvoiceExtractionError)
        assert error.field_name == "amount"
        assert error.value == "invalid"
        assert error.reason == "Not a valid number"
        assert "amount" in str(error)
        assert "validation failed" in str(error)

    def test_database_error(self):
        """Test DatabaseError."""
        error = DatabaseError("Connection failed", {"host": "localhost"})
        assert isinstance(error, InvoiceExtractionError)
        assert "Connection failed" in str(error)

    def test_configuration_error(self):
        """Test ConfigurationError."""
        error = ConfigurationError("Missing required config")
        assert isinstance(error, InvoiceExtractionError)
        assert "Missing required config" in str(error)

    def test_payment_line_parse_error(self):
        """Test PaymentLineParseError."""
        error = PaymentLineParseError(
            "Invalid format",
            {"text": "# 123 # invalid"}
        )
        assert isinstance(error, InvoiceExtractionError)
        assert "Invalid format" in str(error)

    def test_customer_number_parse_error(self):
        """Test CustomerNumberParseError."""
        error = CustomerNumberParseError(
            "No pattern matched",
            {"text": "ABC 123"}
        )
        assert isinstance(error, InvoiceExtractionError)
        assert "No pattern matched" in str(error)


class TestExceptionCatching:
    """Test exception catching in try/except blocks."""

    def test_catch_specific_exception(self):
        """Test catching specific exception type."""
        with pytest.raises(PDFProcessingError):
            raise PDFProcessingError("Test error")

    def test_catch_base_exception(self):
        """Test catching via base class."""
        with pytest.raises(InvoiceExtractionError):
            raise PDFProcessingError("Test error")

    def test_catch_multiple_exceptions(self):
        """Test catching multiple exception types."""
        def risky_operation(error_type: str):
            if error_type == "pdf":
                raise PDFProcessingError("PDF error")
            elif error_type == "ocr":
                raise OCRError("OCR error")
            else:
                raise ValueError("Unknown error")

        # Catch specific exceptions
        with pytest.raises((PDFProcessingError, OCRError)):
            risky_operation("pdf")

        with pytest.raises((PDFProcessingError, OCRError)):
            risky_operation("ocr")

        # Different exception should not be caught
        with pytest.raises(ValueError):
            risky_operation("other")

    def test_exception_details_preserved(self):
        """Test that exception details are preserved when caught."""
        try:
            raise FieldValidationError(
                field_name="test_field",
                value="bad_value",
                reason="Test reason",
                details={"extra": "info"}
            )
        except FieldValidationError as e:
            assert e.field_name == "test_field"
            assert e.value == "bad_value"
            assert e.reason == "Test reason"
            assert e.details["extra"] == "info"


class TestExceptionReraising:
    """Test exception re-raising patterns."""

    def test_reraise_as_different_exception(self):
        """Test converting one exception type to another."""
        def low_level_operation():
            raise ValueError("Low-level error")

        def high_level_operation():
            try:
                low_level_operation()
            except ValueError as e:
                raise PDFProcessingError(
                    f"High-level error: {e}",
                    details={"original_error": str(e)}
                ) from e

        with pytest.raises(PDFProcessingError) as exc_info:
            high_level_operation()

        # Verify exception chain is preserved
        assert exc_info.value.__cause__.__class__ == ValueError
        assert "Low-level error" in str(exc_info.value.__cause__)
