"""
Copyright (C) 2025 Luciano Guerche

This file is part of rabbitmq-mcp-server.

rabbitmq-mcp-server is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

rabbitmq-mcp-server is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with rabbitmq-mcp-server. If not, see <https://www.gnu.org/licenses/>.
"""

"""Unit tests for DeleteExchangeOperation (Story 3.7)."""

from unittest.mock import AsyncMock, Mock

import httpx
import pytest

from rabbitmq_mcp_connection.operations.topology import DeleteExchangeOperation
from rabbitmq_mcp_connection.schemas.topology import (
    DeleteExchangeRequest,
    DeleteExchangeResponse,
)


@pytest.fixture
def mock_http_client():
    """Mock HTTPClientManager."""
    return Mock()


@pytest.fixture
def delete_exchange_operation(mock_http_client):
    """Create DeleteExchangeOperation instance."""
    return DeleteExchangeOperation(mock_http_client)


# ============================================================================
# Operation Tests - Success Cases
# ============================================================================


class TestDeleteExchangeSuccess:
    """Test successful exchange deletion scenarios (AC #2, #3, #6)."""

    @pytest.mark.asyncio
    async def test_delete_exchange_without_bindings_success(
        self, delete_exchange_operation, mock_http_client
    ):
        """Test delete exchange without bindings succeeds (AC #3, #6)."""
        # Mock binding check - no bindings
        mock_bindings_response = Mock()
        mock_bindings_response.status_code = 200
        mock_bindings_response.json.return_value = []

        # Mock DELETE response - success
        mock_delete_response = Mock()
        mock_delete_response.status_code = 204

        mock_http_client.request = AsyncMock(
            side_effect=[mock_bindings_response, mock_delete_response]
        )

        request = DeleteExchangeRequest(vhost="/", name="events")
        response = await delete_exchange_operation.execute(
            request, correlation_id="test-123"
        )

        assert isinstance(response, DeleteExchangeResponse)
        assert response.status == "success"
        assert response.message == "Exchange 'events' deleted"
        assert response.vhost == "/"
        assert response.name == "events"

        # Verify GET bindings was called
        assert mock_http_client.request.call_count == 2
        get_call = mock_http_client.request.call_args_list[0]
        # Check for method keyword argument
        method = get_call.kwargs.get("method") or (
            get_call.args[0] if get_call.args else None
        )
        path = get_call.kwargs.get("path") or (
            get_call.args[1] if len(get_call.args) > 1 else None
        )
        assert method == "GET"
        assert "/bindings/source" in path

        # Verify DELETE was called with if-unused=true
        delete_call = mock_http_client.request.call_args_list[1]
        method = delete_call.kwargs.get("method") or (
            delete_call.args[0] if delete_call.args else None
        )
        path = delete_call.kwargs.get("path") or (
            delete_call.args[1] if len(delete_call.args) > 1 else None
        )
        assert method == "DELETE"
        assert "if-unused=true" in path

    @pytest.mark.asyncio
    async def test_delete_exchange_with_force_flag_success(
        self, delete_exchange_operation, mock_http_client
    ):
        """Test delete exchange with force=True bypasses binding check (AC #2)."""
        # Mock DELETE response - success (no binding check call)
        mock_delete_response = Mock()
        mock_delete_response.status_code = 204

        mock_http_client.request = AsyncMock(return_value=mock_delete_response)

        request = DeleteExchangeRequest(vhost="/", name="orders", force=True)
        response = await delete_exchange_operation.execute(
            request, correlation_id="test-456"
        )

        assert response.status == "success"
        assert response.message == "Exchange 'orders' deleted"
        assert response.vhost == "/"
        assert response.name == "orders"

        # Verify only DELETE was called (no binding check)
        assert mock_http_client.request.call_count == 1
        delete_call = mock_http_client.request.call_args_list[0]
        method = delete_call.kwargs.get("method") or (
            delete_call.args[0] if delete_call.args else None
        )
        path = delete_call.kwargs.get("path") or (
            delete_call.args[1] if len(delete_call.args) > 1 else None
        )
        assert method == "DELETE"
        # Should NOT have if-unused when forced
        assert "if-unused" not in path

    @pytest.mark.asyncio
    async def test_delete_exchange_not_found_idempotent(
        self, delete_exchange_operation, mock_http_client
    ):
        """Test delete non-existent exchange returns success (idempotent, AC #6)."""
        # Mock binding check - 404 (exchange doesn't exist)
        mock_bindings_response = Mock()
        mock_bindings_response.status_code = 404

        # Mock DELETE response - 404
        mock_delete_response = Mock()
        mock_delete_response.status_code = 404

        mock_http_client.request = AsyncMock(
            side_effect=[mock_bindings_response, mock_delete_response]
        )

        request = DeleteExchangeRequest(vhost="/", name="non-existent")
        response = await delete_exchange_operation.execute(
            request, correlation_id="test-789"
        )

        assert response.status == "success"
        assert response.message == "Exchange 'non-existent' deleted"

    @pytest.mark.asyncio
    async def test_delete_exchange_custom_vhost(
        self, delete_exchange_operation, mock_http_client
    ):
        """Test delete exchange from custom vhost."""
        # Mock binding check
        mock_bindings_response = Mock()
        mock_bindings_response.status_code = 200
        mock_bindings_response.json.return_value = []

        # Mock DELETE response
        mock_delete_response = Mock()
        mock_delete_response.status_code = 204

        mock_http_client.request = AsyncMock(
            side_effect=[mock_bindings_response, mock_delete_response]
        )

        request = DeleteExchangeRequest(vhost="/prod", name="notifications")
        response = await delete_exchange_operation.execute(
            request, correlation_id="test-prod"
        )

        assert response.status == "success"
        assert response.vhost == "/prod"


# ============================================================================
# Operation Tests - Safety Validation (Blocking)
# ============================================================================


class TestDeleteExchangeSafetyValidation:
    """Test safety validation for exchanges with bindings (AC #1, #2)."""

    @pytest.mark.asyncio
    async def test_delete_exchange_with_bindings_blocked(
        self, delete_exchange_operation, mock_http_client
    ):
        """Test delete exchange with bindings is blocked (AC #1)."""
        # Mock binding check - 5 active bindings
        mock_bindings_response = Mock()
        mock_bindings_response.status_code = 200
        mock_bindings_response.json.return_value = [
            {"source": "events", "destination": "queue1"},
            {"source": "events", "destination": "queue2"},
            {"source": "events", "destination": "queue3"},
            {"source": "events", "destination": "queue4"},
            {"source": "events", "destination": "queue5"},
        ]

        mock_http_client.request = AsyncMock(return_value=mock_bindings_response)

        request = DeleteExchangeRequest(vhost="/", name="events")

        with pytest.raises(
            ValueError,
            match="Exchange 'events' has 5 active bindings. Use force=true to confirm deletion.",
        ):
            await delete_exchange_operation.execute(
                request, correlation_id="test-block"
            )

        # Verify DELETE was NOT called (blocked at safety check)
        assert mock_http_client.request.call_count == 1

    @pytest.mark.asyncio
    async def test_delete_exchange_with_one_binding_blocked(
        self, delete_exchange_operation, mock_http_client
    ):
        """Test delete exchange with 1 binding is blocked."""
        mock_bindings_response = Mock()
        mock_bindings_response.status_code = 200
        mock_bindings_response.json.return_value = [
            {"source": "orders", "destination": "processing"}
        ]

        mock_http_client.request = AsyncMock(return_value=mock_bindings_response)

        request = DeleteExchangeRequest(vhost="/", name="orders")

        with pytest.raises(
            ValueError,
            match="Exchange 'orders' has 1 active bindings. Use force=true",
        ):
            await delete_exchange_operation.execute(request)

    @pytest.mark.asyncio
    async def test_delete_exchange_with_bindings_force_override(
        self, delete_exchange_operation, mock_http_client
    ):
        """Test force flag overrides binding check (AC #2)."""
        # Mock DELETE response only (no binding check when forced)
        mock_delete_response = Mock()
        mock_delete_response.status_code = 204

        mock_http_client.request = AsyncMock(return_value=mock_delete_response)

        request = DeleteExchangeRequest(vhost="/", name="events", force=True)
        response = await delete_exchange_operation.execute(request)

        assert response.status == "success"
        # Verify binding check was skipped
        assert mock_http_client.request.call_count == 1


# ============================================================================
# Operation Tests - Error Handling
# ============================================================================


class TestDeleteExchangeErrorHandling:
    """Test error handling for delete exchange operation (AC #7, #8)."""

    @pytest.mark.asyncio
    async def test_delete_exchange_timeout(
        self, delete_exchange_operation, mock_http_client
    ):
        """Test timeout error handling (AC #7)."""
        mock_http_client.request = AsyncMock(
            side_effect=httpx.TimeoutException("Request timed out")
        )

        request = DeleteExchangeRequest(vhost="/", name="events", force=True)

        with pytest.raises(httpx.TimeoutException):
            await delete_exchange_operation.execute(request)

    @pytest.mark.asyncio
    async def test_delete_exchange_binding_check_failure_allows_deletion(
        self, delete_exchange_operation, mock_http_client
    ):
        """Test binding check failure logs warning but allows deletion."""
        # Mock binding check - error
        mock_bindings_response = Mock()
        mock_bindings_response.status_code = 500
        mock_bindings_response.raise_for_status.side_effect = httpx.HTTPStatusError(
            "Server error", request=Mock(), response=mock_bindings_response
        )

        # Mock DELETE response - success
        mock_delete_response = Mock()
        mock_delete_response.status_code = 204

        mock_http_client.request = AsyncMock(
            side_effect=[mock_bindings_response, mock_delete_response]
        )

        request = DeleteExchangeRequest(vhost="/", name="events")
        response = await delete_exchange_operation.execute(request)

        # Should succeed despite binding check failure
        assert response.status == "success"


# ============================================================================
# Operation Tests - URL Encoding
# ============================================================================


class TestDeleteExchangeURLEncoding:
    """Test URL encoding for vhost and exchange names."""

    @pytest.mark.asyncio
    async def test_delete_exchange_url_encodes_vhost(
        self, delete_exchange_operation, mock_http_client
    ):
        """Test vhost is URL encoded in API calls."""
        mock_bindings_response = Mock()
        mock_bindings_response.status_code = 200
        mock_bindings_response.json.return_value = []

        mock_delete_response = Mock()
        mock_delete_response.status_code = 204

        mock_http_client.request = AsyncMock(
            side_effect=[mock_bindings_response, mock_delete_response]
        )

        request = DeleteExchangeRequest(vhost="/my-vhost", name="events")
        await delete_exchange_operation.execute(request)

        # Check URL encoding
        get_call = mock_http_client.request.call_args_list[0]
        get_path = get_call.kwargs.get("path") or (
            get_call.args[1] if len(get_call.args) > 1 else None
        )
        assert "%2Fmy-vhost" in get_path

        delete_call = mock_http_client.request.call_args_list[1]
        delete_path = delete_call.kwargs.get("path") or (
            delete_call.args[1] if len(delete_call.args) > 1 else None
        )
        assert "%2Fmy-vhost" in delete_path

    @pytest.mark.asyncio
    async def test_delete_exchange_url_encodes_name(
        self, delete_exchange_operation, mock_http_client
    ):
        """Test exchange name is URL encoded in API calls."""
        mock_bindings_response = Mock()
        mock_bindings_response.status_code = 200
        mock_bindings_response.json.return_value = []

        mock_delete_response = Mock()
        mock_delete_response.status_code = 204

        mock_http_client.request = AsyncMock(
            side_effect=[mock_bindings_response, mock_delete_response]
        )

        request = DeleteExchangeRequest(vhost="/", name="my-exchange.test")
        await delete_exchange_operation.execute(request)

        get_call = mock_http_client.request.call_args_list[0]
        get_path = get_call.kwargs.get("path") or (
            get_call.args[1] if len(get_call.args) > 1 else None
        )
        # Period and hyphen may or may not be encoded depending on quote() safe parameter
        assert "my-exchange" in get_path


# ============================================================================
# Operation Tests - Correlation ID
# ============================================================================


class TestDeleteExchangeCorrelationID:
    """Test correlation_id tracking in logs (AC #8)."""

    @pytest.mark.asyncio
    async def test_delete_exchange_with_correlation_id(
        self, delete_exchange_operation, mock_http_client
    ):
        """Test correlation_id is passed through operation."""
        mock_bindings_response = Mock()
        mock_bindings_response.status_code = 200
        mock_bindings_response.json.return_value = []

        mock_delete_response = Mock()
        mock_delete_response.status_code = 204

        mock_http_client.request = AsyncMock(
            side_effect=[mock_bindings_response, mock_delete_response]
        )

        request = DeleteExchangeRequest(vhost="/", name="events")
        response = await delete_exchange_operation.execute(
            request, correlation_id="test-correlation-123"
        )

        assert response.status == "success"
        # Correlation ID tracking verified in logs (manual inspection)

    @pytest.mark.asyncio
    async def test_delete_exchange_without_correlation_id(
        self, delete_exchange_operation, mock_http_client
    ):
        """Test operation works without correlation_id."""
        mock_bindings_response = Mock()
        mock_bindings_response.status_code = 200
        mock_bindings_response.json.return_value = []

        mock_delete_response = Mock()
        mock_delete_response.status_code = 204

        mock_http_client.request = AsyncMock(
            side_effect=[mock_bindings_response, mock_delete_response]
        )

        request = DeleteExchangeRequest(vhost="/", name="events")
        response = await delete_exchange_operation.execute(request)

        assert response.status == "success"
