"""
Comprehensive tests for error handling system.

Tests cover:
- Custom exception creation and attributes
- Error decorator for sync and async functions
- Error message formatting
- Retry_possible flags
- Integration with WebSocket sending
- Logging behavior
"""

import pytest
import asyncio
import logging
from unittest.mock import Mock, AsyncMock, patch, call
from datetime import datetime
import sys
import os

# Add parent directory to path for imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))

from errors.error_handler import (
    GameError,
    InsufficientResourcesError,
    ServerError,
    handle_errors,
    send_error_to_client,
    example_purchase_energy,
    example_validate_action
)


class TestCustomExceptions:
    """Test custom exception classes."""

    def test_game_error_basic(self):
        """Test basic GameError creation."""
        error = GameError("Something went wrong", "TEST_ERROR", retry_possible=True)

        assert error.message == "Something went wrong"
        assert error.error_code == "TEST_ERROR"
        assert error.retry_possible is True
        assert str(error) == "Something went wrong"

    def test_game_error_defaults(self):
        """Test GameError with default values."""
        error = GameError("Error message")

        assert error.message == "Error message"
        assert error.error_code == "GAME_ERROR"
        assert error.retry_possible is False

    def test_insufficient_resources_error(self):
        """Test InsufficientResourcesError with resource details."""
        error = InsufficientResourcesError('energy', 100, 50)

        assert error.resource_type == 'energy'
        assert error.required == 100
        assert error.current == 50
        assert error.error_code == 'INSUFFICIENT_RESOURCES'
        assert error.retry_possible is True
        assert 'energy' in error.message
        assert '100' in error.message
        assert '50' in error.message

    def test_insufficient_resources_money(self):
        """Test InsufficientResourcesError for money."""
        error = InsufficientResourcesError('money', 500.50, 100.25)

        assert error.resource_type == 'money'
        assert error.required == 500.50
        assert error.current == 100.25
        assert 'money' in error.message.lower()

    def test_server_error_basic(self):
        """Test ServerError creation."""
        error = ServerError("Database connection failed")

        assert error.message == "Database connection failed"
        assert error.error_code == 'SERVER_ERROR'
        assert error.retry_possible is True
        assert error.original_exception is None

    def test_server_error_with_original_exception(self):
        """Test ServerError wrapping another exception."""
        original = ValueError("Invalid value")
        error = ServerError("Wrapped error", original_exception=original)

        assert error.original_exception is original
        assert isinstance(error.original_exception, ValueError)


class TestErrorDecorator:
    """Test handle_errors decorator."""

    @pytest.mark.asyncio
    async def test_async_function_success(self):
        """Test decorator on successful async function."""
        send_mock = AsyncMock()

        @handle_errors(send_to_client=send_mock)
        async def test_func(player_id: int, value: str):
            return {'result': value}

        result = await test_func(player_id=123, value='test')

        assert result == {'result': 'test'}
        send_mock.assert_not_called()

    @pytest.mark.asyncio
    async def test_async_function_game_error(self):
        """Test decorator catching GameError in async function."""
        send_mock = AsyncMock()

        @handle_errors(send_to_client=send_mock)
        async def test_func(player_id: int):
            raise GameError("Test error", "TEST_CODE", retry_possible=True)

        with pytest.raises(GameError) as exc_info:
            await test_func(player_id=123)

        assert exc_info.value.error_code == "TEST_CODE"
        # Should have called send_error_to_client
        assert send_mock.call_count == 1

    @pytest.mark.asyncio
    async def test_async_function_unexpected_error(self):
        """Test decorator catching unexpected errors in async function."""
        send_mock = AsyncMock()

        @handle_errors(send_to_client=send_mock)
        async def test_func(player_id: int):
            raise ValueError("Unexpected error")

        with pytest.raises(ServerError) as exc_info:
            await test_func(player_id=123)

        assert exc_info.value.error_code == "SERVER_ERROR"
        assert isinstance(exc_info.value.original_exception, ValueError)
        # Should send error to client
        assert send_mock.call_count == 1

    def test_sync_function_success(self):
        """Test decorator on successful sync function."""

        @handle_errors()
        def test_func(player_id: int, value: str):
            return {'result': value}

        result = test_func(player_id=123, value='test')

        assert result == {'result': 'test'}

    def test_sync_function_game_error(self):
        """Test decorator catching GameError in sync function."""

        @handle_errors()
        def test_func(player_id: int):
            raise InsufficientResourcesError('energy', 100, 50)

        with pytest.raises(InsufficientResourcesError) as exc_info:
            test_func(player_id=123)

        assert exc_info.value.error_code == "INSUFFICIENT_RESOURCES"
        assert exc_info.value.resource_type == 'energy'

    def test_sync_function_unexpected_error(self):
        """Test decorator catching unexpected errors in sync function."""

        @handle_errors()
        def test_func(player_id: int):
            raise KeyError("Missing key")

        with pytest.raises(ServerError) as exc_info:
            test_func(player_id=123)

        assert exc_info.value.error_code == "SERVER_ERROR"
        assert isinstance(exc_info.value.original_exception, KeyError)

    @pytest.mark.asyncio
    async def test_decorator_preserves_function_signature(self):
        """Test that decorator preserves function name and docstring."""
        send_mock = AsyncMock()

        @handle_errors(send_to_client=send_mock)
        async def test_func(player_id: int, name: str) -> dict:
            """Test function docstring."""
            return {'name': name}

        assert test_func.__name__ == 'test_func'
        assert test_func.__doc__ == 'Test function docstring.'

    @pytest.mark.asyncio
    async def test_decorator_with_different_log_levels(self):
        """Test decorator with different logging levels."""

        @handle_errors(log_level='WARNING')
        async def test_func(player_id: int):
            raise GameError("Warning level error", "WARN_ERROR")

        with pytest.raises(GameError):
            await test_func(player_id=123)

    @pytest.mark.asyncio
    async def test_decorator_extracts_player_id_from_args(self):
        """Test that decorator can extract player_id from args."""
        send_mock = AsyncMock()

        @handle_errors(send_to_client=send_mock)
        async def test_func(player_id: int, other: str):
            raise GameError("Test error")

        with pytest.raises(GameError):
            await test_func(123, "other_value")

        # Should still send error even though player_id is positional
        assert send_mock.call_count == 1

    @pytest.mark.asyncio
    async def test_decorator_without_send_to_client(self):
        """Test decorator without client notification function."""

        @handle_errors()  # No send_to_client provided
        async def test_func(player_id: int):
            raise GameError("Test error", "TEST_CODE")

        with pytest.raises(GameError) as exc_info:
            await test_func(player_id=123)

        assert exc_info.value.error_code == "TEST_CODE"


class TestSendErrorToClient:
    """Test send_error_to_client function."""

    @pytest.mark.asyncio
    async def test_send_error_basic(self):
        """Test sending basic error to client."""
        send_func = AsyncMock()

        await send_error_to_client(
            player_id=123,
            message="Test error message",
            error_code="TEST_ERROR",
            retry_possible=True,
            send_func=send_func
        )

        send_func.assert_called_once()
        call_args = send_func.call_args[0]

        assert call_args[0] == 123
        error_payload = call_args[1]
        assert error_payload['type'] == 'error'
        assert error_payload['error_code'] == 'TEST_ERROR'
        assert error_payload['message'] == "Test error message"
        assert error_payload['retry_possible'] is True
        assert 'timestamp' in error_payload

    @pytest.mark.asyncio
    async def test_send_error_with_failed_send(self):
        """Test error handling when sending to client fails."""
        send_func = AsyncMock(side_effect=Exception("WebSocket closed"))

        # Should not raise, just log the error
        await send_error_to_client(
            player_id=123,
            message="Test error",
            error_code="TEST",
            retry_possible=False,
            send_func=send_func
        )

        send_func.assert_called_once()

    @pytest.mark.asyncio
    async def test_send_error_includes_timestamp(self):
        """Test that error payload includes ISO format timestamp."""
        send_func = AsyncMock()

        await send_error_to_client(
            player_id=123,
            message="Test",
            error_code="TEST",
            retry_possible=False,
            send_func=send_func
        )

        error_payload = send_func.call_args[0][1]
        timestamp = error_payload['timestamp']

        # Should be valid ISO format
        datetime.fromisoformat(timestamp)


class TestExampleFunctions:
    """Test example usage functions."""

    @pytest.mark.asyncio
    async def test_example_purchase_energy_success(self):
        """Test example purchase function with sufficient funds."""
        send_func = AsyncMock()

        # This will fail because player has 50 but needs 100
        # Let's test the structure works
        with pytest.raises(InsufficientResourcesError) as exc_info:
            result = await example_purchase_energy(123, amount=10, send_to_client=send_func)

        assert exc_info.value.resource_type == 'money'
        assert exc_info.value.required == 100
        assert exc_info.value.current == 50

    @pytest.mark.asyncio
    async def test_example_purchase_energy_insufficient_funds(self):
        """Test example purchase function with insufficient funds."""
        send_func = AsyncMock()

        with pytest.raises(InsufficientResourcesError) as exc_info:
            await example_purchase_energy(123, amount=10, send_to_client=send_func)

        error = exc_info.value
        assert error.resource_type == 'money'
        assert error.required == 100
        assert error.current == 50
        assert error.retry_possible is True

        # Should have notified client
        assert send_func.call_count == 1

    def test_example_validate_action_valid(self):
        """Test example validation with valid action."""
        result = example_validate_action(123, 'eat')
        assert result is True

        result = example_validate_action(123, 'sleep')
        assert result is True

    def test_example_validate_action_invalid(self):
        """Test example validation with invalid action."""
        with pytest.raises(GameError) as exc_info:
            example_validate_action(123, 'invalid_action')

        error = exc_info.value
        assert error.error_code == 'INVALID_ACTION'
        assert 'invalid_action' in error.message
        assert error.retry_possible is False


class TestLogging:
    """Test logging behavior of error handler."""

    @pytest.mark.asyncio
    async def test_game_error_logging(self, caplog):
        """Test that GameError is logged properly."""
        with caplog.at_level(logging.ERROR):
            @handle_errors()
            async def test_func(player_id: int):
                raise GameError("Test game error", "TEST_CODE")

            with pytest.raises(GameError):
                await test_func(player_id=123)

        # Check log contains error details
        assert "GameError" in caplog.text
        assert "Test game error" in caplog.text

    @pytest.mark.asyncio
    async def test_unexpected_error_logging(self, caplog):
        """Test that unexpected errors are logged with traceback."""
        with caplog.at_level(logging.ERROR):
            @handle_errors()
            async def test_func(player_id: int):
                raise ValueError("Unexpected")

            with pytest.raises(ServerError):
                await test_func(player_id=123)

        # Check log contains error and function name
        assert "Unexpected error" in caplog.text
        assert "test_func" in caplog.text

    @pytest.mark.asyncio
    async def test_warning_level_logging(self, caplog):
        """Test logging at WARNING level."""
        with caplog.at_level(logging.WARNING):
            @handle_errors(log_level='WARNING')
            async def test_func(player_id: int):
                raise GameError("Warning message", "WARN_CODE")

            with pytest.raises(GameError):
                await test_func(player_id=123)

        assert "GameError" in caplog.text
        assert "Warning message" in caplog.text


class TestIntegrationScenarios:
    """Test real-world integration scenarios."""

    @pytest.mark.asyncio
    async def test_multiple_errors_in_sequence(self):
        """Test handling multiple errors in sequence."""
        send_func = AsyncMock()
        call_count = 0

        @handle_errors(send_to_client=send_func)
        async def test_func(player_id: int, should_fail: bool):
            nonlocal call_count
            call_count += 1

            if should_fail:
                raise GameError(f"Error {call_count}", f"ERROR_{call_count}")

            return {"success": True}

        # Successful call
        result = await test_func(123, False)
        assert result["success"] is True
        assert send_func.call_count == 0

        # Failed call
        with pytest.raises(GameError):
            await test_func(123, True)
        assert send_func.call_count == 1

        # Another failed call
        with pytest.raises(GameError):
            await test_func(123, True)
        assert send_func.call_count == 2

    @pytest.mark.asyncio
    async def test_nested_error_handling(self):
        """Test error handling with nested function calls."""
        send_func = AsyncMock()

        @handle_errors(send_to_client=send_func)
        async def outer_func(player_id: int):
            return await inner_func(player_id)

        @handle_errors(send_to_client=send_func)
        async def inner_func(player_id: int):
            raise InsufficientResourcesError('energy', 100, 50)

        with pytest.raises(InsufficientResourcesError):
            await outer_func(123)

        # Both decorators should attempt to send
        assert send_func.call_count >= 1

    @pytest.mark.asyncio
    async def test_error_with_complex_payload(self):
        """Test error handling with complex error payloads."""
        send_func = AsyncMock()

        await send_error_to_client(
            player_id=123,
            message="Complex error with special characters: éàü & <> \"quotes\"",
            error_code="COMPLEX_ERROR",
            retry_possible=True,
            send_func=send_func
        )

        send_func.assert_called_once()
        error_payload = send_func.call_args[0][1]
        assert "éàü" in error_payload['message']
        assert "quotes" in error_payload['message']


class TestRetryBehavior:
    """Test retry_possible flag behavior."""

    def test_insufficient_resources_is_retryable(self):
        """Test that insufficient resources errors are marked as retryable."""
        error = InsufficientResourcesError('energy', 100, 50)
        assert error.retry_possible is True

    def test_server_error_is_retryable(self):
        """Test that server errors are marked as retryable."""
        error = ServerError("Database error")
        assert error.retry_possible is True

    def test_game_error_retry_configurable(self):
        """Test that GameError retry flag is configurable."""
        retryable = GameError("Try again", "TEST", retry_possible=True)
        assert retryable.retry_possible is True

        non_retryable = GameError("Don't retry", "TEST", retry_possible=False)
        assert non_retryable.retry_possible is False

    @pytest.mark.asyncio
    async def test_retry_flag_sent_to_client(self):
        """Test that retry_possible flag is sent to client."""
        send_func = AsyncMock()

        @handle_errors(send_to_client=send_func)
        async def test_func(player_id: int, retryable: bool):
            raise GameError("Error", "TEST", retry_possible=retryable)

        # Test with retryable error
        with pytest.raises(GameError):
            await test_func(123, retryable=True)

        error_payload = send_func.call_args[0][1]
        assert error_payload['retry_possible'] is True

        # Reset mock
        send_func.reset_mock()

        # Test with non-retryable error
        with pytest.raises(GameError):
            await test_func(123, retryable=False)

        error_payload = send_func.call_args[0][1]
        assert error_payload['retry_possible'] is False
