from unittest.mock import Mock

import pytest
from crypticorn_utils.errors import ErrorLevel, ErrorType
from crypticorn_utils.exceptions import (
    BaseError,
    ExceptionDetail,
    ExceptionHandler,
    ExceptionType,
)
from fastapi import FastAPI, HTTPException, Request, WebSocketException
from fastapi.exceptions import RequestValidationError, ResponseValidationError
from fastapi.responses import JSONResponse
from pydantic import ValidationError


# Test fixtures and mock classes
class MockApiError(BaseError):
    """Mock API error for testing"""

    UNKNOWN_ERROR = (
        "unknown_error",
        ErrorType.SERVER_ERROR,
        ErrorLevel.ERROR,
        500,
        1011,
    )
    TEST_ERROR = (
        "test_error",
        ErrorType.USER_ERROR,
        ErrorLevel.WARNING,
        400,
        1007,
    )
    NOT_FOUND = (
        "not_found",
        ErrorType.USER_ERROR,
        ErrorLevel.ERROR,
        404,
        1008,
    )


@pytest.fixture
def mock_error_callback():
    """Mock callback function for error retrieval"""

    def callback(identifier: str) -> MockApiError:
        return MockApiError.from_identifier(identifier)

    return callback


@pytest.fixture
def exception_handler(mock_error_callback):
    """Create an ExceptionHandler instance for testing"""
    return ExceptionHandler(callback=mock_error_callback)


@pytest.fixture
def websocket_exception_handler(mock_error_callback):
    """Create a WebSocket ExceptionHandler instance for testing"""
    return ExceptionHandler(callback=mock_error_callback, type=ExceptionType.WEBSOCKET)


@pytest.fixture
def mock_request():
    """Create a mock FastAPI request"""
    request = Mock(spec=Request)
    request.url = Mock()
    request.url.path = "/test"
    return request


class TestErrorEnums:
    """Test cases for error enums from errors.py"""

    def test_error_type_values(self):
        """Test ErrorType enum values"""
        assert ErrorType.USER_ERROR == "user error"
        assert ErrorType.EXCHANGE_ERROR == "exchange error"
        assert ErrorType.SERVER_ERROR == "server error"
        assert ErrorType.NO_ERROR == "no error"

    def test_error_level_values(self):
        """Test ErrorLevel enum values"""
        assert ErrorLevel.ERROR == "error"
        assert ErrorLevel.INFO == "info"
        assert ErrorLevel.SUCCESS == "success"
        assert ErrorLevel.WARNING == "warning"

    def test_error_type_enum_completeness(self):
        """Test that all expected ErrorType values are present"""
        expected_values = {"user error", "exchange error", "server error", "no error"}
        actual_values = {error_type.value for error_type in ErrorType}
        assert actual_values == expected_values

    def test_error_level_enum_completeness(self):
        """Test that all expected ErrorLevel values are present"""
        expected_values = {"error", "info", "success", "warning"}
        actual_values = {error_level.value for error_level in ErrorLevel}
        assert actual_values == expected_values


class TestExceptionType:
    """Test cases for ExceptionType enum"""

    def test_exception_type_values(self):
        """Test ExceptionType enum values"""
        assert ExceptionType.HTTP == "http"
        assert ExceptionType.WEBSOCKET == "websocket"


class TestExceptionDetail:
    """Test cases for ExceptionDetail model"""

    def test_exception_detail_creation(self):
        """Test creating an ExceptionDetail instance"""
        detail = ExceptionDetail(
            code="test_error",
            type=ErrorType.USER_ERROR,
            level=ErrorLevel.WARNING,
            status_code=400,
        )

        assert detail.code == "test_error"
        assert detail.type == ErrorType.USER_ERROR
        assert detail.level == ErrorLevel.WARNING
        assert detail.status_code == 400
        assert detail.message is None
        assert detail.details is None

    def test_exception_detail_with_optional_fields(self):
        """Test creating ExceptionDetail with all fields"""
        detail = ExceptionDetail(
            message="Test error message",
            code="test_error",
            type=ErrorType.USER_ERROR,
            level=ErrorLevel.ERROR,
            status_code=400,
            details={"extra": "info"},
        )

        assert detail.message == "Test error message"
        assert detail.code == "test_error"
        assert detail.type == ErrorType.USER_ERROR
        assert detail.level == ErrorLevel.ERROR
        assert detail.status_code == 400
        assert detail.details == {"extra": "info"}

    def test_exception_detail_model_dump(self):
        """Test ExceptionDetail model serialization"""
        detail = ExceptionDetail(
            message="Test message",
            code="test_code",
            type=ErrorType.SERVER_ERROR,
            level=ErrorLevel.ERROR,
            status_code=500,
        )

        dumped = detail.model_dump(mode="json")
        expected = {
            "message": "Test message",
            "code": "test_code",
            "type": "server error",
            "level": "error",
            "status_code": 500,
            "details": None,
        }

        assert dumped == expected

    def test_exception_detail_validation_error(self):
        """Test ExceptionDetail validation with invalid data"""
        with pytest.raises(ValidationError):
            ExceptionDetail(
                code="test_error",
                type="invalid_type",  # Invalid type
                level=ErrorLevel.ERROR,
                status_code=400,
            )


class TestBaseError:
    """Test cases for BaseError class"""

    def test_base_error_properties(self):
        """Test BaseError property access"""
        error = MockApiError.TEST_ERROR

        assert error.identifier == "test_error"
        assert error.type == ErrorType.USER_ERROR
        assert error.level == ErrorLevel.WARNING
        assert error.http_code == 400
        assert error.websocket_code == 1007

    def test_base_error_from_identifier(self):
        """Test BaseError.from_identifier method"""
        error = MockApiError.from_identifier("test_error")
        assert error == MockApiError.TEST_ERROR

        error = MockApiError.from_identifier("unknown_error")
        assert error == MockApiError.UNKNOWN_ERROR

    def test_base_error_from_identifier_not_found(self):
        """Test BaseError.from_identifier with non-existent identifier"""
        with pytest.raises(StopIteration):
            MockApiError.from_identifier("non_existent_error")

    def test_all_mock_api_errors(self):
        """Test all mock API error definitions"""
        # Test UNKNOWN_ERROR
        error = MockApiError.UNKNOWN_ERROR
        assert error.identifier == "unknown_error"
        assert error.type == ErrorType.SERVER_ERROR
        assert error.level == ErrorLevel.ERROR
        assert error.http_code == 500
        assert error.websocket_code == 1011

        # Test NOT_FOUND
        error = MockApiError.NOT_FOUND
        assert error.identifier == "not_found"
        assert error.type == ErrorType.USER_ERROR
        assert error.level == ErrorLevel.ERROR
        assert error.http_code == 404
        assert error.websocket_code == 1008


class TestExceptionHandler:
    """Test cases for ExceptionHandler class"""

    def test_exception_handler_initialization(self, mock_error_callback):
        """Test ExceptionHandler initialization"""
        handler = ExceptionHandler(callback=mock_error_callback)
        assert handler.callback == mock_error_callback
        assert handler.type == ExceptionType.HTTP

    def test_exception_handler_websocket_initialization(self, mock_error_callback):
        """Test ExceptionHandler initialization with WebSocket type"""
        handler = ExceptionHandler(
            callback=mock_error_callback, type=ExceptionType.WEBSOCKET
        )
        assert handler.callback == mock_error_callback
        assert handler.type == ExceptionType.WEBSOCKET

    def test_build_http_exception(self, exception_handler):
        """Test building HTTP exception"""
        exception = exception_handler.build_exception("test_error")

        assert isinstance(exception, HTTPException)
        assert exception.status_code == 400
        assert exception.detail["code"] == "test_error"
        assert exception.detail["type"] == "user error"
        assert exception.detail["level"] == "warning"
        assert exception.detail["status_code"] == 400

    def test_build_websocket_exception(self, websocket_exception_handler):
        """Test building WebSocket exception"""
        exception = websocket_exception_handler.build_exception("test_error")

        assert isinstance(exception, WebSocketException)
        assert exception.code == 400
        assert exception.reason["code"] == "test_error"
        assert exception.reason["type"] == "user error"
        assert exception.reason["level"] == "warning"

    def test_build_exception_with_message(self, exception_handler):
        """Test building exception with custom message"""
        exception = exception_handler.build_exception(
            "test_error", message="Custom message"
        )

        assert isinstance(exception, HTTPException)
        assert exception.detail["message"] == "Custom message"
        assert exception.detail["code"] == "test_error"

    def test_build_exception_with_headers(self, exception_handler):
        """Test building HTTP exception with headers"""
        headers = {"X-Custom-Header": "value"}
        exception = exception_handler.build_exception("test_error", headers=headers)

        assert isinstance(exception, HTTPException)
        assert exception.headers == headers

    def test_build_exception_with_details(self, exception_handler):
        """Test building exception with additional details"""
        details = {"field": "value", "extra": "info"}
        exception = exception_handler.build_exception("test_error", details=details)

        assert isinstance(exception, HTTPException)
        assert exception.detail["details"] == details

    def test_build_exception_all_parameters(self, exception_handler):
        """Test building exception with all parameters"""
        headers = {"X-Custom": "header"}
        details = {"extra": "details"}

        exception = exception_handler.build_exception(
            "test_error", message="Custom message", headers=headers, details=details
        )

        assert isinstance(exception, HTTPException)
        assert exception.detail["message"] == "Custom message"
        assert exception.detail["code"] == "test_error"
        assert exception.detail["details"] == details
        assert exception.headers == headers
        assert exception.status_code == 400


class TestExceptionHandlerMethods:
    """Test cases for ExceptionHandler exception handling methods"""

    @pytest.mark.asyncio
    async def test_general_handler(self, mock_request):
        """Test general exception handler"""
        test_exception = Exception("Test error")

        response = await ExceptionHandler._general_handler(mock_request, test_exception)

        assert isinstance(response, JSONResponse)
        assert response.status_code == 500

        # Check response content
        content = response.body.decode()
        assert "Test error" in content
        assert "unknown_error" in content
        assert "server error" in content

    @pytest.mark.asyncio
    async def test_request_validation_handler(self, mock_request):
        """Test request validation error handler"""
        # Create a mock RequestValidationError
        validation_error = RequestValidationError(errors=[])

        response = await ExceptionHandler._request_validation_handler(
            mock_request, validation_error
        )

        assert isinstance(response, JSONResponse)
        assert response.status_code == 400

        # Check response content
        content = response.body.decode()
        assert "invalid_data_request" in content
        assert "user error" in content

    @pytest.mark.asyncio
    async def test_response_validation_handler(self, mock_request):
        """Test response validation error handler"""
        # Create a mock ResponseValidationError
        validation_error = ResponseValidationError(errors=[])

        response = await ExceptionHandler._response_validation_handler(
            mock_request, validation_error
        )

        assert isinstance(response, JSONResponse)
        assert response.status_code == 400

        # Check response content
        content = response.body.decode()
        assert "invalid_data_response" in content
        assert "user error" in content

    @pytest.mark.asyncio
    async def test_http_handler(self, mock_request):
        """Test HTTP exception handler"""
        http_exception = HTTPException(
            status_code=404,
            detail={"code": "not_found", "message": "Resource not found"},
            headers={"X-Custom": "header"},
        )

        response = await ExceptionHandler._http_handler(mock_request, http_exception)

        assert isinstance(response, JSONResponse)
        assert response.status_code == 404
        assert response.headers["X-Custom"] == "header"

        # Check response content matches original detail
        content = response.body.decode()
        assert "not_found" in content
        assert "Resource not found" in content

    def test_register_exception_handlers(self, exception_handler):
        """Test registering exception handlers with FastAPI app"""
        app = FastAPI()

        # Mock the add_exception_handler method
        app.add_exception_handler = Mock()

        exception_handler.register_exception_handlers(app)

        # Verify all exception handlers were registered
        assert app.add_exception_handler.call_count == 4

        # Check that the right exception types were registered
        calls = app.add_exception_handler.call_args_list
        exception_types = [call[0][0] for call in calls]

        assert Exception in exception_types
        assert HTTPException in exception_types
        assert RequestValidationError in exception_types
        assert ResponseValidationError in exception_types


class TestIntegrationScenarios:
    """Integration test scenarios"""

    def test_complete_error_flow(self, exception_handler):
        """Test complete error handling flow"""
        # Build exception
        exception = exception_handler.build_exception(
            "not_found", message="User not found", details={"user_id": 123}
        )

        # Verify exception properties
        assert isinstance(exception, HTTPException)
        assert exception.status_code == 404
        assert exception.detail["code"] == "not_found"
        assert exception.detail["message"] == "User not found"
        assert exception.detail["type"] == "user error"
        assert exception.detail["level"] == "error"
        assert exception.detail["details"]["user_id"] == 123

    def test_websocket_error_flow(self, websocket_exception_handler):
        """Test WebSocket error handling flow"""
        exception = websocket_exception_handler.build_exception(
            "unknown_error", message="Internal server error"
        )

        # Verify WebSocket exception properties
        assert isinstance(exception, WebSocketException)
        assert exception.code == 500
        assert exception.reason["code"] == "unknown_error"
        assert exception.reason["message"] == "Internal server error"
        assert exception.reason["type"] == "server error"
        assert exception.reason["level"] == "error"

    def test_error_callback_integration(self):
        """Test integration between ExceptionHandler and BaseError"""

        def callback(identifier: str) -> MockApiError:
            return MockApiError.from_identifier(identifier)

        handler = ExceptionHandler(callback=callback)

        # Test with each mock error
        for mock_error in MockApiError:
            exception = handler.build_exception(mock_error.identifier)

            assert isinstance(exception, HTTPException)
            assert exception.status_code == mock_error.http_code
            assert exception.detail["code"] == mock_error.identifier
            assert exception.detail["type"] == mock_error.type.value
            assert exception.detail["level"] == mock_error.level.value
