"""
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 CreateExchangeOperation (Story 3.6)."""

from unittest.mock import AsyncMock, Mock

import pytest

from rabbitmq_mcp_connection.operations.topology import CreateExchangeOperation
from rabbitmq_mcp_connection.schemas.topology import CreateExchangeRequest, ExchangeInfo


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


@pytest.fixture
def create_exchange_operation(mock_http_client):
    """Create CreateExchangeOperation instance."""
    return CreateExchangeOperation(mock_http_client)


# ============================================================================
# Schema Validation Tests (~15 tests)
# ============================================================================


class TestCreateExchangeRequestValidation:
    """Test CreateExchangeRequest schema validation (AC #1, #2, #3, #7, #8)."""

    def test_valid_vhost_default(self):
        """Test valid default vhost '/'."""
        request = CreateExchangeRequest(name="events", type="topic")
        assert request.vhost == "/"

    def test_valid_vhost_custom(self):
        """Test valid custom vhost."""
        request = CreateExchangeRequest(name="events", type="topic", vhost="/prod")
        assert request.vhost == "/prod"

    def test_invalid_vhost_empty(self):
        """Test invalid empty vhost."""
        with pytest.raises(ValueError, match="Virtual host name cannot be empty"):
            CreateExchangeRequest(name="events", type="topic", vhost="")

    def test_valid_exchange_name_alphanumeric(self):
        """Test valid alphanumeric exchange name."""
        request = CreateExchangeRequest(name="events123", type="topic")
        assert request.name == "events123"

    def test_valid_exchange_name_with_special_chars(self):
        """Test valid exchange name with allowed special characters."""
        request = CreateExchangeRequest(name="test-exchange.name_123", type="topic")
        assert request.name == "test-exchange.name_123"

    def test_invalid_exchange_name_empty(self):
        """Test invalid empty exchange name (default exchange cannot be created, AC #7)."""
        with pytest.raises(
            ValueError, match="Exchange name cannot be empty.*default exchange"
        ):
            CreateExchangeRequest(name="", type="topic")

    def test_invalid_exchange_name_too_long(self):
        """Test invalid exchange name exceeding 255 characters (AC #7)."""
        long_name = "a" * 256
        with pytest.raises(
            ValueError, match="Exchange name exceeds maximum length of 255 characters"
        ):
            CreateExchangeRequest(name=long_name, type="topic")

    def test_valid_exchange_name_at_boundary(self):
        """Test valid exchange name at 255 character boundary."""
        boundary_name = "a" * 255
        request = CreateExchangeRequest(name=boundary_name, type="topic")
        assert request.name == boundary_name

    def test_invalid_exchange_name_system_exchange(self):
        """Test invalid system exchange with amq. prefix (AC #8)."""
        with pytest.raises(
            ValueError, match="Cannot create exchanges with 'amq.' prefix"
        ):
            CreateExchangeRequest(name="amq.test", type="topic")

    def test_invalid_exchange_name_special_chars(self):
        """Test invalid exchange name with disallowed special characters (AC #7)."""
        with pytest.raises(ValueError, match="contains invalid characters"):
            CreateExchangeRequest(name="test@exchange", type="topic")

    def test_valid_exchange_type_direct(self):
        """Test valid direct exchange type (AC #2)."""
        request = CreateExchangeRequest(name="events", type="direct")
        assert request.type == "direct"

    def test_valid_exchange_type_topic(self):
        """Test valid topic exchange type (AC #2)."""
        request = CreateExchangeRequest(name="events", type="topic")
        assert request.type == "topic"

    def test_valid_exchange_type_fanout(self):
        """Test valid fanout exchange type (AC #2)."""
        request = CreateExchangeRequest(name="events", type="fanout")
        assert request.type == "fanout"

    def test_valid_exchange_type_headers(self):
        """Test valid headers exchange type (AC #2)."""
        request = CreateExchangeRequest(name="events", type="headers")
        assert request.type == "headers"

    def test_invalid_exchange_type(self):
        """Test invalid exchange type (AC #2)."""
        from pydantic import ValidationError

        with pytest.raises(ValidationError):
            CreateExchangeRequest(name="events", type="invalid")  # type: ignore

    def test_valid_properties_defaults(self):
        """Test valid default properties (AC #3)."""
        request = CreateExchangeRequest(name="events", type="topic")
        assert request.durable is True
        assert request.auto_delete is False
        assert request.internal is False
        assert request.arguments is None

    def test_valid_properties_custom(self):
        """Test valid custom properties (AC #3)."""
        request = CreateExchangeRequest(
            name="events",
            type="topic",
            durable=False,
            auto_delete=True,
            internal=True,
        )
        assert request.durable is False
        assert request.auto_delete is True
        assert request.internal is True

    def test_valid_arguments_alternate_exchange(self):
        """Test valid arguments with alternate-exchange (AC #3)."""
        request = CreateExchangeRequest(
            name="events",
            type="topic",
            arguments={"alternate-exchange": "unroutable"},
        )
        assert request.arguments == {"alternate-exchange": "unroutable"}

    def test_valid_arguments_empty_dict(self):
        """Test valid empty arguments dict."""
        request = CreateExchangeRequest(name="events", type="topic", arguments={})
        assert request.arguments == {}


# ============================================================================
# Successful Create Tests (~8 tests)
# ============================================================================


class TestCreateExchangeSuccess:
    """Test successful exchange creation scenarios (AC #1, #2, #3, #4)."""

    @pytest.mark.asyncio
    async def test_create_direct_exchange(
        self, create_exchange_operation, mock_http_client
    ):
        """Test create direct exchange with minimal properties (AC #1, #2, #4)."""
        # Arrange
        request = CreateExchangeRequest(name="orders", type="direct")

        mock_http_client.request = AsyncMock(
            return_value=Mock(status_code=201, json=lambda: {})
        )

        # Mock GET to fetch exchange details
        exchange_data = {
            "name": "orders",
            "vhost": "/",
            "type": "direct",
            "durable": True,
            "auto_delete": False,
            "internal": False,
            "arguments": {},
        }

        mock_http_client.request.side_effect = [
            Mock(status_code=201, json=lambda: {}),  # PUT response
            Mock(status_code=200, json=lambda: exchange_data),  # GET response
        ]

        # Act
        result = await create_exchange_operation.execute(
            request, correlation_id="test-123"
        )

        # Assert
        assert isinstance(result, ExchangeInfo)
        assert result.name == "orders"
        assert result.type == "direct"
        assert result.durable is True
        assert mock_http_client.request.call_count == 2

    @pytest.mark.asyncio
    async def test_create_topic_exchange_with_durable(
        self, create_exchange_operation, mock_http_client
    ):
        """Test create topic exchange with durable=true (AC #1, #2, #3)."""
        # Arrange
        request = CreateExchangeRequest(
            name="events", type="topic", durable=True, vhost="/prod"
        )

        exchange_data = {
            "name": "events",
            "vhost": "/prod",
            "type": "topic",
            "durable": True,
            "auto_delete": False,
            "internal": False,
            "arguments": {},
        }

        mock_http_client.request = AsyncMock(
            side_effect=[
                Mock(status_code=201, json=lambda: {}),
                Mock(status_code=200, json=lambda: exchange_data),
            ]
        )

        # Act
        result = await create_exchange_operation.execute(request)

        # Assert
        assert result.name == "events"
        assert result.type == "topic"
        assert result.vhost == "/prod"
        assert result.durable is True

    @pytest.mark.asyncio
    async def test_create_fanout_exchange_with_auto_delete(
        self, create_exchange_operation, mock_http_client
    ):
        """Test create fanout exchange with auto_delete=true (AC #1, #2, #3)."""
        # Arrange
        request = CreateExchangeRequest(
            name="notifications", type="fanout", auto_delete=True
        )

        exchange_data = {
            "name": "notifications",
            "vhost": "/",
            "type": "fanout",
            "durable": True,
            "auto_delete": True,
            "internal": False,
            "arguments": {},
        }

        mock_http_client.request = AsyncMock(
            side_effect=[
                Mock(status_code=201, json=lambda: {}),
                Mock(status_code=200, json=lambda: exchange_data),
            ]
        )

        # Act
        result = await create_exchange_operation.execute(request)

        # Assert
        assert result.name == "notifications"
        assert result.type == "fanout"
        assert result.auto_delete is True

    @pytest.mark.asyncio
    async def test_create_headers_exchange_with_internal(
        self, create_exchange_operation, mock_http_client
    ):
        """Test create headers exchange with internal=true (AC #1, #2, #3)."""
        # Arrange
        request = CreateExchangeRequest(name="routing", type="headers", internal=True)

        exchange_data = {
            "name": "routing",
            "vhost": "/",
            "type": "headers",
            "durable": True,
            "auto_delete": False,
            "internal": True,
            "arguments": {},
        }

        mock_http_client.request = AsyncMock(
            side_effect=[
                Mock(status_code=201, json=lambda: {}),
                Mock(status_code=200, json=lambda: exchange_data),
            ]
        )

        # Act
        result = await create_exchange_operation.execute(request)

        # Assert
        assert result.name == "routing"
        assert result.type == "headers"
        assert result.internal is True

    @pytest.mark.asyncio
    async def test_create_with_alternate_exchange_argument(
        self, create_exchange_operation, mock_http_client
    ):
        """Test create exchange with alternate-exchange argument (AC #3)."""
        # Arrange
        request = CreateExchangeRequest(
            name="main",
            type="topic",
            arguments={"alternate-exchange": "unroutable"},
        )

        exchange_data = {
            "name": "main",
            "vhost": "/",
            "type": "topic",
            "durable": True,
            "auto_delete": False,
            "internal": False,
            "arguments": {"alternate-exchange": "unroutable"},
        }

        mock_http_client.request = AsyncMock(
            side_effect=[
                Mock(status_code=201, json=lambda: {}),
                Mock(status_code=200, json=lambda: exchange_data),
            ]
        )

        # Act
        result = await create_exchange_operation.execute(request)

        # Assert
        assert result.arguments == {"alternate-exchange": "unroutable"}

    @pytest.mark.asyncio
    async def test_create_with_custom_vhost_url_encoding(
        self, create_exchange_operation, mock_http_client
    ):
        """Test create exchange in custom vhost with URL encoding."""
        # Arrange
        request = CreateExchangeRequest(name="events", type="topic", vhost="/prod")

        exchange_data = {
            "name": "events",
            "vhost": "/prod",
            "type": "topic",
            "durable": True,
            "auto_delete": False,
            "internal": False,
            "arguments": {},
        }

        mock_http_client.request = AsyncMock(
            side_effect=[
                Mock(status_code=201, json=lambda: {}),
                Mock(status_code=200, json=lambda: exchange_data),
            ]
        )

        # Act
        result = await create_exchange_operation.execute(request)

        # Assert
        assert result.vhost == "/prod"
        # Verify URL encoding in call
        put_call = mock_http_client.request.call_args_list[0]
        assert "%2Fprod" in put_call[0][1]  # Vhost should be URL-encoded

    @pytest.mark.asyncio
    async def test_parse_exchange_info_response(
        self, create_exchange_operation, mock_http_client
    ):
        """Test parsing ExchangeInfo response correctly (AC #4)."""
        # Arrange
        request = CreateExchangeRequest(name="events", type="topic")

        exchange_data = {
            "name": "events",
            "vhost": "/",
            "type": "topic",
            "durable": True,
            "auto_delete": False,
            "internal": False,
            "arguments": {"alternate-exchange": "dlx"},
        }

        mock_http_client.request = AsyncMock(
            side_effect=[
                Mock(status_code=201, json=lambda: {}),
                Mock(status_code=200, json=lambda: exchange_data),
            ]
        )

        # Act
        result = await create_exchange_operation.execute(request)

        # Assert
        assert isinstance(result, ExchangeInfo)
        assert result.name == "events"
        assert result.type == "topic"
        assert result.arguments == {"alternate-exchange": "dlx"}

    @pytest.mark.asyncio
    async def test_log_successful_creation_with_correlation_id(
        self, create_exchange_operation, mock_http_client
    ):
        """Test logging successful creation with correlation_id."""
        # Arrange
        request = CreateExchangeRequest(name="events", type="topic")

        exchange_data = {
            "name": "events",
            "vhost": "/",
            "type": "topic",
            "durable": True,
            "auto_delete": False,
            "internal": False,
            "arguments": {},
        }

        mock_http_client.request = AsyncMock(
            side_effect=[
                Mock(status_code=201, json=lambda: {}),
                Mock(status_code=200, json=lambda: exchange_data),
            ]
        )

        # Act
        result = await create_exchange_operation.execute(
            request, correlation_id="test-456"
        )

        # Assert
        assert result is not None


# ============================================================================
# Idempotency Tests (~5 tests)
# ============================================================================


class TestCreateExchangeIdempotency:
    """Test idempotent exchange creation (AC #5)."""

    @pytest.mark.asyncio
    async def test_idempotent_create_same_properties(
        self, create_exchange_operation, mock_http_client
    ):
        """Test create same exchange twice with same properties returns success (AC #5)."""
        # Arrange
        request = CreateExchangeRequest(name="events", type="topic")

        exchange_data = {
            "name": "events",
            "vhost": "/",
            "type": "topic",
            "durable": True,
            "auto_delete": False,
            "internal": False,
            "arguments": {},
        }

        # 204 No Content indicates idempotent creation
        mock_http_client.request = AsyncMock(
            side_effect=[
                Mock(status_code=204, json=lambda: {}),  # PUT returns 204
                Mock(status_code=200, json=lambda: exchange_data),  # GET details
            ]
        )

        # Act
        result = await create_exchange_operation.execute(request)

        # Assert
        assert isinstance(result, ExchangeInfo)
        assert result.name == "events"
        assert result.type == "topic"

    @pytest.mark.asyncio
    async def test_idempotent_no_error_raised(
        self, create_exchange_operation, mock_http_client
    ):
        """Test idempotent creation does not raise error (AC #5)."""
        # Arrange
        request = CreateExchangeRequest(name="events", type="topic")

        exchange_data = {
            "name": "events",
            "vhost": "/",
            "type": "topic",
            "durable": True,
            "auto_delete": False,
            "internal": False,
            "arguments": {},
        }

        mock_http_client.request = AsyncMock(
            side_effect=[
                Mock(status_code=204),
                Mock(status_code=200, json=lambda: exchange_data),
            ]
        )

        # Act - should not raise
        result = await create_exchange_operation.execute(request)

        # Assert
        assert result is not None

    @pytest.mark.asyncio
    async def test_idempotent_returns_exchange_info(
        self, create_exchange_operation, mock_http_client
    ):
        """Test idempotent creation returns ExchangeInfo."""
        # Arrange
        request = CreateExchangeRequest(name="events", type="topic")

        exchange_data = {
            "name": "events",
            "vhost": "/",
            "type": "topic",
            "durable": True,
            "auto_delete": False,
            "internal": False,
            "arguments": {},
        }

        mock_http_client.request = AsyncMock(
            side_effect=[
                Mock(status_code=204),
                Mock(status_code=200, json=lambda: exchange_data),
            ]
        )

        # Act
        result = await create_exchange_operation.execute(request)

        # Assert
        assert isinstance(result, ExchangeInfo)

    @pytest.mark.asyncio
    async def test_multiple_idempotent_creates_succeed(
        self, create_exchange_operation, mock_http_client
    ):
        """Test multiple idempotent creates do not fail."""
        # Arrange
        request = CreateExchangeRequest(name="events", type="topic")

        exchange_data = {
            "name": "events",
            "vhost": "/",
            "type": "topic",
            "durable": True,
            "auto_delete": False,
            "internal": False,
            "arguments": {},
        }

        mock_http_client.request = AsyncMock(
            side_effect=[
                Mock(status_code=204),
                Mock(status_code=200, json=lambda: exchange_data),
                Mock(status_code=204),
                Mock(status_code=200, json=lambda: exchange_data),
            ]
        )

        # Act - call twice
        result1 = await create_exchange_operation.execute(request)
        result2 = await create_exchange_operation.execute(request)

        # Assert
        assert result1 is not None
        assert result2 is not None


# ============================================================================
# Conflict Detection Tests (~5 tests)
# ============================================================================


class TestCreateExchangeConflictDetection:
    """Test conflict detection for existing exchanges (AC #6)."""

    @pytest.mark.asyncio
    async def test_conflict_different_type(
        self, create_exchange_operation, mock_http_client
    ):
        """Test create exchange with different type raises ValueError (AC #6)."""
        # Arrange
        request = CreateExchangeRequest(name="events", type="topic")

        # 409 Conflict response
        mock_http_client.request = AsyncMock(
            return_value=Mock(
                status_code=409,
                json=lambda: {
                    "reason": "Exchange 'events' exists with type 'direct', cannot change to 'topic'"
                },
            )
        )

        # Act & Assert
        with pytest.raises(ValueError, match="already exists with a different type"):
            await create_exchange_operation.execute(request)

    @pytest.mark.asyncio
    async def test_conflict_parse_error_response(
        self, create_exchange_operation, mock_http_client
    ):
        """Test conflict detection parses error response body."""
        # Arrange
        request = CreateExchangeRequest(name="events", type="topic")

        mock_http_client.request = AsyncMock(
            return_value=Mock(
                status_code=409,
                json=lambda: {
                    "reason": "type mismatch: existing=direct, requested=topic"
                },
            )
        )

        # Act & Assert
        with pytest.raises(ValueError) as exc_info:
            await create_exchange_operation.execute(request)

        assert "different type" in str(exc_info.value)

    @pytest.mark.asyncio
    async def test_conflict_user_friendly_message(
        self, create_exchange_operation, mock_http_client
    ):
        """Test conflict error has user-friendly message (AC #6)."""
        # Arrange
        request = CreateExchangeRequest(name="events", type="topic")

        mock_http_client.request = AsyncMock(
            return_value=Mock(
                status_code=409,
                json=lambda: {"reason": "Exchange type mismatch"},
            )
        )

        # Act & Assert
        with pytest.raises(ValueError) as exc_info:
            await create_exchange_operation.execute(request)

        error_msg = str(exc_info.value)
        assert "events" in error_msg
        assert "different" in error_msg.lower()


# ============================================================================
# Error Handling Tests (~7 tests)
# ============================================================================


class TestCreateExchangeErrorHandling:
    """Test error handling scenarios."""

    @pytest.mark.asyncio
    async def test_vhost_not_found_404(
        self, create_exchange_operation, mock_http_client
    ):
        """Test vhost not found returns ValueError."""
        # Arrange
        request = CreateExchangeRequest(
            name="events", type="topic", vhost="/nonexistent"
        )

        mock_http_client.request = AsyncMock(return_value=Mock(status_code=404))

        # Act & Assert
        with pytest.raises(
            ValueError, match="Virtual host '/nonexistent' does not exist"
        ):
            await create_exchange_operation.execute(request)

    @pytest.mark.asyncio
    async def test_timeout_exception(self, create_exchange_operation, mock_http_client):
        """Test timeout during create raises TimeoutException."""
        # Arrange
        request = CreateExchangeRequest(name="events", type="topic")

        import httpx

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

        # Act & Assert
        with pytest.raises(httpx.TimeoutException):
            await create_exchange_operation.execute(request)

    @pytest.mark.asyncio
    async def test_verification_failed_after_creation(
        self, create_exchange_operation, mock_http_client
    ):
        """Test verification failure after successful creation."""
        # Arrange
        request = CreateExchangeRequest(name="events", type="topic")

        mock_http_client.request = AsyncMock(
            side_effect=[
                Mock(status_code=201),  # PUT succeeds
                Mock(status_code=404),  # GET fails (verification)
            ]
        )

        # Act & Assert
        with pytest.raises(ValueError, match="created but verification failed"):
            await create_exchange_operation.execute(request)


# ============================================================================
# Performance Monitoring Tests (~3 tests)
# ============================================================================


class TestCreateExchangePerformanceMonitoring:
    """Test performance monitoring (AC #9)."""

    @pytest.mark.asyncio
    async def test_no_warning_fast_operation(
        self, create_exchange_operation, mock_http_client
    ):
        """Test no warning if duration < 100ms."""
        # Arrange
        request = CreateExchangeRequest(name="events", type="topic")

        exchange_data = {
            "name": "events",
            "vhost": "/",
            "type": "topic",
            "durable": True,
            "auto_delete": False,
            "internal": False,
            "arguments": {},
        }

        mock_http_client.request = AsyncMock(
            side_effect=[
                Mock(status_code=201),
                Mock(status_code=200, json=lambda: exchange_data),
            ]
        )

        # Act - should complete quickly (no actual delay in mock)
        result = await create_exchange_operation.execute(request)

        # Assert
        assert result is not None
