"""Unit tests for DeleteBindingOperation (Story 3.10 Tasks 2-6)."""

from unittest.mock import AsyncMock, Mock

import httpx
import pytest

from rabbitmq_mcp_connection.operations.topology import (
    DeleteBindingOperation,
    ListBindingsOperation,
)
from rabbitmq_mcp_connection.schemas.topology import (
    BindingInfo,
    DeleteBindingRequest,
    DeleteBindingResponse,
)


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


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


@pytest.fixture
def sample_bindings():
    """Sample bindings list from Management API."""
    return [
        BindingInfo(
            source="events",
            destination="orders-queue",
            destination_type="queue",
            routing_key="order.*",
            vhost="/",
            arguments={},
            properties_key="~order.*",
        ),
        BindingInfo(
            source="events",
            destination="orders-queue",
            destination_type="queue",
            routing_key="order.created",
            vhost="/",
            arguments={},
            properties_key="~order.created",
        ),
    ]


class TestDeleteBindingOperation:
    """Test DeleteBindingOperation.execute() method (AC #1-7)."""

    @pytest.mark.asyncio
    async def test_execute_delete_binding_success(
        self, operation, mock_http_client, sample_bindings
    ):
        """Test successful binding deletion (AC #1, #3)."""
        # Mock list bindings response
        operation.list_bindings_operation.execute = AsyncMock(
            return_value=sample_bindings
        )

        # Mock DELETE response (204 No Content)
        delete_response = Mock()
        delete_response.status_code = 204
        mock_http_client.request.return_value = delete_response

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

        response = await operation.execute(request, correlation_id="test-123")

        # Verify response
        assert response.status == "success"
        assert response.message == "Binding deleted"
        assert response.source == "events"
        assert response.destination == "orders-queue"
        assert response.routing_key == "order.*"
        assert response.vhost == "/"
        assert response.destination_type == "queue"
        assert response.correlation_id == "test-123"
        assert response.duration_ms is not None
        assert response.duration_ms >= 0

        # Verify list bindings was called
        operation.list_bindings_operation.execute.assert_called_once()

        # Verify DELETE request was called with correct endpoint
        mock_http_client.request.assert_called_once()
        call_args = mock_http_client.request.call_args
        assert call_args[0][0] == "DELETE"
        assert "/api/bindings/" in call_args[0][1]
        assert "~order.*" in call_args[0][1]  # properties_key in URL

    @pytest.mark.asyncio
    async def test_execute_delete_binding_exchange_to_exchange(
        self, operation, mock_http_client
    ):
        """Test exchange-to-exchange binding deletion (AC #1)."""
        # Mock list bindings response
        binding = BindingInfo(
            source="upstream",
            destination="downstream",
            destination_type="exchange",
            routing_key="events.*",
            vhost="/",
            arguments={},
            properties_key="~events.*",
        )
        operation.list_bindings_operation.execute = AsyncMock(return_value=[binding])

        # Mock DELETE response
        delete_response = Mock()
        delete_response.status_code = 204
        mock_http_client.request.return_value = delete_response

        # Execute delete operation
        request = DeleteBindingRequest(
            source="upstream",
            destination="downstream",
            destination_type="exchange",
            routing_key="events.*",
            vhost="/",
        )

        response = await operation.execute(request, correlation_id="test-123")

        # Verify response
        assert response.status == "success"
        assert response.destination_type == "exchange"

        # Verify endpoint includes /e/ for exchange
        call_args = mock_http_client.request.call_args
        endpoint = call_args[0][1]
        assert "/e/upstream/e/downstream/" in endpoint

    @pytest.mark.asyncio
    async def test_execute_binding_not_found_in_list(self, operation, mock_http_client):
        """Test error when binding not found in list (AC #4, Task 3)."""
        # Mock list bindings response with different routing_key
        binding = BindingInfo(
            source="events",
            destination="orders-queue",
            destination_type="queue",
            routing_key="order.created",  # Different routing key
            vhost="/",
            arguments={},
            properties_key="~order.created",
        )
        operation.list_bindings_operation.execute = AsyncMock(return_value=[binding])

        # Execute delete operation
        request = DeleteBindingRequest(
            source="events",
            destination="orders-queue",
            routing_key="order.*",  # Looking for this routing key
            vhost="/",
        )

        # Should raise ValueError
        with pytest.raises(ValueError) as exc_info:
            await operation.execute(request, correlation_id="test-123")

        # Verify error message
        error_msg = str(exc_info.value)
        assert "Binding not found" in error_msg
        assert "source='events'" in error_msg
        assert "destination='orders-queue'" in error_msg
        assert "routing_key='order.*'" in error_msg

        # Verify DELETE was not called
        mock_http_client.request.assert_not_called()

    @pytest.mark.asyncio
    async def test_execute_binding_not_found_404(self, operation, mock_http_client):
        """Test 404 Not Found response handling (AC #4, Task 4)."""
        # Mock list bindings response
        binding = BindingInfo(
            source="events",
            destination="orders-queue",
            destination_type="queue",
            routing_key="order.*",
            vhost="/",
            arguments={},
            properties_key="~order.*",
        )
        operation.list_bindings_operation.execute = AsyncMock(return_value=[binding])

        # Mock DELETE response (404 Not Found)
        delete_response = Mock()
        delete_response.status_code = 404
        mock_http_client.request.return_value = delete_response

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

        # Should raise ValueError
        with pytest.raises(ValueError) as exc_info:
            await operation.execute(request, correlation_id="test-123")

        # Verify error message format
        error_msg = str(exc_info.value)
        assert "Binding not found" in error_msg
        assert "Verify binding exists before deletion" in error_msg

    @pytest.mark.asyncio
    async def test_execute_multiple_bindings_preserved(
        self, operation, mock_http_client, sample_bindings
    ):
        """Test deletion preserves other bindings (AC #2)."""
        # Mock list bindings response with multiple bindings
        operation.list_bindings_operation.execute = AsyncMock(
            return_value=sample_bindings
        )

        # Mock DELETE response
        delete_response = Mock()
        delete_response.status_code = 204
        mock_http_client.request.return_value = delete_response

        # Delete only one binding
        request = DeleteBindingRequest(
            source="events",
            destination="orders-queue",
            routing_key="order.*",  # Delete this one
            vhost="/",
        )

        response = await operation.execute(request, correlation_id="test-123")

        # Verify success
        assert response.status == "success"

        # Verify correct properties_key was used
        call_args = mock_http_client.request.call_args
        endpoint = call_args[0][1]
        assert "~order.*" in endpoint  # Correct properties_key
        assert "~order.created" not in endpoint  # Other binding preserved

    @pytest.mark.asyncio
    async def test_execute_performance_monitoring_slow(
        self, operation, mock_http_client, sample_bindings
    ):
        """Test performance monitoring for slow operations (AC #5, Task 6)."""
        # Mock list bindings response
        operation.list_bindings_operation.execute = AsyncMock(
            return_value=sample_bindings
        )

        # Mock DELETE response
        delete_response = Mock()
        delete_response.status_code = 204
        mock_http_client.request.return_value = delete_response

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

        response = await operation.execute(request, correlation_id="test-123")

        # Verify success and duration is tracked
        assert response.status == "success"
        assert response.duration_ms is not None
        assert response.duration_ms >= 0

    @pytest.mark.asyncio
    async def test_execute_url_encoding(self, operation, mock_http_client):
        """Test URL encoding for vhost, source, destination (Architecture Constraints)."""
        # Mock list bindings response
        binding = BindingInfo(
            source="test-exchange",
            destination="test-queue",
            destination_type="queue",
            routing_key="test.key",
            vhost="/custom",
            arguments={},
            properties_key="~test.key",
        )
        operation.list_bindings_operation.execute = AsyncMock(return_value=[binding])

        # Mock DELETE response
        delete_response = Mock()
        delete_response.status_code = 204
        mock_http_client.request.return_value = delete_response

        # Execute delete operation
        request = DeleteBindingRequest(
            source="test-exchange",
            destination="test-queue",
            routing_key="test.key",
            vhost="/custom",
        )

        await operation.execute(request, correlation_id="test-123")

        # Verify URL encoding in endpoint
        call_args = mock_http_client.request.call_args
        endpoint = call_args[0][1]
        assert "%2Fcustom" in endpoint or "/custom" in endpoint  # URL-encoded vhost

    @pytest.mark.asyncio
    async def test_execute_timeout_exception(self, operation, mock_http_client):
        """Test timeout exception handling (AC #5)."""
        # Mock list bindings response
        binding = BindingInfo(
            source="events",
            destination="orders-queue",
            destination_type="queue",
            routing_key="order.*",
            vhost="/",
            arguments={},
            properties_key="~order.*",
        )
        operation.list_bindings_operation.execute = AsyncMock(return_value=[binding])

        # Mock timeout exception
        mock_http_client.request.side_effect = httpx.TimeoutException("Request timeout")

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

        # Should raise TimeoutException
        with pytest.raises(httpx.TimeoutException):
            await operation.execute(request, correlation_id="test-123")

    @pytest.mark.asyncio
    async def test_execute_unexpected_status_code(
        self, operation, mock_http_client, sample_bindings
    ):
        """Test handling of unexpected response status codes."""
        # Mock list bindings response
        operation.list_bindings_operation.execute = AsyncMock(
            return_value=sample_bindings
        )

        # Mock unexpected response (e.g., 500 Internal Server Error)
        delete_response = Mock()
        delete_response.status_code = 500
        mock_http_client.request.return_value = delete_response

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

        # Should raise ValueError
        with pytest.raises(ValueError) as exc_info:
            await operation.execute(request, correlation_id="test-123")

        error_msg = str(exc_info.value)
        assert "Unexpected response status: 500" in error_msg

    @pytest.mark.asyncio
    async def test_execute_list_bindings_error(self, operation, mock_http_client):
        """Test error handling when listing bindings fails (Task 3)."""
        # Mock list bindings to raise an error
        operation.list_bindings_operation.execute = AsyncMock(
            side_effect=Exception("Failed to list bindings")
        )

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

        # Should raise ValueError with helpful message
        with pytest.raises(ValueError) as exc_info:
            await operation.execute(request, correlation_id="test-123")

        error_msg = str(exc_info.value)
        assert "Failed to list bindings" in error_msg
        assert "events" in error_msg
        assert "orders-queue" in error_msg

    @pytest.mark.asyncio
    async def test_find_binding_properties_key_success(
        self, operation, sample_bindings
    ):
        """Test _find_binding_properties_key private method (Task 3)."""
        # Mock list bindings response
        operation.list_bindings_operation.execute = AsyncMock(
            return_value=sample_bindings
        )

        # Test finding properties_key
        request = DeleteBindingRequest(
            source="events",
            destination="orders-queue",
            routing_key="order.*",
            vhost="/",
        )

        properties_key = await operation._find_binding_properties_key(
            request, correlation_id="test-123"
        )

        assert properties_key == "~order.*"

    @pytest.mark.asyncio
    async def test_find_binding_properties_key_not_found(
        self, operation, sample_bindings
    ):
        """Test _find_binding_properties_key when binding not found (Task 3)."""
        # Mock list bindings response
        operation.list_bindings_operation.execute = AsyncMock(
            return_value=sample_bindings
        )

        # Test with non-existent routing_key
        request = DeleteBindingRequest(
            source="events",
            destination="orders-queue",
            routing_key="nonexistent.key",
            vhost="/",
        )

        # Should raise ValueError
        with pytest.raises(ValueError) as exc_info:
            await operation._find_binding_properties_key(
                request, correlation_id="test-123"
            )

        error_msg = str(exc_info.value)
        assert "Binding not found" in error_msg
        assert "routing_key='nonexistent.key'" in error_msg
