from __future__ import annotations

"""Unit tests for OperationExecutor class."""

from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import pytest
from pydantic import ValidationError

from rabbitmq_mcp_connection.executor import (
    ErrorResponse,
    ExecutionResult,
    OperationExecutor,
)
from rabbitmq_mcp_connection.http_client import HTTPClient


@pytest.fixture
def mock_http_client():
    """Create a mock HTTP client."""
    client = MagicMock(spec=HTTPClient)
    client.base_url = "http://localhost:15672"
    client.timeout = 30
    client.password = "guest"  # Add password attribute for sanitization
    return client


@pytest.fixture
def sample_registry():
    """Create a sample operations registry."""
    return {
        "operations": {
            "queues.list": {
                "operation_id": "queues.list",
                "namespace": "queues",
                "http_method": "GET",
                "url_path": "/api/queues/{vhost}",
                "description": "List queues in vhost",
                "parameters": [{"name": "vhost", "in": "path", "required": True}],
            },
            "queues.create": {
                "operation_id": "queues.create",
                "namespace": "queues",
                "http_method": "PUT",
                "url_path": "/api/queues/{vhost}/{name}",
                "description": "Create a queue",
                "parameters": [
                    {"name": "vhost", "in": "path", "required": True},
                    {"name": "name", "in": "path", "required": True},
                ],
            },
        }
    }


class TestOperationExecutor:
    """Test suite for OperationExecutor class."""

    def test_init(self, mock_http_client, sample_registry):
        """Test OperationExecutor initialization."""
        executor = OperationExecutor(
            http_client=mock_http_client,
            operation_registry=sample_registry,
        )

        assert executor.http_client == mock_http_client
        assert executor.operation_registry == sample_registry

    def test_build_url_with_path_parameters(self, mock_http_client, sample_registry):
        """Test URL building with path parameters."""
        executor = OperationExecutor(mock_http_client, sample_registry)

        url = executor._build_url(
            "/api/queues/{vhost}",
            {"vhost": "/"},
        )

        # URL encoded / → %2F
        assert url == "/api/queues/%2F"

    def test_build_url_with_query_parameters(self, mock_http_client, sample_registry):
        """Test URL building with query parameters."""
        executor = OperationExecutor(mock_http_client, sample_registry)

        url = executor._build_url(
            "/api/queues/{vhost}",
            {"vhost": "/", "page": 1, "limit": 10},
        )

        assert url.startswith("/api/queues/%2F?")
        assert "page=1" in url
        assert "limit=10" in url

    def test_build_url_missing_required_parameter(
        self, mock_http_client, sample_registry
    ):
        """Test URL building with missing required parameter."""
        executor = OperationExecutor(mock_http_client, sample_registry)

        with pytest.raises(Exception) as exc_info:
            executor._build_url(
                "/api/queues/{vhost}",
                {},  # Missing 'vhost'
            )

        assert "vhost" in str(exc_info.value)

    def test_build_request_body_for_get(self, mock_http_client, sample_registry):
        """Test request body building for GET request (should be None)."""
        executor = OperationExecutor(mock_http_client, sample_registry)

        operation = sample_registry["operations"]["queues.list"]
        body = executor._build_request_body(operation, {"vhost": "/"})

        assert body is None

    def test_build_request_body_for_put(self, mock_http_client, sample_registry):
        """Test request body building for PUT request."""
        executor = OperationExecutor(mock_http_client, sample_registry)

        operation = sample_registry["operations"]["queues.create"]
        body = executor._build_request_body(
            operation,
            {"vhost": "/", "name": "test-queue", "durable": True},
        )

        # Should filter out path parameters
        assert body == {"durable": True}

    @pytest.mark.asyncio
    async def test_execute_successful_get(self, mock_http_client, sample_registry):
        """Test successful GET operation execution."""
        executor = OperationExecutor(mock_http_client, sample_registry)

        # Mock HTTP response
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 200
        mock_response.text = '{"queues": []}'
        mock_response.json.return_value = {"queues": []}
        mock_response.raise_for_status = MagicMock()

        mock_http_client.request = AsyncMock(return_value=mock_response)

        result = await executor.execute("queues.list", {"vhost": "/"})

        assert isinstance(result, ExecutionResult)
        assert result.status == "success"
        assert result.result == {"queues": []}
        assert "operation_id" in result.metadata
        assert "http_status_code" in result.metadata
        assert "response_time_ms" in result.metadata

    @pytest.mark.asyncio
    async def test_execute_operation_not_found(self, mock_http_client, sample_registry):
        """Test execution with invalid operation ID."""
        executor = OperationExecutor(mock_http_client, sample_registry)

        with pytest.raises(KeyError) as exc_info:
            await executor.execute("invalid.operation", {})

        assert "Operation 'invalid.operation' not found" in str(exc_info.value)

    @pytest.mark.asyncio
    async def test_execute_http_404_error(self, mock_http_client, sample_registry):
        """Test execution with HTTP 404 error."""
        executor = OperationExecutor(mock_http_client, sample_registry)

        # Mock HTTP 404 error
        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 404

        http_error = httpx.HTTPStatusError(
            "Not found",
            request=MagicMock(),
            response=mock_response,
        )

        mock_http_client.request = AsyncMock(side_effect=http_error)

        result = await executor.execute("queues.list", {"vhost": "/"})

        assert result.status == "error"
        assert result.result["error_code"] == "RESOURCE_NOT_FOUND"
        assert "queues" in result.result["message"]

    @pytest.mark.asyncio
    async def test_execute_http_401_error(self, mock_http_client, sample_registry):
        """Test execution with HTTP 401 authentication error."""
        executor = OperationExecutor(mock_http_client, sample_registry)

        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 401

        http_error = httpx.HTTPStatusError(
            "Unauthorized",
            request=MagicMock(),
            response=mock_response,
        )

        mock_http_client.request = AsyncMock(side_effect=http_error)

        result = await executor.execute("queues.list", {"vhost": "/"})

        assert result.status == "error"
        assert result.result["error_code"] == "AUTHENTICATION_FAILED"
        assert "check credentials" in result.result["message"]

    @pytest.mark.asyncio
    async def test_execute_http_403_error(self, mock_http_client, sample_registry):
        """Test execution with HTTP 403 permission error."""
        executor = OperationExecutor(mock_http_client, sample_registry)

        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 403

        http_error = httpx.HTTPStatusError(
            "Forbidden",
            request=MagicMock(),
            response=mock_response,
        )

        mock_http_client.request = AsyncMock(side_effect=http_error)

        result = await executor.execute("queues.list", {"vhost": "/"})

        assert result.status == "error"
        assert result.result["error_code"] == "PERMISSION_DENIED"

    @pytest.mark.asyncio
    async def test_execute_timeout_error(self, mock_http_client, sample_registry):
        """Test execution with timeout error."""
        executor = OperationExecutor(mock_http_client, sample_registry)

        mock_http_client.request = AsyncMock(
            side_effect=httpx.TimeoutException("Request timed out")
        )

        result = await executor.execute("queues.list", {"vhost": "/"})

        assert result.status == "error"
        assert result.result["error_code"] == "OPERATION_TIMEOUT"
        assert "30s" in result.result["message"]

    @pytest.mark.asyncio
    async def test_execute_connection_error(self, mock_http_client, sample_registry):
        """Test execution with connection error."""
        executor = OperationExecutor(mock_http_client, sample_registry)

        mock_http_client.request = AsyncMock(
            side_effect=httpx.ConnectError("Connection refused")
        )

        result = await executor.execute("queues.list", {"vhost": "/"})

        assert result.status == "error"
        assert result.result["error_code"] == "CONNECTION_FAILED"
        assert "Failed to connect" in result.result["message"]

    def test_map_http_error_500(self, mock_http_client, sample_registry):
        """Test error mapping for HTTP 500 server error."""
        executor = OperationExecutor(mock_http_client, sample_registry)

        mock_response = MagicMock(spec=httpx.Response)
        mock_response.status_code = 500
        mock_response.text = "Internal server error"

        http_error = httpx.HTTPStatusError(
            "Server error",
            request=MagicMock(),
            response=mock_response,
        )

        error_response = executor._map_http_error(
            http_error,
            "queues.list",
            "correlation-123",
        )

        assert isinstance(error_response, ErrorResponse)
        assert error_response.error_code == "RABBITMQ_SERVER_ERROR"
        assert error_response.correlation_id == "correlation-123"
