"""Unit tests for CreateBindingOperation (Story 3.9 Tasks 2-7)."""

from unittest.mock import AsyncMock, Mock

import httpx
import pytest

from rabbitmq_mcp_connection.operations.topology import CreateBindingOperation
from rabbitmq_mcp_connection.schemas.topology import BindingInfo, CreateBindingRequest


@pytest.fixture
def mock_http_client():
    """Create mock HTTPClientManager."""
    client = Mock()
    client.request = AsyncMock()
    return client


@pytest.fixture
def operation(mock_http_client):
    """Create CreateBindingOperation instance."""
    return CreateBindingOperation(mock_http_client)


@pytest.fixture
def sample_exchange_response():
    """Sample exchange response from Management API."""
    return {
        "name": "events",
        "vhost": "/",
        "type": "topic",
        "durable": True,
        "auto_delete": False,
        "internal": False,
        "arguments": {},
    }


@pytest.fixture
def sample_queue_response():
    """Sample queue response from Management API."""
    return {
        "name": "orders-queue",
        "vhost": "/",
        "durable": True,
        "auto_delete": False,
        "arguments": {},
        "messages": 0,
        "state": "idle",
    }


@pytest.fixture
def sample_binding_response():
    """Sample binding response from Management API."""
    return [
        {
            "source": "events",
            "destination": "orders-queue",
            "destination_type": "queue",
            "routing_key": "order.*",
            "vhost": "/",
            "arguments": {},
            "properties_key": "~order.*",
        }
    ]


class TestCreateBindingOperation:
    """Test CreateBindingOperation.execute() method (AC #1-9)."""

    @pytest.mark.asyncio
    async def test_execute_create_exchange_to_queue_binding(
        self,
        operation,
        mock_http_client,
        sample_exchange_response,
        sample_queue_response,
        sample_binding_response,
    ):
        """Test successful exchange-to-queue binding creation (AC #1, #2)."""
        # Setup mock responses
        # GET exchange (validation)
        exchange_response = Mock()
        exchange_response.status_code = 200
        exchange_response.json.return_value = sample_exchange_response

        # GET queue (validation)
        queue_response = Mock()
        queue_response.status_code = 200
        queue_response.json.return_value = sample_queue_response

        # POST binding returns 201 Created
        post_response = Mock()
        post_response.status_code = 201

        # GET binding details after creation
        binding_list_response = Mock()
        binding_list_response.status_code = 200
        binding_list_response.json.return_value = sample_binding_response

        mock_http_client.request.side_effect = [
            exchange_response,
            queue_response,
            post_response,
            binding_list_response,
        ]

        # Execute
        request = CreateBindingRequest(
            source="events",
            destination="orders-queue",
            routing_key="order.*",
            vhost="/",
        )
        result = await operation.execute(request, correlation_id="test-123")

        # Verify result
        assert isinstance(result, BindingInfo)
        assert result.source == "events"
        assert result.destination == "orders-queue"
        assert result.destination_type == "queue"
        assert result.routing_key == "order.*"
        assert result.vhost == "/"

        # Verify HTTP calls
        assert mock_http_client.request.call_count == 4

        # Validate calls
        assert mock_http_client.request.call_args_list[0][0] == (
            "GET",
            "/api/exchanges/%2F/events",
        )
        assert mock_http_client.request.call_args_list[1][0] == (
            "GET",
            "/api/queues/%2F/orders-queue",
        )
        assert mock_http_client.request.call_args_list[2][0] == (
            "POST",
            "/api/bindings/%2F/e/events/q/orders-queue",
        )
        assert mock_http_client.request.call_args_list[2][1]["json"] == {
            "routing_key": "order.*",
            "arguments": {},
        }

    @pytest.mark.asyncio
    async def test_execute_create_exchange_to_exchange_binding(
        self, operation, mock_http_client, sample_exchange_response
    ):
        """Test successful exchange-to-exchange binding creation (AC #2)."""
        # Setup mock responses
        source_exchange = Mock()
        source_exchange.status_code = 200
        source_exchange.json.return_value = sample_exchange_response

        dest_exchange = Mock()
        dest_exchange.status_code = 200
        dest_exchange_data = sample_exchange_response.copy()
        dest_exchange_data["name"] = "downstream"
        dest_exchange.json.return_value = dest_exchange_data

        post_response = Mock()
        post_response.status_code = 201

        binding_list_response = Mock()
        binding_list_response.status_code = 200
        binding_list_response.json.return_value = [
            {
                "source": "events",
                "destination": "downstream",
                "destination_type": "exchange",
                "routing_key": "events.*",
                "vhost": "/",
                "arguments": {},
            }
        ]

        mock_http_client.request.side_effect = [
            source_exchange,
            dest_exchange,
            post_response,
            binding_list_response,
        ]

        # Execute
        request = CreateBindingRequest(
            source="events",
            destination="downstream",
            destination_type="exchange",
            routing_key="events.*",
            vhost="/",
        )
        result = await operation.execute(request)

        # Verify result
        assert result.source == "events"
        assert result.destination == "downstream"
        assert result.destination_type == "exchange"
        assert result.routing_key == "events.*"

        # Verify POST endpoint for exchange-to-exchange
        assert mock_http_client.request.call_args_list[2][0] == (
            "POST",
            "/api/bindings/%2F/e/events/e/downstream",
        )

    @pytest.mark.asyncio
    async def test_execute_idempotent_binding_creation(
        self,
        operation,
        mock_http_client,
        sample_exchange_response,
        sample_queue_response,
        sample_binding_response,
    ):
        """Test idempotent binding creation (AC #4, Task 4)."""
        # Setup mock responses
        exchange_response = Mock()
        exchange_response.status_code = 200
        exchange_response.json.return_value = sample_exchange_response

        queue_response = Mock()
        queue_response.status_code = 200
        queue_response.json.return_value = sample_queue_response

        # POST binding returns 204 No Content (idempotent)
        post_response = Mock()
        post_response.status_code = 204

        # GET existing binding details
        binding_list_response = Mock()
        binding_list_response.status_code = 200
        binding_list_response.json.return_value = sample_binding_response

        mock_http_client.request.side_effect = [
            exchange_response,
            queue_response,
            post_response,
            binding_list_response,
        ]

        # Execute
        request = CreateBindingRequest(
            source="events",
            destination="orders-queue",
            routing_key="order.*",
            vhost="/",
        )
        result = await operation.execute(request, correlation_id="test-456")

        # Verify idempotent result
        assert isinstance(result, BindingInfo)
        assert result.source == "events"
        assert result.destination == "orders-queue"

    @pytest.mark.asyncio
    async def test_execute_source_exchange_not_found(self, operation, mock_http_client):
        """Test error when source exchange doesn't exist (AC #5, Task 3, 5)."""
        # Setup mock: GET exchange returns 404
        exchange_response = Mock()
        exchange_response.status_code = 404

        mock_http_client.request.side_effect = [exchange_response]

        # Execute
        request = CreateBindingRequest(
            source="non-existent",
            destination="orders-queue",
            routing_key="order.*",
            vhost="/",
        )

        with pytest.raises(ValueError) as exc_info:
            await operation.execute(request)

        # Verify error message (AC #5)
        assert "Source exchange 'non-existent' not found" in str(exc_info.value)
        assert "Please create the exchange first" in str(exc_info.value)

        # Verify only validation call was made (no POST)
        assert mock_http_client.request.call_count == 1

    @pytest.mark.asyncio
    async def test_execute_destination_queue_not_found(
        self, operation, mock_http_client, sample_exchange_response
    ):
        """Test error when destination queue doesn't exist (AC #6, Task 3, 5)."""
        # Setup mock responses
        exchange_response = Mock()
        exchange_response.status_code = 200
        exchange_response.json.return_value = sample_exchange_response

        # GET queue returns 404
        queue_response = Mock()
        queue_response.status_code = 404

        mock_http_client.request.side_effect = [exchange_response, queue_response]

        # Execute
        request = CreateBindingRequest(
            source="events",
            destination="non-existent-queue",
            routing_key="order.*",
            vhost="/",
        )

        with pytest.raises(ValueError) as exc_info:
            await operation.execute(request)

        # Verify error message (AC #6)
        assert "Destination queue 'non-existent-queue' not found" in str(exc_info.value)
        assert "Please create the queue first" in str(exc_info.value)

    @pytest.mark.asyncio
    async def test_execute_invalid_routing_key_for_direct_exchange(
        self, operation, mock_http_client
    ):
        """Test validation error for wildcards in direct exchange (AC #7, Task 3)."""
        # Setup mock: direct exchange
        exchange_response = Mock()
        exchange_response.status_code = 200
        exchange_response.json.return_value = {
            "name": "direct-exchange",
            "type": "direct",
            "vhost": "/",
        }

        mock_http_client.request.side_effect = [exchange_response]

        # Execute with wildcard routing key (invalid for direct exchange)
        request = CreateBindingRequest(
            source="direct-exchange",
            destination="orders-queue",
            routing_key="order.*",
            vhost="/",
        )

        with pytest.raises(ValueError) as exc_info:
            await operation.execute(request)

        # Verify error message (AC #7)
        assert "does not support wildcards" in str(exc_info.value)

    @pytest.mark.asyncio
    async def test_execute_fanout_exchange_ignores_routing_key(
        self, operation, mock_http_client, sample_queue_response
    ):
        """Test fanout exchange ignores routing key (AC #3, Task 3)."""
        # Setup mock: fanout exchange
        exchange_response = Mock()
        exchange_response.status_code = 200
        exchange_response.json.return_value = {
            "name": "fanout-exchange",
            "type": "fanout",
            "vhost": "/",
        }

        queue_response = Mock()
        queue_response.status_code = 200
        queue_response.json.return_value = sample_queue_response

        post_response = Mock()
        post_response.status_code = 201

        binding_list_response = Mock()
        binding_list_response.status_code = 200
        binding_list_response.json.return_value = [
            {
                "source": "fanout-exchange",
                "destination": "orders-queue",
                "destination_type": "queue",
                "routing_key": "",
                "vhost": "/",
                "arguments": {},
            }
        ]

        mock_http_client.request.side_effect = [
            exchange_response,
            queue_response,
            post_response,
            binding_list_response,
        ]

        # Execute with empty routing key (valid for fanout)
        request = CreateBindingRequest(
            source="fanout-exchange",
            destination="orders-queue",
            routing_key="",
            vhost="/",
        )

        result = await operation.execute(request)

        # No validation error should be raised
        assert result.source == "fanout-exchange"

    @pytest.mark.asyncio
    async def test_execute_topic_exchange_with_wildcards(
        self, operation, mock_http_client, sample_queue_response
    ):
        """Test topic exchange with wildcard patterns (AC #3, Task 3)."""
        # Setup mock: topic exchange
        exchange_response = Mock()
        exchange_response.status_code = 200
        exchange_response.json.return_value = {
            "name": "topic-exchange",
            "type": "topic",
            "vhost": "/",
        }

        queue_response = Mock()
        queue_response.status_code = 200
        queue_response.json.return_value = sample_queue_response

        post_response = Mock()
        post_response.status_code = 201

        binding_list_response = Mock()
        binding_list_response.status_code = 200
        binding_list_response.json.return_value = [
            {
                "source": "topic-exchange",
                "destination": "orders-queue",
                "destination_type": "queue",
                "routing_key": "order.#",
                "vhost": "/",
                "arguments": {},
            }
        ]

        mock_http_client.request.side_effect = [
            exchange_response,
            queue_response,
            post_response,
            binding_list_response,
        ]

        # Execute with wildcard patterns (valid for topic)
        request = CreateBindingRequest(
            source="topic-exchange",
            destination="orders-queue",
            routing_key="order.#",
            vhost="/",
        )

        result = await operation.execute(request)

        # No validation error should be raised
        assert result.routing_key == "order.#"

    @pytest.mark.asyncio
    async def test_execute_with_headers_exchange_arguments(
        self, operation, mock_http_client, sample_queue_response
    ):
        """Test headers exchange with x-match arguments (AC #3)."""
        # Setup mock: headers exchange
        exchange_response = Mock()
        exchange_response.status_code = 200
        exchange_response.json.return_value = {
            "name": "headers-exchange",
            "type": "headers",
            "vhost": "/",
        }

        queue_response = Mock()
        queue_response.status_code = 200
        queue_response.json.return_value = sample_queue_response

        post_response = Mock()
        post_response.status_code = 201

        binding_list_response = Mock()
        binding_list_response.status_code = 200
        binding_list_response.json.return_value = [
            {
                "source": "headers-exchange",
                "destination": "orders-queue",
                "destination_type": "queue",
                "routing_key": "",
                "vhost": "/",
                "arguments": {"x-match": "all", "priority": "high"},
            }
        ]

        mock_http_client.request.side_effect = [
            exchange_response,
            queue_response,
            post_response,
            binding_list_response,
        ]

        # Execute with arguments
        request = CreateBindingRequest(
            source="headers-exchange",
            destination="orders-queue",
            routing_key="",
            arguments={"x-match": "all", "priority": "high"},
            vhost="/",
        )

        await operation.execute(request)

        # Verify arguments in POST body
        assert mock_http_client.request.call_args_list[2][1]["json"] == {
            "routing_key": "",
            "arguments": {"x-match": "all", "priority": "high"},
        }

    @pytest.mark.asyncio
    async def test_execute_400_validation_error(
        self,
        operation,
        mock_http_client,
        sample_exchange_response,
        sample_queue_response,
    ):
        """Test 400 Bad Request validation error handling (AC #7, Task 2)."""
        # Setup mock responses
        exchange_response = Mock()
        exchange_response.status_code = 200
        exchange_response.json.return_value = sample_exchange_response

        queue_response = Mock()
        queue_response.status_code = 200
        queue_response.json.return_value = sample_queue_response

        # POST returns 400 Bad Request
        post_response = Mock()
        post_response.status_code = 400
        post_response.json.return_value = {
            "reason": "Invalid routing key for exchange type"
        }

        mock_http_client.request.side_effect = [
            exchange_response,
            queue_response,
            post_response,
        ]

        # Execute
        request = CreateBindingRequest(
            source="events",
            destination="orders-queue",
            routing_key="invalid-pattern",
            vhost="/",
        )

        with pytest.raises(ValueError) as exc_info:
            await operation.execute(request)

        # Verify error message
        assert "Validation error" in str(exc_info.value)
        assert "Invalid routing key" in str(exc_info.value)

    @pytest.mark.asyncio
    async def test_execute_timeout_exception(self, operation, mock_http_client):
        """Test timeout exception handling (AC #8, Task 7)."""
        # Setup mock to raise TimeoutException during validation
        mock_http_client.request.side_effect = httpx.TimeoutException("Request timeout")

        # Execute
        request = CreateBindingRequest(
            source="events",
            destination="orders-queue",
            routing_key="order.*",
            vhost="/",
        )

        # Timeout can occur during validation or binding creation
        # Both should re-raise the exception
        with pytest.raises((httpx.TimeoutException, ValueError)):
            await operation.execute(request)

    @pytest.mark.asyncio
    async def test_execute_url_encoding(
        self,
        operation,
        mock_http_client,
        sample_exchange_response,
        sample_queue_response,
    ):
        """Test URL encoding of vhost, source, and destination names."""
        # Setup mock responses
        exchange_response = Mock()
        exchange_response.status_code = 200
        exchange_response.json.return_value = sample_exchange_response

        queue_response = Mock()
        queue_response.status_code = 200
        queue_response.json.return_value = sample_queue_response

        post_response = Mock()
        post_response.status_code = 201

        binding_list_response = Mock()
        binding_list_response.status_code = 200
        binding_list_response.json.return_value = [
            {
                "source": "events",
                "destination": "orders-queue",
                "destination_type": "queue",
                "routing_key": "order.*",
                "vhost": "/test",
                "arguments": {},
            }
        ]

        mock_http_client.request.side_effect = [
            exchange_response,
            queue_response,
            post_response,
            binding_list_response,
        ]

        # Execute with special vhost name
        request = CreateBindingRequest(
            source="events",
            destination="orders-queue",
            routing_key="order.*",
            vhost="/test",
        )

        result = await operation.execute(request)

        # Verify result
        assert result.vhost == "/test"

        # Verify URL encoding (/ becomes %2F)
        assert (
            "/api/exchanges/%2Ftest/events"
            in mock_http_client.request.call_args_list[0][0][1]
        )
        assert (
            "/api/queues/%2Ftest/orders-queue"
            in mock_http_client.request.call_args_list[1][0][1]
        )
        assert (
            "/api/bindings/%2Ftest/e/events/q/orders-queue"
            in mock_http_client.request.call_args_list[2][0][1]
        )
