# File: tests/unit/infrastructure/json/mcp/test_tool_executor.py
# Purpose: Unit tests for ToolExecutor class
# Context: Validates tool execution, error handling, and lifecycle management

"""Comprehensive unit tests for ToolExecutor execution engine.

Tests cover:
- Synchronous tool execution
- Asynchronous tool execution
- Error handling (TypeError, TimeoutError, Exception)
- Argument validation
- Retry logic with exponential backoff
- Result formatting and status codes
- Execution statistics and monitoring
"""

import asyncio
from unittest.mock import AsyncMock, Mock

import pytest

from noveler.infrastructure.json.mcp.servers.tool_executor import (
    ExecutionContext,
    ExecutionStatus,
    ToolExecutor,
    ToolResult,
)
from noveler.infrastructure.logging.unified_logger import get_logger


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


@pytest.fixture
def tool_executor(mock_logger):
    """Create a ToolExecutor instance for testing."""
    return ToolExecutor(logger=mock_logger)


@pytest.fixture
def execution_context():
    """Create an ExecutionContext for testing."""
    return ExecutionContext(
        tool_name="test_tool",
        timeout_seconds=5,
        retry_count=2,
    )


class TestToolExecutorInitialization:
    """Test ToolExecutor initialization and setup."""

    def test_tool_executor_initializes_with_logger(self, mock_logger):
        """Test that ToolExecutor initializes with a logger."""
        executor = ToolExecutor(logger=mock_logger)
        assert executor._logger is mock_logger

    def test_tool_executor_initializes_statistics(self, tool_executor):
        """Test that statistics are initialized to zero."""
        assert tool_executor._execution_count == 0
        assert tool_executor._failure_count == 0
        assert tool_executor._total_duration_ms == 0.0


class TestExecutionContext:
    """Test ExecutionContext dataclass."""

    def test_execution_context_creation(self):
        """Test creation of ExecutionContext."""
        ctx = ExecutionContext(
            tool_name="my_tool",
            timeout_seconds=10,
            retry_count=3,
        )
        assert ctx.tool_name == "my_tool"
        assert ctx.timeout_seconds == 10
        assert ctx.retry_count == 3

    def test_execution_context_defaults(self):
        """Test ExecutionContext with default values."""
        ctx = ExecutionContext(tool_name="my_tool")
        assert ctx.project_root is None
        assert ctx.timeout_seconds == 300
        assert ctx.async_mode is False
        assert ctx.retry_count == 0


class TestToolResult:
    """Test ToolResult dataclass."""

    def test_tool_result_creation_success(self):
        """Test creation of successful ToolResult."""
        result = ToolResult(
            status=ExecutionStatus.SUCCESS,
            output={"data": "test"},
            duration_ms=100.5,
        )
        assert result.status == ExecutionStatus.SUCCESS
        assert result.output == {"data": "test"}
        assert result.error is None

    def test_tool_result_to_dict(self):
        """Test ToolResult conversion to dictionary."""
        result = ToolResult(
            status=ExecutionStatus.SUCCESS,
            output={"data": "test"},
            duration_ms=100.5,
            retry_count=1,
        )
        result_dict = result.to_dict()

        assert result_dict["status"] == "success"
        assert result_dict["output"] == {"data": "test"}
        assert result_dict["error"] is None
        assert result_dict["duration_ms"] == 100.5


class TestSynchronousExecution:
    """Test synchronous tool execution."""

    def test_execute_successful_tool(self, tool_executor, execution_context):
        """Test successful tool execution."""
        handler = Mock(return_value={"result": "success"})

        result = tool_executor.execute(
            handler=handler,
            arguments={"param": "value"},
            context=execution_context,
        )

        assert result.status == ExecutionStatus.SUCCESS
        assert result.output == {"result": "success"}
        assert result.error is None
        handler.assert_called_once_with(param="value")

    def test_execute_updates_statistics(self, tool_executor, execution_context):
        """Test that execution updates statistics."""
        handler = Mock(return_value={"result": "success"})

        tool_executor.execute(
            handler=handler,
            arguments={"param": "value"},
            context=execution_context,
        )

        assert tool_executor._execution_count == 1
        assert tool_executor._failure_count == 0

    def test_execute_tool_with_exception(self, tool_executor, mock_logger):
        """Test tool execution that raises an exception."""
        executor = ToolExecutor(logger=mock_logger)
        handler = Mock(side_effect=ValueError("Test error"))

        context = ExecutionContext(
            tool_name="test_tool",
            retry_count=0,  # No retries for this test
        )

        result = executor.execute(
            handler=handler,
            arguments={},
            context=context,
        )

        assert result.status == ExecutionStatus.FAILURE
        assert "Test error" in result.error
        assert executor._failure_count == 1

    def test_execute_with_invalid_arguments_type(self, tool_executor, execution_context):
        """Test execution with invalid argument type."""
        handler = Mock()

        result = tool_executor.execute(
            handler=handler,
            arguments="invalid",  # Should be dict
            context=execution_context,
        )

        assert result.status == ExecutionStatus.ERROR
        assert "Invalid arguments type" in result.error

    def test_execute_with_argument_mismatch(self, tool_executor, execution_context):
        """Test execution with mismatched arguments."""
        handler = Mock(side_effect=TypeError("missing required argument"))

        result = tool_executor.execute(
            handler=handler,
            arguments={"wrong_param": "value"},
            context=execution_context,
        )

        assert result.status == ExecutionStatus.ERROR
        assert "Argument mismatch" in result.error


class TestRetryLogic:
    """Test retry logic and exponential backoff."""

    def test_execute_with_retry_succeeds_on_second_attempt(
        self, tool_executor, mock_logger
    ):
        """Test that retry succeeds on second attempt."""
        executor = ToolExecutor(logger=mock_logger)
        call_count = 0

        def handler_with_retry():
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                raise ValueError("First attempt fails")
            return {"result": "success"}

        context = ExecutionContext(
            tool_name="test_tool",
            retry_count=2,  # Allow 2 retries (3 total attempts)
        )

        result = executor.execute(
            handler=handler_with_retry,
            arguments={},
            context=context,
        )

        assert result.status == ExecutionStatus.SUCCESS
        assert call_count == 2
        assert result.retry_count == 1

    def test_execute_fails_after_all_retries(self, tool_executor, mock_logger):
        """Test that execution fails after all retries are exhausted."""
        executor = ToolExecutor(logger=mock_logger)
        handler = Mock(side_effect=ValueError("Persistent error"))

        context = ExecutionContext(
            tool_name="test_tool",
            retry_count=1,  # Allow 1 retry (2 total attempts)
        )

        result = executor.execute(
            handler=handler,
            arguments={},
            context=context,
        )

        assert result.status == ExecutionStatus.FAILURE
        assert handler.call_count == 2


class TestAsynchronousExecution:
    """Test asynchronous tool execution."""

    @pytest.mark.asyncio
    async def test_execute_async_successful_tool(self, tool_executor, execution_context):
        """Test successful async tool execution."""
        async_handler = AsyncMock(return_value={"result": "async_success"})

        result = await tool_executor.execute_async(
            handler=async_handler,
            arguments={"param": "value"},
            context=execution_context,
        )

        assert result.status == ExecutionStatus.SUCCESS
        assert result.output == {"result": "async_success"}
        async_handler.assert_called_once_with(param="value")

    @pytest.mark.asyncio
    async def test_execute_async_with_exception(self, tool_executor, execution_context):
        """Test async tool execution that raises an exception."""
        async_handler = AsyncMock(side_effect=ValueError("Async error"))

        result = await tool_executor.execute_async(
            handler=async_handler,
            arguments={},
            context=execution_context,
        )

        assert result.status == ExecutionStatus.FAILURE
        assert "Async error" in result.error

    @pytest.mark.asyncio
    async def test_execute_async_timeout(self, tool_executor):
        """Test async tool execution timeout."""

        async def slow_handler():
            await asyncio.sleep(10)
            return {"result": "slow"}

        context = ExecutionContext(
            tool_name="slow_tool",
            timeout_seconds=0.1,  # Very short timeout
        )

        result = await tool_executor.execute_async(
            handler=slow_handler,
            arguments={},
            context=context,
        )

        assert result.status == ExecutionStatus.TIMEOUT
        assert "timeout" in result.error.lower()


class TestExecutionStatistics:
    """Test execution statistics and monitoring."""

    def test_get_statistics_empty(self, tool_executor):
        """Test statistics when no executions have occurred."""
        stats = tool_executor.get_statistics()

        assert stats["total_executions"] == 0
        assert stats["total_failures"] == 0
        assert stats["success_rate"] == 0.0
        assert stats["average_duration_ms"] == 0.0

    def test_get_statistics_after_successful_execution(
        self, tool_executor, execution_context
    ):
        """Test statistics after successful execution."""
        handler = Mock(return_value={"result": "success"})

        tool_executor.execute(
            handler=handler,
            arguments={},
            context=execution_context,
        )

        stats = tool_executor.get_statistics()

        assert stats["total_executions"] == 1
        assert stats["total_failures"] == 0
        assert stats["success_rate"] == 1.0
        assert stats["average_duration_ms"] > 0

    def test_get_statistics_with_failures(self, mock_logger):
        """Test statistics with both successes and failures."""
        executor = ToolExecutor(logger=mock_logger)
        success_handler = Mock(return_value={"result": "success"})
        failure_handler = Mock(side_effect=ValueError("Error"))

        context = ExecutionContext(tool_name="test", retry_count=0)

        # Execute successful tool
        executor.execute(
            handler=success_handler,
            arguments={},
            context=context,
        )

        # Execute failing tool
        executor.execute(
            handler=failure_handler,
            arguments={},
            context=context,
        )

        stats = executor.get_statistics()

        assert stats["total_executions"] == 2
        assert stats["total_failures"] == 1
        assert stats["success_rate"] == 0.5


class TestToolExecutorIntegration:
    """Integration tests with real logger."""

    def test_tool_executor_with_real_logger(self):
        """Test ToolExecutor with real logger."""
        logger = get_logger(__name__)
        executor = ToolExecutor(logger=logger)

        handler = Mock(return_value={"result": "success"})
        context = ExecutionContext(tool_name="integration_test")

        result = executor.execute(
            handler=handler,
            arguments={},
            context=context,
        )

        assert result.status == ExecutionStatus.SUCCESS
        assert executor._execution_count == 1

    @pytest.mark.asyncio
    async def test_tool_executor_async_with_real_logger(self):
        """Test async execution with real logger."""
        logger = get_logger(__name__)
        executor = ToolExecutor(logger=logger)

        async def async_handler():
            return {"result": "async_success"}

        context = ExecutionContext(tool_name="async_integration_test")

        result = await executor.execute_async(
            handler=async_handler,
            arguments={},
            context=context,
        )

        assert result.status == ExecutionStatus.SUCCESS
