"""
Tests for error and retry handlers.
"""

import unittest
import logging
from pygeai_orchestration.core.handlers import ErrorHandler, RetryHandler
from pygeai_orchestration.core.exceptions import (
    OrchestrationError,
    PatternExecutionError,
)


class TestErrorHandlerDetection(unittest.TestCase):
    """Test error detection functionality."""

    def test_has_errors_with_error_key(self):
        """Test detection of error key in dict."""
        response = {"error": "Something went wrong"}
        self.assertTrue(ErrorHandler.has_errors(response))

    def test_has_errors_with_errors_key(self):
        """Test detection of errors key in dict."""
        response = {"errors": ["Error 1", "Error 2"]}
        self.assertTrue(ErrorHandler.has_errors(response))

    def test_has_errors_with_success_false(self):
        """Test detection of success=False."""
        response = {"success": False, "message": "Failed"}
        self.assertTrue(ErrorHandler.has_errors(response))

    def test_has_errors_with_error_status(self):
        """Test detection of error status."""
        self.assertTrue(ErrorHandler.has_errors({"status": "error"}))
        self.assertTrue(ErrorHandler.has_errors({"status": "failed"}))
        self.assertTrue(ErrorHandler.has_errors({"status": 500}))

    def test_has_errors_with_error_code(self):
        """Test detection of error codes."""
        self.assertTrue(ErrorHandler.has_errors({"code": 400}))
        self.assertTrue(ErrorHandler.has_errors({"code": 500}))
        self.assertTrue(ErrorHandler.has_errors({"code": 404}))

    def test_has_errors_with_exception(self):
        """Test detection of Exception objects."""
        error = Exception("Test error")
        self.assertTrue(ErrorHandler.has_errors(error))

    def test_has_no_errors_with_success_true(self):
        """Test no error detection with success response."""
        response = {"success": True, "data": "result"}
        self.assertFalse(ErrorHandler.has_errors(response))

    def test_has_no_errors_with_good_status(self):
        """Test no error detection with 2xx status."""
        response = {"status": 200}
        self.assertFalse(ErrorHandler.has_errors(response))

    def test_has_no_errors_with_none(self):
        """Test no error detection with None."""
        self.assertFalse(ErrorHandler.has_errors(None))

    def test_has_no_errors_with_empty_dict(self):
        """Test no error detection with empty dict."""
        self.assertFalse(ErrorHandler.has_errors({}))


class TestErrorHandlerExtraction(unittest.TestCase):
    """Test error extraction functionality."""

    def test_extract_error_from_dict_with_error_key(self):
        """Test extracting error from dict with error key."""
        response = {"error": "Connection failed"}
        error = ErrorHandler.extract_error(response)
        self.assertEqual(error, "Connection failed")

    def test_extract_error_from_dict_with_nested_error(self):
        """Test extracting from nested error object."""
        response = {"error": {"message": "Invalid input", "code": 400}}
        error = ErrorHandler.extract_error(response)
        self.assertIn("Invalid input", error)

    def test_extract_error_from_errors_list(self):
        """Test extracting from errors list."""
        response = {"errors": ["Error 1", "Error 2"]}
        error = ErrorHandler.extract_error(response)
        self.assertIn("Error 1", error)

    def test_extract_error_from_message_key(self):
        """Test extracting from message key."""
        response = {"message": "Operation failed"}
        error = ErrorHandler.extract_error(response)
        self.assertEqual(error, "Operation failed")

    def test_extract_error_from_status_code(self):
        """Test extracting from status/code."""
        response = {"status": 500, "code": "SERVER_ERROR"}
        error = ErrorHandler.extract_error(response)
        self.assertIn("500", error)
        self.assertIn("SERVER_ERROR", error)

    def test_extract_error_from_exception(self):
        """Test extracting from Exception."""
        exc = ValueError("Invalid value")
        error = ErrorHandler.extract_error(exc)
        self.assertEqual(error, "Invalid value")

    def test_extract_error_from_string(self):
        """Test extracting from string."""
        error = ErrorHandler.extract_error("Simple error message")
        self.assertEqual(error, "Simple error message")

    def test_extract_error_from_none(self):
        """Test extracting from None."""
        error = ErrorHandler.extract_error(None)
        self.assertIn("None", error)


class TestErrorHandlerHandling(unittest.TestCase):
    """Test error handling functionality."""

    def test_handle_error_basic(self):
        """Test basic error handling."""
        error = {"error": "Test error"}
        message = ErrorHandler.handle_error(error)
        self.assertEqual(message, "Test error")

    def test_handle_error_with_context(self):
        """Test error handling with context."""
        error = {"error": "Operation failed"}
        message = ErrorHandler.handle_error(
            error,
            context={"operation": "generate", "attempt": 1}
        )
        self.assertIn("Operation failed", message)
        self.assertIn("operation=generate", message)
        self.assertIn("attempt=1", message)

    def test_handle_error_with_log_level(self):
        """Test error handling with different log levels."""
        error = {"error": "Warning message"}
        # Should not raise, just return message
        message = ErrorHandler.handle_error(error, log_level=logging.WARNING)
        self.assertEqual(message, "Warning message")


class TestErrorHandlerWrap(unittest.TestCase):
    """Test exception wrapping functionality."""

    def test_wrap_exception_basic(self):
        """Test basic exception wrapping."""
        original = ValueError("Original error")
        wrapped = ErrorHandler.wrap_exception(original)

        self.assertIsInstance(wrapped, OrchestrationError)
        self.assertIn("Original error", str(wrapped))
        self.assertEqual(wrapped.__cause__, original)

    def test_wrap_exception_with_custom_class(self):
        """Test wrapping with custom exception class."""
        original = ValueError("Original error")
        wrapped = ErrorHandler.wrap_exception(
            original,
            exception_class=PatternExecutionError,
            pattern_name="reflection"
        )

        self.assertIsInstance(wrapped, PatternExecutionError)
        self.assertEqual(wrapped.pattern_name, "reflection")

    def test_wrap_exception_with_prefix(self):
        """Test wrapping with message prefix."""
        original = ValueError("Original error")
        wrapped = ErrorHandler.wrap_exception(
            original,
            message_prefix="Pattern failed"
        )

        self.assertIn("Pattern failed", str(wrapped))
        self.assertIn("Original error", str(wrapped))


class TestErrorHandlerRecoverable(unittest.TestCase):
    """Test recoverable error detection."""

    def test_is_recoverable_timeout(self):
        """Test timeout errors are recoverable."""
        self.assertTrue(ErrorHandler.is_recoverable({"code": 504}))
        self.assertTrue(ErrorHandler.is_recoverable({"error": "Request timeout"}))

    def test_is_recoverable_rate_limit(self):
        """Test rate limit errors are recoverable."""
        self.assertTrue(ErrorHandler.is_recoverable({"code": 429}))
        self.assertTrue(ErrorHandler.is_recoverable({"error": "Rate limit exceeded"}))

    def test_is_recoverable_service_unavailable(self):
        """Test service unavailable is recoverable."""
        self.assertTrue(ErrorHandler.is_recoverable({"code": 503}))
        self.assertTrue(ErrorHandler.is_recoverable({"error": "Service unavailable"}))

    def test_is_not_recoverable_bad_request(self):
        """Test bad request is not recoverable."""
        self.assertFalse(ErrorHandler.is_recoverable({"code": 400}))

    def test_is_not_recoverable_not_found(self):
        """Test not found is not recoverable."""
        self.assertFalse(ErrorHandler.is_recoverable({"code": 404}))

    def test_is_recoverable_exception(self):
        """Test exception recoverability."""
        timeout_exc = TimeoutError("Connection timeout")
        self.assertTrue(ErrorHandler.is_recoverable(timeout_exc))

        value_exc = ValueError("Invalid value")
        self.assertFalse(ErrorHandler.is_recoverable(value_exc))


class TestErrorHandlerFormat(unittest.TestCase):
    """Test error response formatting."""

    def test_format_error_response_basic(self):
        """Test basic error formatting."""
        error = ValueError("Test error")
        response = ErrorHandler.format_error_response(error)

        self.assertFalse(response['success'])
        self.assertEqual(response['error'], "Test error")
        self.assertEqual(response['error_type'], "ValueError")

    def test_format_error_response_with_details(self):
        """Test formatting with details."""
        error = {"error": "Failed", "code": 500, "details": {"reason": "timeout"}}
        response = ErrorHandler.format_error_response(error)

        self.assertFalse(response['success'])
        self.assertEqual(response['error'], "Failed")
        self.assertEqual(response['code'], 500)
        self.assertEqual(response['details'], {"reason": "timeout"})

    def test_format_error_response_without_details(self):
        """Test formatting without details."""
        error = ValueError("Test error")
        response = ErrorHandler.format_error_response(error, include_details=False)

        self.assertFalse(response['success'])
        self.assertEqual(response['error'], "Test error")
        self.assertNotIn('error_type', response)

    def test_format_error_response_with_cause(self):
        """Test formatting with exception cause."""
        original = ValueError("Original")
        wrapped = ErrorHandler.wrap_exception(original, message_prefix="Wrapped")
        response = ErrorHandler.format_error_response(wrapped)

        self.assertEqual(response['original_error'], "Original")


class TestRetryHandler(unittest.TestCase):
    """Test retry handler functionality."""

    def test_retry_handler_init(self):
        """Test retry handler initialization."""
        handler = RetryHandler(max_retries=5, delay=2.0)
        self.assertEqual(handler.max_retries, 5)
        self.assertEqual(handler.delay, 2.0)

    def test_should_retry_within_limit(self):
        """Test should retry when within limit."""
        handler = RetryHandler(max_retries=3, recoverable_only=False)
        self.assertTrue(handler.should_retry({"error": "test"}, attempt=0))
        self.assertTrue(handler.should_retry({"error": "test"}, attempt=2))

    def test_should_not_retry_at_limit(self):
        """Test should not retry when at limit."""
        handler = RetryHandler(max_retries=3, recoverable_only=False)
        self.assertFalse(handler.should_retry({"error": "test"}, attempt=3))

    def test_should_retry_recoverable_only(self):
        """Test retry only recoverable errors."""
        handler = RetryHandler(max_retries=3, recoverable_only=True)

        # Recoverable error
        self.assertTrue(handler.should_retry({"code": 504}, attempt=0))

        # Non-recoverable error
        self.assertFalse(handler.should_retry({"code": 400}, attempt=0))

    def test_get_delay_exponential_backoff(self):
        """Test exponential backoff delay calculation."""
        handler = RetryHandler(delay=1.0, backoff_factor=2.0)

        self.assertEqual(handler.get_delay(0), 1.0)
        self.assertEqual(handler.get_delay(1), 2.0)
        self.assertEqual(handler.get_delay(2), 4.0)
        self.assertEqual(handler.get_delay(3), 8.0)

    def test_get_delay_custom_backoff(self):
        """Test custom backoff factor."""
        handler = RetryHandler(delay=1.0, backoff_factor=3.0)

        self.assertEqual(handler.get_delay(0), 1.0)
        self.assertEqual(handler.get_delay(1), 3.0)
        self.assertEqual(handler.get_delay(2), 9.0)


if __name__ == "__main__":
    unittest.main()
