# File: tests/unit/domain/services/test_unified_error_handler.py
# Purpose: Test UnifiedErrorHandler service implementation
# Context: Validates error handling, recovery strategies, and logging

"""Tests for UnifiedErrorHandler service.

Test coverage:
- Error classification and categorization
- Recovery strategy selection
- Recovery execution
- Error statistics and tracking
- Callback registration and execution
"""

from datetime import datetime, timedelta
from unittest.mock import Mock

import pytest

from noveler.domain.services.unified_error_handler import (
    ErrorCategory,
    ErrorContext,
    RecoveryStrategy,
    UnifiedErrorHandler,
)


@pytest.fixture
def mock_logger():
    """Create a mock logger."""
    return Mock()


@pytest.fixture
def error_handler(mock_logger):
    """Create an UnifiedErrorHandler instance."""
    return UnifiedErrorHandler(logger=mock_logger, service_name="TestService")


class TestErrorClassification:
    """Test error classification logic."""

    def test_classify_file_not_found_error(self, error_handler):
        """Test classification of FileNotFoundError."""
        error = FileNotFoundError("File not found")
        category = error_handler._classify_error(error)
        assert category == ErrorCategory.INFRASTRUCTURE

    def test_classify_value_error(self, error_handler):
        """Test classification of ValueError."""
        error = ValueError("Invalid value")
        category = error_handler._classify_error(error)
        assert category == ErrorCategory.VALIDATION

    def test_classify_type_error(self, error_handler):
        """Test classification of TypeError."""
        error = TypeError("Type mismatch")
        category = error_handler._classify_error(error)
        assert category == ErrorCategory.VALIDATION

    def test_classify_timeout_error(self, error_handler):
        """Test classification of TimeoutError."""
        error = TimeoutError("Timeout occurred")
        category = error_handler._classify_error(error)
        assert category == ErrorCategory.LLM

    def test_classify_unknown_error(self, error_handler):
        """Test classification of unknown error."""

        class CustomError(Exception):
            pass

        error = CustomError("Custom error")
        category = error_handler._classify_error(error)
        assert category == ErrorCategory.UNKNOWN


class TestSeverityDetermination:
    """Test error severity determination."""

    def test_critical_error_severity(self, error_handler):
        """Test severity of critical errors."""
        error = SystemExit("Exit")
        severity = error_handler._determine_severity(error, ErrorCategory.UNKNOWN)
        assert severity == 5

    def test_infrastructure_error_severity(self, error_handler):
        """Test severity of infrastructure errors."""
        error = OSError("IO error")
        severity = error_handler._determine_severity(error, ErrorCategory.INFRASTRUCTURE)
        assert severity == 4

    def test_state_error_severity(self, error_handler):
        """Test severity of state errors."""
        error = ValueError("State error")
        severity = error_handler._determine_severity(error, ErrorCategory.STATE)
        assert severity == 3

    def test_validation_error_severity(self, error_handler):
        """Test severity of validation errors."""
        error = ValueError("Validation error")
        severity = error_handler._determine_severity(error, ErrorCategory.VALIDATION)
        assert severity == 2


class TestRecoveryStrategy:
    """Test recovery strategy selection."""

    def test_state_error_recovery_strategy(self, error_handler):
        """Test recovery strategy for state errors."""
        error_ctx = ErrorContext(
            timestamp=datetime.now(),
            error_type="StateError",
            error_message="Invalid state",
            category=ErrorCategory.STATE,
            severity=3,
            operation="execute_step",
        )
        strategy = error_handler._select_recovery_strategy(error_ctx)
        assert strategy == RecoveryStrategy.MANUAL

    def test_validation_error_recovery_strategy(self, error_handler):
        """Test recovery strategy for validation errors."""
        error_ctx = ErrorContext(
            timestamp=datetime.now(),
            error_type="ValueError",
            error_message="Invalid value",
            category=ErrorCategory.VALIDATION,
            severity=2,
            operation="validate_data",
        )
        strategy = error_handler._select_recovery_strategy(error_ctx)
        assert strategy == RecoveryStrategy.SKIP

    def test_infrastructure_error_recovery_strategy(self, error_handler):
        """Test recovery strategy for infrastructure errors."""
        error_ctx = ErrorContext(
            timestamp=datetime.now(),
            error_type="IOError",
            error_message="File not found",
            category=ErrorCategory.INFRASTRUCTURE,
            severity=4,
            operation="load_file",
        )
        strategy = error_handler._select_recovery_strategy(error_ctx)
        assert strategy == RecoveryStrategy.RETRY

    def test_llm_error_recovery_strategy_retry(self, error_handler):
        """Test recovery strategy for LLM errors (retry)."""
        error_ctx = ErrorContext(
            timestamp=datetime.now(),
            error_type="APIError",
            error_message="API error",
            category=ErrorCategory.LLM,
            severity=2,
            operation="call_llm",
            recovery_attempts=0,
        )
        strategy = error_handler._select_recovery_strategy(error_ctx)
        assert strategy == RecoveryStrategy.RETRY

    def test_llm_error_recovery_strategy_fallback(self, error_handler):
        """Test recovery strategy for LLM errors (fallback after retries)."""
        error_ctx = ErrorContext(
            timestamp=datetime.now(),
            error_type="APIError",
            error_message="API error",
            category=ErrorCategory.LLM,
            severity=2,
            operation="call_llm",
            recovery_attempts=5,  # Exceeds MAX_RECOVERY_ATTEMPTS
        )
        strategy = error_handler._select_recovery_strategy(error_ctx)
        assert strategy == RecoveryStrategy.FALLBACK


class TestErrorHandling:
    """Test main error handling flow."""

    def test_handle_validation_error(self, error_handler):
        """Test handling of validation error."""
        error = ValueError("Invalid input")
        resolved, message = error_handler.handle_error(
            error, operation="validate_input", context_data={"input": "invalid"}
        )

        assert resolved is True  # Validation errors are skipped (resolved)
        assert message is None

    def test_handle_infrastructure_error(self, error_handler):
        """Test handling of infrastructure error."""
        error = FileNotFoundError("File not found")
        resolved, message = error_handler.handle_error(
            error, operation="load_config", context_data={"file": "config.yaml"}
        )

        assert resolved is True  # Infrastructure errors are retried (resolved)
        assert message is None

    def test_handle_state_error(self, error_handler):
        """Test handling of state error."""

        class SystemStateError(Exception):
            pass

        error = SystemStateError("Invalid state")

        resolved, message = error_handler.handle_error(
            error, operation="update_state", context_data={"state": "invalid"}
        )

        # SystemStateError is classified as UNKNOWN, which retries
        # This test validates error handling flow
        assert isinstance(message, str | type(None))

    def test_error_statistics(self, error_handler):
        """Test error statistics collection."""
        # Generate some errors
        error_handler.handle_error(ValueError("Error 1"), "op1")
        error_handler.handle_error(ValueError("Error 2"), "op2")

        stats = error_handler.get_error_statistics()

        assert stats["service_name"] == "TestService"
        assert stats["total_errors"] == 2
        assert stats["resolved_errors"] == 2  # Validation errors are resolved
        assert stats["recovery_rate"] == 100.0


class TestRecoveryCallbacks:
    """Test recovery callback registration and execution."""

    def test_register_recovery_callback(self, error_handler):
        """Test registering a recovery callback."""

        def recovery_callback(error_ctx):
            pass

        error_handler.register_recovery_callback(
            ErrorCategory.INFRASTRUCTURE, recovery_callback
        )

        assert ErrorCategory.INFRASTRUCTURE in error_handler._recovery_callbacks
        assert recovery_callback in error_handler._recovery_callbacks[
            ErrorCategory.INFRASTRUCTURE
        ]

    def test_execute_recovery_callback(self, error_handler):
        """Test executing a recovery callback."""

        def recovery_callback(error_ctx):
            error_ctx.context_data["recovered"] = True

        error_handler.register_recovery_callback(
            ErrorCategory.VALIDATION, recovery_callback
        )

        error_ctx = ErrorContext(
            timestamp=datetime.now(),
            error_type="ValueError",
            error_message="Error",
            category=ErrorCategory.VALIDATION,
            severity=2,
            operation="test",
        )

        error_handler._recovery_callbacks[ErrorCategory.VALIDATION] = [recovery_callback]
        resolved = error_handler._execute_recovery(
            error_ctx, RecoveryStrategy.FALLBACK
        )

        assert resolved is True
        assert error_ctx.context_data.get("recovered") is True


class TestErrorCleanup:
    """Test error history cleanup."""

    def test_cleanup_old_errors(self, error_handler):
        """Test cleaning up old error records."""
        # Add old error
        old_time = datetime.now() - timedelta(hours=2)
        old_error = ErrorContext(
            timestamp=old_time,
            error_type="ValueError",
            error_message="Old error",
            category=ErrorCategory.VALIDATION,
            severity=2,
            operation="test",
        )
        error_handler._error_history.append(old_error)

        # Add recent error
        error_handler._error_history.append(
            ErrorContext(
                timestamp=datetime.now(),
                error_type="ValueError",
                error_message="Recent error",
                category=ErrorCategory.VALIDATION,
                severity=2,
                operation="test",
            )
        )

        assert len(error_handler._error_history) == 2

        # Clean up errors older than 1 hour
        error_handler.cleanup_old_errors(max_age_seconds=3600)

        assert len(error_handler._error_history) == 1
        assert error_handler._error_history[0].error_message == "Recent error"


class TestErrorLogging:
    """Test error logging functionality."""

    def test_log_critical_error(self, error_handler):
        """Test logging of critical error."""
        error_ctx = ErrorContext(
            timestamp=datetime.now(),
            error_type="SystemError",
            error_message="Critical error",
            category=ErrorCategory.INFRASTRUCTURE,
            severity=4,
            operation="critical_op",
        )

        error_handler._log_error(error_ctx)

        # Verify error was logged
        assert error_handler._logger.error.called

    def test_log_warning_error(self, error_handler):
        """Test logging of warning-level error."""
        error_ctx = ErrorContext(
            timestamp=datetime.now(),
            error_type="ValueError",
            error_message="Warning error",
            category=ErrorCategory.STATE,
            severity=3,
            operation="state_op",
        )

        error_handler._log_error(error_ctx)

        # Verify warning was logged
        assert error_handler._logger.warning.called

    def test_log_info_error(self, error_handler):
        """Test logging of info-level error."""
        error_ctx = ErrorContext(
            timestamp=datetime.now(),
            error_type="ValueError",
            error_message="Info error",
            category=ErrorCategory.VALIDATION,
            severity=1,
            operation="info_op",
        )

        error_handler._log_error(error_ctx)

        # Verify info was logged
        assert error_handler._logger.info.called
