"""Unit tests for topology schemas (Story 3.1 Task 1, Story 3.2 Task 1, Story 3.3 Task 1)."""

import pytest
from pydantic import ValidationError

from rabbitmq_mcp_connection.schemas.topology import (
    BindingInfo,
    CreateBindingRequest,
    CreateExchangeRequest,
    CreateQueueRequest,
    DeleteBindingRequest,
    DeleteBindingResponse,
    DeleteExchangeRequest,
    DeleteExchangeResponse,
    DeleteQueueRequest,
    ExchangeInfo,
    ListBindingsRequest,
    ListExchangesRequest,
    MessageStats,
    PurgeQueueRequest,
    QueueDetails,
    format_memory,
)


class TestQueueDetails:
    """Test QueueDetails schema validation (AC #1, #2, #3)."""

    def test_valid_queue_details(self):
        """Test QueueDetails with all valid fields."""
        queue = QueueDetails(
            name="test-queue",
            vhost="/",
            durable=True,
            auto_delete=False,
            arguments={"x-max-length": 1000},
            messages=100,
            messages_ready=50,
            messages_unacknowledged=50,
            consumers=2,
            memory=1048576,
            state="running",
        )

        assert queue.name == "test-queue"
        assert queue.vhost == "/"
        assert queue.durable is True
        assert queue.auto_delete is False
        assert queue.arguments == {"x-max-length": 1000}
        assert queue.messages == 100
        assert queue.messages_ready == 50
        assert queue.messages_unacknowledged == 50
        assert queue.consumers == 2
        assert queue.memory == 1048576
        assert queue.state == "running"

    def test_queue_details_defaults(self):
        """Test QueueDetails with default values for statistics."""
        queue = QueueDetails(
            name="minimal-queue",
            vhost="/",
            durable=False,
            auto_delete=True,
        )

        assert queue.messages == 0
        assert queue.messages_ready == 0
        assert queue.messages_unacknowledged == 0
        assert queue.consumers == 0
        assert queue.memory == 0
        assert queue.state == "idle"
        assert queue.arguments == {}

    @pytest.mark.parametrize(
        "field,value",
        [
            ("messages", -1),
            ("messages_ready", -10),
            ("messages_unacknowledged", -5),
            ("consumers", -2),
            ("memory", -1024),
        ],
    )
    def test_negative_values_rejected(self, field, value):
        """Test non-negative integer validation (AC #1)."""
        data = {
            "name": "test-queue",
            "vhost": "/",
            "durable": True,
            "auto_delete": False,
            field: value,
        }

        with pytest.raises(ValidationError) as exc_info:
            QueueDetails(**data)

        errors = exc_info.value.errors()
        assert len(errors) >= 1
        assert any(field in str(err) for err in errors)

    @pytest.mark.parametrize("state", ["running", "idle", "flow"])
    def test_valid_states(self, state):
        """Test all valid queue states (AC #3)."""
        queue = QueueDetails(
            name="test-queue",
            vhost="/",
            durable=True,
            auto_delete=False,
            state=state,
        )

        assert queue.state == state

    def test_invalid_state_rejected(self):
        """Test invalid state is rejected (AC #3)."""
        with pytest.raises(ValidationError):
            QueueDetails(
                name="test-queue",
                vhost="/",
                durable=True,
                auto_delete=False,
                state="invalid-state",
            )

    def test_memory_formatted_property(self):
        """Test memory_formatted computed property (AC #2, Task 6)."""
        queue = QueueDetails(
            name="test-queue",
            vhost="/",
            durable=True,
            auto_delete=False,
            memory=1048576,  # 1 MB
        )

        assert queue.memory_formatted == "1.00 MB"

    def test_message_rates_optional(self):
        """Test message_rates is optional."""
        queue = QueueDetails(
            name="test-queue",
            vhost="/",
            durable=True,
            auto_delete=False,
        )

        assert queue.message_rates is None

        queue_with_rates = QueueDetails(
            name="test-queue",
            vhost="/",
            durable=True,
            auto_delete=False,
            message_rates={"publish": 10.5, "deliver": 8.2, "ack": 7.8},
        )

        assert queue_with_rates.message_rates == {
            "publish": 10.5,
            "deliver": 8.2,
            "ack": 7.8,
        }

    def test_extra_fields_ignored(self):
        """Test extra fields are ignored per model_config."""
        queue = QueueDetails(
            name="test-queue",
            vhost="/",
            durable=True,
            auto_delete=False,
            unknown_field="should be ignored",
        )

        assert not hasattr(queue, "unknown_field")


class TestFormatMemory:
    """Test format_memory utility function (Task 6)."""

    @pytest.mark.parametrize(
        "bytes_val,expected",
        [
            (0, "0 B"),
            (512, "512 B"),
            (1023, "1023 B"),
            (1024, "1.00 KB"),
            (1536, "1.50 KB"),
            (1048576, "1.00 MB"),
            (1572864, "1.50 MB"),
            (1073741824, "1.00 GB"),
            (1610612736, "1.50 GB"),
            (5368709120, "5.00 GB"),
        ],
    )
    def test_format_memory_units(self, bytes_val, expected):
        """Test memory formatting with various byte values (AC #2)."""
        result = format_memory(bytes_val)
        assert result == expected

    def test_format_memory_precision(self):
        """Test memory formatting maintains 2 decimal places."""
        result = format_memory(1234567)  # ~1.18 MB
        assert result == "1.18 MB"

        result = format_memory(123456789)  # ~117.74 MB
        assert result == "117.74 MB"

    def test_format_memory_edge_cases(self):
        """Test format_memory with edge cases."""
        # Exactly at boundary
        assert format_memory(1024) == "1.00 KB"
        assert format_memory(1024**2) == "1.00 MB"
        assert format_memory(1024**3) == "1.00 GB"

        # Just below boundary
        assert format_memory(1023) == "1023 B"
        assert format_memory(1024**2 - 1) == "1024.00 KB"
        assert format_memory(1024**3 - 1) == "1024.00 MB"


class TestCreateQueueRequest:
    """Test CreateQueueRequest schema validation (Story 3.2 AC #1, #2, #6, #7)."""

    def test_valid_queue_creation_minimal(self):
        """Test CreateQueueRequest with minimal valid fields."""
        request = CreateQueueRequest(name="test-queue")

        assert request.name == "test-queue"
        assert request.vhost == "/"  # Default
        assert request.durable is True  # Default
        assert request.auto_delete is False  # Default
        assert request.arguments is None

    def test_valid_queue_creation_all_fields(self):
        """Test CreateQueueRequest with all fields."""
        request = CreateQueueRequest(
            name="orders-queue",
            vhost="/prod",
            durable=False,
            auto_delete=True,
            arguments={"x-message-ttl": 60000, "x-max-length": 1000},
        )

        assert request.name == "orders-queue"
        assert request.vhost == "/prod"
        assert request.durable is False
        assert request.auto_delete is True
        assert request.arguments == {"x-message-ttl": 60000, "x-max-length": 1000}

    @pytest.mark.parametrize(
        "name",
        [
            "simple",
            "with-hyphen",
            "with_underscore",
            "with.period",
            "CamelCase",
            "mix-ALL_types.123",
            "a" * 255,  # Max length
        ],
    )
    def test_valid_queue_names(self, name):
        """Test valid queue names (AC #7)."""
        request = CreateQueueRequest(name=name)
        assert request.name == name

    @pytest.mark.parametrize(
        "name,expected_error",
        [
            ("", "empty"),
            ("with space", "invalid characters"),
            ("with$dollar", "invalid characters"),
            ("with@at", "invalid characters"),
            ("with/slash", "invalid characters"),
            ("with#hash", "invalid characters"),
            ("a" * 256, "exceeds 255 characters"),  # Too long
        ],
    )
    def test_invalid_queue_names(self, name, expected_error):
        """Test invalid queue names are rejected (AC #6, #7)."""
        with pytest.raises(ValidationError) as exc_info:
            CreateQueueRequest(name=name)

        error_msg = str(exc_info.value)
        assert expected_error.lower() in error_msg.lower()

    def test_queue_with_ttl_argument(self):
        """Test queue with x-message-ttl argument (AC #2)."""
        request = CreateQueueRequest(
            name="ttl-queue",
            arguments={"x-message-ttl": 60000},  # 60 seconds
        )

        assert request.arguments == {"x-message-ttl": 60000}

    def test_queue_with_max_length_argument(self):
        """Test queue with x-max-length argument (AC #2)."""
        request = CreateQueueRequest(
            name="bounded-queue",
            arguments={"x-max-length": 1000},
        )

        assert request.arguments == {"x-max-length": 1000}

    def test_queue_with_dlx_argument(self):
        """Test queue with x-dead-letter-exchange argument (AC #2)."""
        request = CreateQueueRequest(
            name="dlx-queue",
            arguments={"x-dead-letter-exchange": "dlx-exchange"},
        )

        assert request.arguments == {"x-dead-letter-exchange": "dlx-exchange"}

    @pytest.mark.parametrize("queue_type", ["classic", "quorum"])
    def test_queue_with_queue_type_argument(self, queue_type):
        """Test queue with x-queue-type argument (AC #2, Task 5)."""
        request = CreateQueueRequest(
            name="typed-queue",
            arguments={"x-queue-type": queue_type},
        )

        assert request.arguments == {"x-queue-type": queue_type}

    def test_invalid_ttl_negative(self):
        """Test x-message-ttl validation rejects negative values (Task 5)."""
        with pytest.raises(ValidationError) as exc_info:
            CreateQueueRequest(
                name="test-queue",
                arguments={"x-message-ttl": -1000},
            )

        error_msg = str(exc_info.value)
        assert "positive integer" in error_msg.lower()

    def test_invalid_ttl_zero(self):
        """Test x-message-ttl validation rejects zero (Task 5)."""
        with pytest.raises(ValidationError) as exc_info:
            CreateQueueRequest(
                name="test-queue",
                arguments={"x-message-ttl": 0},
            )

        error_msg = str(exc_info.value)
        assert "positive integer" in error_msg.lower()

    def test_invalid_ttl_non_integer(self):
        """Test x-message-ttl validation rejects non-integers (Task 5)."""
        with pytest.raises(ValidationError) as exc_info:
            CreateQueueRequest(
                name="test-queue",
                arguments={"x-message-ttl": "60000"},  # String not int
            )

        error_msg = str(exc_info.value)
        assert "positive integer" in error_msg.lower()

    def test_invalid_max_length_negative(self):
        """Test x-max-length validation rejects negative values (Task 5)."""
        with pytest.raises(ValidationError) as exc_info:
            CreateQueueRequest(
                name="test-queue",
                arguments={"x-max-length": -100},
            )

        error_msg = str(exc_info.value)
        assert "positive integer" in error_msg.lower()

    def test_invalid_max_length_zero(self):
        """Test x-max-length validation rejects zero (Task 5)."""
        with pytest.raises(ValidationError) as exc_info:
            CreateQueueRequest(
                name="test-queue",
                arguments={"x-max-length": 0},
            )

        error_msg = str(exc_info.value)
        assert "positive integer" in error_msg.lower()

    def test_invalid_queue_type(self):
        """Test x-queue-type validation rejects invalid types (Task 5)."""
        with pytest.raises(ValidationError) as exc_info:
            CreateQueueRequest(
                name="test-queue",
                arguments={"x-queue-type": "invalid"},
            )

        error_msg = str(exc_info.value)
        assert "classic" in error_msg.lower() or "quorum" in error_msg.lower()

    def test_invalid_dlx_empty_string(self):
        """Test x-dead-letter-exchange validation rejects empty string (Task 5)."""
        with pytest.raises(ValidationError) as exc_info:
            CreateQueueRequest(
                name="test-queue",
                arguments={"x-dead-letter-exchange": ""},
            )

        error_msg = str(exc_info.value)
        assert "non-empty string" in error_msg.lower()

    def test_invalid_dlx_non_string(self):
        """Test x-dead-letter-exchange validation rejects non-strings (Task 5)."""
        with pytest.raises(ValidationError) as exc_info:
            CreateQueueRequest(
                name="test-queue",
                arguments={"x-dead-letter-exchange": 123},  # Number not string
            )

        error_msg = str(exc_info.value)
        assert "non-empty string" in error_msg.lower()

    def test_multiple_arguments_valid(self):
        """Test queue with multiple valid arguments (AC #2)."""
        request = CreateQueueRequest(
            name="complex-queue",
            arguments={
                "x-message-ttl": 60000,
                "x-max-length": 1000,
                "x-dead-letter-exchange": "dlx",
                "x-queue-type": "quorum",
            },
        )

        assert request.arguments == {
            "x-message-ttl": 60000,
            "x-max-length": 1000,
            "x-dead-letter-exchange": "dlx",
            "x-queue-type": "quorum",
        }

    def test_extra_arguments_allowed(self):
        """Test unvalidated arguments are passed through (future extensibility)."""
        request = CreateQueueRequest(
            name="test-queue",
            arguments={
                "x-message-ttl": 60000,
                "x-custom-arg": "custom-value",  # Unknown argument
            },
        )

        assert request.arguments == {
            "x-message-ttl": 60000,
            "x-custom-arg": "custom-value",
        }

    def test_extra_fields_ignored(self):
        """Test extra fields are ignored per model_config."""
        request = CreateQueueRequest(
            name="test-queue",
            unknown_field="should be ignored",
        )

        assert not hasattr(request, "unknown_field")


class TestDeleteQueueRequest:
    """Test DeleteQueueRequest schema validation (Story 3.3, AC #1, #2, #5)."""

    def test_valid_delete_request_minimal(self):
        """Test DeleteQueueRequest with minimal required fields."""
        request = DeleteQueueRequest(name="test-queue")

        assert request.name == "test-queue"
        assert request.vhost == "/"
        assert request.force is False

    def test_valid_delete_request_all_fields(self):
        """Test DeleteQueueRequest with all fields specified."""
        request = DeleteQueueRequest(name="orders", vhost="/prod", force=True)

        assert request.name == "orders"
        assert request.vhost == "/prod"
        assert request.force is True

    def test_valid_queue_name_alphanumeric(self):
        """Test valid queue name with alphanumeric characters."""
        request = DeleteQueueRequest(name="queue123")
        assert request.name == "queue123"

    def test_valid_queue_name_with_hyphen(self):
        """Test valid queue name with hyphens."""
        request = DeleteQueueRequest(name="my-queue-name")
        assert request.name == "my-queue-name"

    def test_valid_queue_name_with_underscore(self):
        """Test valid queue name with underscores."""
        request = DeleteQueueRequest(name="my_queue_name")
        assert request.name == "my_queue_name"

    def test_valid_queue_name_with_period(self):
        """Test valid queue name with periods."""
        request = DeleteQueueRequest(name="my.queue.name")
        assert request.name == "my.queue.name"

    def test_valid_queue_name_max_length(self):
        """Test valid queue name at maximum length (255 chars)."""
        name = "a" * 255
        request = DeleteQueueRequest(name=name)
        assert request.name == name

    def test_invalid_queue_name_empty(self):
        """Test empty queue name raises ValueError."""
        with pytest.raises(ValidationError) as exc_info:
            DeleteQueueRequest(name="")

        errors = exc_info.value.errors()
        assert len(errors) == 1
        assert "Queue name cannot be empty" in str(errors[0]["ctx"]["error"])

    def test_invalid_queue_name_too_long(self):
        """Test queue name exceeding 255 characters raises ValueError."""
        name = "a" * 256
        with pytest.raises(ValidationError) as exc_info:
            DeleteQueueRequest(name=name)

        errors = exc_info.value.errors()
        assert len(errors) == 1
        assert "exceeds 255 characters" in str(errors[0]["ctx"]["error"])

    def test_invalid_queue_name_special_chars(self):
        """Test queue name with invalid special characters raises ValueError."""
        invalid_names = ["queue$name", "queue@host", "queue#tag", "queue name"]

        for name in invalid_names:
            with pytest.raises(ValidationError) as exc_info:
                DeleteQueueRequest(name=name)

            errors = exc_info.value.errors()
            assert len(errors) == 1
            assert "invalid characters" in str(errors[0]["ctx"]["error"])

    def test_force_flag_default_false(self):
        """Test force flag defaults to False."""
        request = DeleteQueueRequest(name="test-queue")
        assert request.force is False

    def test_force_flag_explicit_false(self):
        """Test force flag can be explicitly set to False."""
        request = DeleteQueueRequest(name="test-queue", force=False)
        assert request.force is False

    def test_force_flag_explicit_true(self):
        """Test force flag can be set to True."""
        request = DeleteQueueRequest(name="test-queue", force=True)
        assert request.force is True

    def test_vhost_default(self):
        """Test vhost defaults to root '/'."""
        request = DeleteQueueRequest(name="test-queue")
        assert request.vhost == "/"

    def test_vhost_custom(self):
        """Test custom vhost can be specified."""
        request = DeleteQueueRequest(name="test-queue", vhost="/prod")
        assert request.vhost == "/prod"

    def test_extra_fields_ignored(self):
        """Test extra fields are ignored per model_config."""
        request = DeleteQueueRequest(
            name="test-queue",
            unknown_field="should be ignored",
        )

        assert not hasattr(request, "unknown_field")


class TestPurgeQueueRequest:
    """Test PurgeQueueRequest schema validation (Story 3.4 Task 1, AC #1, #7)."""

    def test_valid_purge_request_default_vhost(self):
        """Test PurgeQueueRequest with valid name and default vhost."""
        request = PurgeQueueRequest(name="test-queue")

        assert request.name == "test-queue"
        assert request.vhost == "/"

    def test_valid_purge_request_custom_vhost(self):
        """Test PurgeQueueRequest with custom vhost."""
        request = PurgeQueueRequest(name="orders", vhost="/prod")

        assert request.name == "orders"
        assert request.vhost == "/prod"

    @pytest.mark.parametrize(
        "name",
        [
            "simple-queue",
            "queue_with_underscores",
            "queue.with.periods",
            "queue-123",
            "UPPERCASE",
            "MixedCase123",
            "a",  # Single character
            "a" * 255,  # Exactly 255 characters
        ],
    )
    def test_valid_queue_names(self, name):
        """Test various valid queue name formats."""
        request = PurgeQueueRequest(name=name)
        assert request.name == name

    def test_empty_queue_name_raises_error(self):
        """Test empty queue name raises ValueError."""
        with pytest.raises(ValidationError) as exc_info:
            PurgeQueueRequest(name="")

        errors = exc_info.value.errors()
        assert len(errors) == 1
        assert "cannot be empty" in str(errors[0]["ctx"]["error"])

    def test_queue_name_too_long_raises_error(self):
        """Test queue name exceeding 255 characters raises ValueError."""
        long_name = "a" * 256

        with pytest.raises(ValidationError) as exc_info:
            PurgeQueueRequest(name=long_name)

        errors = exc_info.value.errors()
        assert len(errors) == 1
        assert "exceeds 255 characters" in str(errors[0]["ctx"]["error"])

    @pytest.mark.parametrize(
        "invalid_name",
        [
            "queue$name",
            "queue@host",
            "queue#tag",
            "queue name",  # Space
            "queue/path",
            "queue\\path",
            "queue:port",
            "queue*wildcard",
        ],
    )
    def test_invalid_queue_name_special_chars(self, invalid_name):
        """Test queue name with invalid special characters raises ValueError."""
        with pytest.raises(ValidationError) as exc_info:
            PurgeQueueRequest(name=invalid_name)

        errors = exc_info.value.errors()
        assert len(errors) == 1
        assert "invalid characters" in str(errors[0]["ctx"]["error"])

    def test_vhost_variations(self):
        """Test various vhost values are accepted."""
        test_cases = [
            ("/", "/"),
            ("/prod", "/prod"),
            ("/staging", "/staging"),
            ("/test-env", "/test-env"),
        ]

        for vhost_input, expected_vhost in test_cases:
            request = PurgeQueueRequest(name="test-queue", vhost=vhost_input)
            assert request.vhost == expected_vhost

    def test_extra_fields_ignored(self):
        """Test extra fields are ignored per model_config."""
        request = PurgeQueueRequest(
            name="test-queue",
            unknown_field="should be ignored",
        )

        assert not hasattr(request, "unknown_field")

    def test_purge_request_immutable_after_creation(self):
        """Test PurgeQueueRequest fields are set correctly."""
        request = PurgeQueueRequest(name="orders", vhost="/prod")

        # Verify fields are accessible
        assert request.name == "orders"
        assert request.vhost == "/prod"

        # Pydantic v2 models are not frozen by default, but fields are validated
        request.name = "new-name"
        assert request.name == "new-name"


# =====================================================================
# MessageStats Schema Tests (Story 3.5, Task 2)
# =====================================================================


class TestMessageStatsSchema:
    """Test MessageStats schema for exchanges (Story 3.5, AC #3)."""

    def test_message_stats_with_all_fields(self):
        """Test MessageStats with all optional fields."""
        stats = MessageStats(publish_in=10.5, publish_out=9.8)
        assert stats.publish_in == 10.5
        assert stats.publish_out == 9.8

    def test_message_stats_with_none_values(self):
        """Test MessageStats with None values (optional fields)."""
        stats = MessageStats()
        assert stats.publish_in is None
        assert stats.publish_out is None

    def test_message_stats_extra_fields_ignored(self):
        """Test MessageStats ignores extra fields (forward compatibility)."""
        stats = MessageStats(**{"publish_in": 5.0, "unknown_field": "ignored"})
        assert stats.publish_in == 5.0
        assert not hasattr(stats, "unknown_field")


# =====================================================================
# ExchangeInfo Schema Tests (Story 3.5, Task 2)
# =====================================================================


class TestExchangeInfoSchema:
    """Test ExchangeInfo schema validation (Story 3.5, AC #2, #3)."""

    def test_valid_direct_exchange(self):
        """Test valid direct exchange."""
        exchange = ExchangeInfo(
            name="orders",
            vhost="/",
            type="direct",
            durable=True,
            auto_delete=False,
            internal=False,
            arguments={},
        )
        assert exchange.name == "orders"
        assert exchange.type == "direct"
        assert exchange.durable is True

    def test_valid_topic_exchange(self):
        """Test valid topic exchange."""
        exchange = ExchangeInfo(
            name="logs",
            vhost="/",
            type="topic",
            durable=True,
            auto_delete=False,
            internal=False,
            arguments={},
        )
        assert exchange.type == "topic"

    def test_valid_fanout_exchange(self):
        """Test valid fanout exchange."""
        exchange = ExchangeInfo(
            name="broadcast",
            vhost="/",
            type="fanout",
            durable=False,
            auto_delete=True,
            internal=False,
            arguments={},
        )
        assert exchange.type == "fanout"
        assert exchange.auto_delete is True

    def test_valid_headers_exchange(self):
        """Test valid headers exchange."""
        exchange = ExchangeInfo(
            name="routing",
            vhost="/",
            type="headers",
            durable=True,
            auto_delete=False,
            internal=False,
            arguments={"x-match": "all"},
        )
        assert exchange.type == "headers"
        assert exchange.arguments == {"x-match": "all"}

    def test_system_exchange_amq_direct(self):
        """Test system exchange (amq.direct)."""
        exchange = ExchangeInfo(
            name="amq.direct",
            vhost="/",
            type="direct",
            durable=True,
            auto_delete=False,
            internal=False,
            arguments={},
        )
        assert exchange.name == "amq.direct"
        assert exchange.name.startswith("amq.")

    def test_default_exchange_empty_name(self):
        """Test default exchange with empty string name (AC #1)."""
        exchange = ExchangeInfo(
            name="",
            vhost="/",
            type="direct",
            durable=True,
            auto_delete=False,
            internal=False,
            arguments={},
        )
        assert exchange.name == ""

    def test_internal_exchange(self):
        """Test internal exchange flag."""
        exchange = ExchangeInfo(
            name="internal-ex",
            vhost="/",
            type="direct",
            durable=True,
            auto_delete=False,
            internal=True,
            arguments={},
        )
        assert exchange.internal is True

    def test_exchange_with_message_stats(self):
        """Test exchange with message_stats (AC #3)."""
        exchange = ExchangeInfo(
            name="active",
            vhost="/",
            type="direct",
            durable=True,
            auto_delete=False,
            internal=False,
            arguments={},
            message_stats=MessageStats(publish_in=15.2, publish_out=14.8),
        )
        assert exchange.message_stats is not None
        assert exchange.message_stats.publish_in == 15.2

    def test_exchange_without_message_stats(self):
        """Test exchange without message_stats (optional, AC #3)."""
        exchange = ExchangeInfo(
            name="idle",
            vhost="/",
            type="direct",
            durable=True,
            auto_delete=False,
            internal=False,
            arguments={},
        )
        assert exchange.message_stats is None

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

        with pytest.raises(ValidationError):
            ExchangeInfo(
                name="test",
                vhost="/",
                type="invalid",
                durable=True,
                auto_delete=False,
                internal=False,
                arguments={},
            )

    def test_exchange_info_extra_fields_ignored(self):
        """Test ExchangeInfo ignores extra fields (forward compatibility)."""
        exchange = ExchangeInfo(
            **{
                "name": "test",
                "vhost": "/",
                "type": "direct",
                "durable": True,
                "auto_delete": False,
                "internal": False,
                "arguments": {},
                "future_field": "ignored",
            }
        )
        assert exchange.name == "test"
        assert not hasattr(exchange, "future_field")


# =====================================================================
# ListExchangesRequest Schema Tests (Story 3.5, Task 1)
# =====================================================================


class TestListExchangesRequestSchema:
    """Test ListExchangesRequest schema validation (Story 3.5, AC #1, #6, #7)."""

    def test_default_vhost_and_sort(self):
        """Test default vhost is '/' and sort_by is 'name'."""
        request = ListExchangesRequest()
        assert request.vhost == "/"
        assert request.sort_by == "name"
        assert request.name_pattern is None
        assert request.type is None

    def test_custom_vhost(self):
        """Test custom vhost."""
        request = ListExchangesRequest(vhost="/prod")
        assert request.vhost == "/prod"

    def test_empty_vhost_raises_error(self):
        """Test empty vhost raises ValidationError (AC #6)."""
        with pytest.raises(ValueError, match="Virtual host name cannot be empty"):
            ListExchangesRequest(vhost="")

    def test_name_pattern_filter(self):
        """Test name_pattern filter (AC #7)."""
        request = ListExchangesRequest(name_pattern="^amq\\..*")
        assert request.name_pattern == "^amq\\..*"

    def test_type_filter_direct(self):
        """Test type filter with 'direct' (AC #7)."""
        request = ListExchangesRequest(type="direct")
        assert request.type == "direct"

    def test_type_filter_topic(self):
        """Test type filter with 'topic' (AC #7)."""
        request = ListExchangesRequest(type="topic")
        assert request.type == "topic"

    def test_type_filter_fanout(self):
        """Test type filter with 'fanout' (AC #7)."""
        request = ListExchangesRequest(type="fanout")
        assert request.type == "fanout"

    def test_type_filter_headers(self):
        """Test type filter with 'headers' (AC #7)."""
        request = ListExchangesRequest(type="headers")
        assert request.type == "headers"

    def test_invalid_type_filter(self):
        """Test invalid type filter raises ValidationError."""
        with pytest.raises(ValueError):
            ListExchangesRequest(type="invalid")

    def test_sort_by_name(self):
        """Test sort_by='name' (AC #8)."""
        request = ListExchangesRequest(sort_by="name")
        assert request.sort_by == "name"

    def test_sort_by_type(self):
        """Test sort_by='type' (AC #8)."""
        request = ListExchangesRequest(sort_by="type")
        assert request.sort_by == "type"

    def test_sort_by_message_in_rate(self):
        """Test sort_by='message_in_rate' (AC #8)."""
        request = ListExchangesRequest(sort_by="message_in_rate")
        assert request.sort_by == "message_in_rate"

    def test_invalid_sort_by(self):
        """Test invalid sort_by raises ValidationError."""
        with pytest.raises(ValueError):
            ListExchangesRequest(sort_by="invalid_field")

    def test_combined_filters_and_sort(self):
        """Test combining all filters and sort options."""
        request = ListExchangesRequest(
            vhost="/prod",
            name_pattern="^orders",
            type="topic",
            sort_by="message_in_rate",
        )
        assert request.vhost == "/prod"
        assert request.name_pattern == "^orders"
        assert request.type == "topic"
        assert request.sort_by == "message_in_rate"

    def test_extra_fields_ignored(self):
        """Test extra fields ignored (forward compatibility)."""
        request = ListExchangesRequest(**{"vhost": "/", "unknown": "ignored"})
        assert request.vhost == "/"
        assert not hasattr(request, "unknown")


class TestCreateExchangeRequest:
    """Test CreateExchangeRequest schema validation (Story 3.6, AC #1-8)."""

    def test_minimal_valid_request(self):
        """Test minimal valid request with required fields."""
        request = CreateExchangeRequest(name="events", type="topic")
        assert request.name == "events"
        assert request.type == "topic"
        assert request.vhost == "/"
        assert request.durable is True
        assert request.auto_delete is False
        assert request.internal is False
        assert request.arguments is None

    def test_vhost_default(self):
        """Test vhost defaults to '/' (AC #1)."""
        request = CreateExchangeRequest(name="events", type="topic")
        assert request.vhost == "/"

    def test_vhost_custom(self):
        """Test custom vhost (AC #1)."""
        request = CreateExchangeRequest(name="events", type="topic", vhost="/prod")
        assert request.vhost == "/prod"

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

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

    def test_exchange_name_valid_with_special_chars(self):
        """Test valid exchange name with hyphen, underscore, period (AC #7)."""
        request = CreateExchangeRequest(name="test-exchange.name_123", type="topic")
        assert request.name == "test-exchange.name_123"

    def test_exchange_name_empty_raises_error(self):
        """Test empty exchange name raises error (AC #7)."""
        with pytest.raises(ValueError, match="Exchange name cannot be empty"):
            CreateExchangeRequest(name="", type="topic")

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

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

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

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

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

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

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

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

    def test_exchange_type_invalid_raises_error(self):
        """Test invalid exchange type raises error (AC #2)."""
        with pytest.raises(
            ValidationError,
            match="Input should be 'direct', 'topic', 'fanout' or 'headers'",
        ):
            CreateExchangeRequest(name="events", type="invalid")  # type: ignore

    def test_durable_default_true(self):
        """Test durable defaults to True (AC #3)."""
        request = CreateExchangeRequest(name="events", type="topic")
        assert request.durable is True

    def test_durable_custom_false(self):
        """Test custom durable=False (AC #3)."""
        request = CreateExchangeRequest(name="events", type="topic", durable=False)
        assert request.durable is False

    def test_auto_delete_default_false(self):
        """Test auto_delete defaults to False (AC #3)."""
        request = CreateExchangeRequest(name="events", type="topic")
        assert request.auto_delete is False

    def test_auto_delete_custom_true(self):
        """Test custom auto_delete=True (AC #3)."""
        request = CreateExchangeRequest(name="events", type="topic", auto_delete=True)
        assert request.auto_delete is True

    def test_internal_default_false(self):
        """Test internal defaults to False (AC #3)."""
        request = CreateExchangeRequest(name="events", type="topic")
        assert request.internal is False

    def test_internal_custom_true(self):
        """Test custom internal=True (AC #3)."""
        request = CreateExchangeRequest(name="events", type="topic", internal=True)
        assert request.internal is True

    def test_arguments_default_none(self):
        """Test arguments defaults to None (AC #3)."""
        request = CreateExchangeRequest(name="events", type="topic")
        assert request.arguments is None

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

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

    def test_all_properties_custom(self):
        """Test all properties with custom values."""
        request = CreateExchangeRequest(
            name="custom",
            type="headers",
            vhost="/prod",
            durable=False,
            auto_delete=True,
            internal=True,
            arguments={"x-match": "all"},
        )
        assert request.name == "custom"
        assert request.type == "headers"
        assert request.vhost == "/prod"
        assert request.durable is False
        assert request.auto_delete is True
        assert request.internal is True
        assert request.arguments == {"x-match": "all"}

    def test_extra_fields_ignored(self):
        """Test extra fields ignored (forward compatibility)."""
        request = CreateExchangeRequest(
            **{"name": "events", "type": "topic", "unknown": "ignored"}
        )
        assert request.name == "events"
        assert not hasattr(request, "unknown")


class TestDeleteExchangeRequest:
    """Test DeleteExchangeRequest schema validation (Story 3.7, AC #4, #5, Task 1)."""

    def test_valid_delete_request(self):
        """Test DeleteExchangeRequest with valid fields."""
        request = DeleteExchangeRequest(vhost="/", name="events")
        assert request.vhost == "/"
        assert request.name == "events"
        assert request.force is False

    def test_delete_request_with_force(self):
        """Test DeleteExchangeRequest with force=True."""
        request = DeleteExchangeRequest(vhost="/prod", name="orders", force=True)
        assert request.vhost == "/prod"
        assert request.name == "orders"
        assert request.force is True

    def test_delete_request_defaults(self):
        """Test DeleteExchangeRequest default values."""
        request = DeleteExchangeRequest(name="test")
        assert request.vhost == "/"
        assert request.force is False

    def test_default_exchange_rejected(self):
        """Test default exchange ("") cannot be deleted (AC #5)."""
        with pytest.raises(ValidationError) as exc_info:
            DeleteExchangeRequest(vhost="/", name="")

        errors = exc_info.value.errors()
        assert len(errors) == 1
        assert "Cannot delete default exchange" in str(errors[0]["ctx"]["error"])

    @pytest.mark.parametrize(
        "system_exchange",
        [
            "amq.direct",
            "amq.topic",
            "amq.fanout",
            "amq.headers",
            "amq.match",
            "amq.custom",
        ],
    )
    def test_system_exchanges_rejected(self, system_exchange):
        """Test system exchanges (amq.*) cannot be deleted (AC #4)."""
        with pytest.raises(ValidationError) as exc_info:
            DeleteExchangeRequest(vhost="/", name=system_exchange)

        errors = exc_info.value.errors()
        assert len(errors) == 1
        assert "Cannot delete system exchange" in str(errors[0]["ctx"]["error"])

    def test_valid_custom_exchange_names(self):
        """Test valid custom exchange names are accepted."""
        valid_names = ["events", "orders", "my-exchange", "my_exchange", "ex.1"]
        for name in valid_names:
            request = DeleteExchangeRequest(vhost="/", name=name)
            assert request.name == name

    def test_empty_vhost_rejected(self):
        """Test empty vhost is rejected."""
        with pytest.raises(ValidationError) as exc_info:
            DeleteExchangeRequest(vhost="", name="events")

        errors = exc_info.value.errors()
        assert len(errors) == 1
        assert "cannot be empty" in str(errors[0]["ctx"]["error"])

    def test_extra_fields_ignored(self):
        """Test extra fields are ignored (forward compatibility)."""
        request = DeleteExchangeRequest(
            **{"vhost": "/", "name": "events", "unknown": "ignored"}
        )
        assert request.name == "events"
        assert not hasattr(request, "unknown")


class TestDeleteExchangeResponse:
    """Test DeleteExchangeResponse schema validation (Story 3.7, AC #6, Task 2)."""

    def test_valid_delete_response(self):
        """Test DeleteExchangeResponse with valid fields."""
        response = DeleteExchangeResponse(
            status="success",
            message="Exchange 'events' deleted",
            vhost="/",
            name="events",
        )
        assert response.status == "success"
        assert response.message == "Exchange 'events' deleted"
        assert response.vhost == "/"
        assert response.name == "events"

    def test_status_must_be_success(self):
        """Test status field must be 'success' literal."""
        response = DeleteExchangeResponse(
            status="success",
            message="Exchange deleted",
            vhost="/",
            name="test",
        )
        assert response.status == "success"

    def test_all_fields_required(self):
        """Test all fields are required."""
        with pytest.raises(ValidationError) as exc_info:
            DeleteExchangeResponse(status="success", message="deleted")

        errors = exc_info.value.errors()
        assert len(errors) == 2  # vhost and name missing

    def test_extra_fields_ignored(self):
        """Test extra fields are ignored."""
        response = DeleteExchangeResponse(
            **{
                "status": "success",
                "message": "deleted",
                "vhost": "/",
                "name": "events",
                "unknown": "ignored",
            }
        )
        assert response.status == "success"
        assert not hasattr(response, "unknown")


class TestBindingInfo:
    """Test BindingInfo schema validation (Story 3.8, AC #1, #3, #4)."""

    def test_exchange_to_queue_binding(self):
        """Test exchange-to-queue binding (AC #1, #3)."""
        binding = BindingInfo(
            source="events",
            destination="orders-queue",
            destination_type="queue",
            routing_key="order.created",
            vhost="/",
            properties_key="~order.created",
        )

        assert binding.source == "events"
        assert binding.destination == "orders-queue"
        assert binding.destination_type == "queue"
        assert binding.routing_key == "order.created"
        assert binding.vhost == "/"
        assert binding.properties_key == "~order.created"

    def test_exchange_to_exchange_binding(self):
        """Test exchange-to-exchange binding (AC #4)."""
        binding = BindingInfo(
            source="upstream-exchange",
            destination="downstream-exchange",
            destination_type="exchange",
            routing_key="events.*",
            vhost="/prod",
        )

        assert binding.source == "upstream-exchange"
        assert binding.destination == "downstream-exchange"
        assert binding.destination_type == "exchange"
        assert binding.routing_key == "events.*"
        assert binding.vhost == "/prod"
        assert binding.properties_key is None

    def test_fanout_binding_empty_routing_key(self):
        """Test fanout exchange binding with empty routing key."""
        binding = BindingInfo(
            source="notifications",
            destination="email-queue",
            destination_type="queue",
            routing_key="",
            vhost="/",
        )

        assert binding.routing_key == ""
        assert binding.destination_type == "queue"

    def test_topic_binding_with_wildcards(self):
        """Test topic exchange binding with wildcard routing key."""
        binding = BindingInfo(
            source="logs",
            destination="error-queue",
            destination_type="queue",
            routing_key="logs.*.error",
            vhost="/",
        )

        assert binding.routing_key == "logs.*.error"

    def test_headers_binding_with_arguments(self):
        """Test headers exchange binding with x-match arguments."""
        binding = BindingInfo(
            source="headers-exchange",
            destination="priority-queue",
            destination_type="queue",
            routing_key="",
            arguments={"x-match": "all", "priority": "high", "type": "urgent"},
            vhost="/",
        )

        assert binding.arguments == {
            "x-match": "all",
            "priority": "high",
            "type": "urgent",
        }

    def test_binding_optional_fields(self):
        """Test binding with minimal required fields."""
        binding = BindingInfo(
            source="events",
            destination="orders-queue",
            destination_type="queue",
            routing_key="order.*",
            vhost="/",
        )

        assert binding.arguments is None
        assert binding.properties_key is None

    def test_invalid_destination_type(self):
        """Test invalid destination_type is rejected."""
        with pytest.raises(ValidationError) as exc_info:
            BindingInfo(
                source="events",
                destination="invalid",
                destination_type="invalid",  # type: ignore
                routing_key="test",
                vhost="/",
            )

        errors = exc_info.value.errors()
        assert len(errors) >= 1
        assert any("destination_type" in str(err) for err in errors)

    def test_extra_fields_ignored(self):
        """Test extra fields are ignored (model_config)."""
        binding = BindingInfo(
            **{
                "source": "events",
                "destination": "orders",
                "destination_type": "queue",
                "routing_key": "order.*",
                "vhost": "/",
                "unknown_field": "ignored",
            }
        )

        assert binding.source == "events"
        assert not hasattr(binding, "unknown_field")


class TestListBindingsRequest:
    """Test ListBindingsRequest schema validation (Story 3.8, AC #1, #2, #7)."""

    def test_minimal_request_defaults(self):
        """Test request with minimal parameters (vhost defaults to '/')."""
        request = ListBindingsRequest()

        assert request.vhost == "/"
        assert request.source is None
        assert request.destination is None
        assert request.destination_type is None
        assert request.sort_by is None

    def test_request_with_all_filters(self):
        """Test request with all filter parameters (AC #2)."""
        request = ListBindingsRequest(
            vhost="/prod",
            source="events",
            destination="orders-queue",
            destination_type="queue",
            sort_by="routing_key",
        )

        assert request.vhost == "/prod"
        assert request.source == "events"
        assert request.destination == "orders-queue"
        assert request.destination_type == "queue"
        assert request.sort_by == "routing_key"

    def test_request_with_source_filter(self):
        """Test filtering by source exchange (AC #2)."""
        request = ListBindingsRequest(vhost="/", source="events")

        assert request.source == "events"
        assert request.destination is None

    def test_request_with_destination_filter(self):
        """Test filtering by destination (AC #2)."""
        request = ListBindingsRequest(vhost="/", destination="orders-queue")

        assert request.destination == "orders-queue"
        assert request.source is None

    def test_request_with_destination_type_filter(self):
        """Test filtering by destination type (AC #2)."""
        request = ListBindingsRequest(vhost="/", destination_type="exchange")

        assert request.destination_type == "exchange"

    @pytest.mark.parametrize("sort_field", ["source", "destination", "routing_key"])
    def test_valid_sort_fields(self, sort_field):
        """Test all valid sort_by options (AC #7)."""
        request = ListBindingsRequest(vhost="/", sort_by=sort_field)  # type: ignore

        assert request.sort_by == sort_field

    def test_invalid_destination_type(self):
        """Test invalid destination_type is rejected."""
        with pytest.raises(ValidationError):
            ListBindingsRequest(
                vhost="/",
                destination_type="invalid",  # type: ignore
            )

    def test_invalid_sort_by(self):
        """Test invalid sort_by field is rejected."""
        with pytest.raises(ValidationError):
            ListBindingsRequest(
                vhost="/",
                sort_by="invalid",  # type: ignore
            )

    def test_empty_vhost_rejected(self):
        """Test empty vhost string is rejected (AC #1)."""
        with pytest.raises(ValidationError) as exc_info:
            ListBindingsRequest(vhost="")

        errors = exc_info.value.errors()
        assert len(errors) >= 1
        assert any("vhost" in str(err) for err in errors)

    def test_extra_fields_ignored(self):
        """Test extra fields are ignored (forward compatibility)."""
        request = ListBindingsRequest(
            **{
                "vhost": "/",
                "source": "events",
                "unknown_param": "ignored",
            }
        )

        assert request.source == "events"
        assert not hasattr(request, "unknown_param")


class TestCreateBindingRequest:
    """Test CreateBindingRequest schema validation (Story 3.9, AC #1, #2, #3, #7, Task 1)."""

    def test_exchange_to_queue_binding_valid(self):
        """Test valid exchange-to-queue binding creation (AC #1, #2)."""
        request = CreateBindingRequest(
            source="events",
            destination="orders-queue",
            routing_key="order.*",
            vhost="/",
        )

        assert request.source == "events"
        assert request.destination == "orders-queue"
        assert request.routing_key == "order.*"
        assert request.vhost == "/"
        assert request.destination_type == "queue"
        assert request.arguments is None

    def test_exchange_to_exchange_binding_valid(self):
        """Test valid exchange-to-exchange binding creation (AC #2)."""
        request = CreateBindingRequest(
            source="upstream",
            destination="downstream",
            destination_type="exchange",
            routing_key="events.*",
            vhost="/",
        )

        assert request.source == "upstream"
        assert request.destination == "downstream"
        assert request.destination_type == "exchange"
        assert request.routing_key == "events.*"

    def test_fanout_binding_empty_routing_key(self):
        """Test fanout binding with empty routing key (AC #3)."""
        request = CreateBindingRequest(
            source="notifications",
            destination="email-queue",
            routing_key="",
            vhost="/",
        )

        assert request.routing_key == ""

    def test_topic_binding_with_wildcards(self):
        """Test topic binding with wildcard patterns (AC #3)."""
        # One word wildcard
        request1 = CreateBindingRequest(
            source="logs",
            destination="error-queue",
            routing_key="logs.*.error",
        )
        assert request1.routing_key == "logs.*.error"

        # Zero or more words wildcard
        request2 = CreateBindingRequest(
            source="logs",
            destination="all-queue",
            routing_key="logs.#",
        )
        assert request2.routing_key == "logs.#"

    def test_headers_binding_with_arguments(self):
        """Test headers binding with x-match arguments (AC #3)."""
        request = CreateBindingRequest(
            source="headers-exchange",
            destination="priority-queue",
            routing_key="",
            arguments={"x-match": "all", "priority": "high", "type": "urgent"},
        )

        assert request.arguments == {
            "x-match": "all",
            "priority": "high",
            "type": "urgent",
        }

    def test_default_values(self):
        """Test default values (AC #1, #2)."""
        request = CreateBindingRequest(
            source="events",
            destination="orders-queue",
        )

        assert request.vhost == "/"
        assert request.routing_key == ""
        assert request.destination_type == "queue"
        assert request.arguments is None

    def test_invalid_source_name_empty(self):
        """Test validation error for empty source name (AC #7)."""
        with pytest.raises(ValidationError) as exc_info:
            CreateBindingRequest(
                source="",
                destination="orders-queue",
            )

        errors = exc_info.value.errors()
        assert any("source" in str(err).lower() for err in errors)

    def test_invalid_destination_name_empty(self):
        """Test validation error for empty destination name (AC #7)."""
        with pytest.raises(ValidationError) as exc_info:
            CreateBindingRequest(
                source="events",
                destination="",
            )

        errors = exc_info.value.errors()
        assert any("destination" in str(err).lower() for err in errors)

    def test_invalid_name_too_long(self):
        """Test validation error for name exceeding 255 characters (AC #7)."""
        long_name = "a" * 256

        with pytest.raises(ValidationError) as exc_info:
            CreateBindingRequest(
                source=long_name,
                destination="orders-queue",
            )

        errors = exc_info.value.errors()
        assert any("255" in str(err) for err in errors)

    def test_invalid_name_special_characters(self):
        """Test validation error for invalid characters in name (AC #7)."""
        with pytest.raises(ValidationError) as exc_info:
            CreateBindingRequest(
                source="events@invalid",
                destination="orders-queue",
            )

        errors = exc_info.value.errors()
        assert any("invalid characters" in str(err).lower() for err in errors)

    def test_valid_name_characters(self):
        """Test valid name characters: alphanumeric, hyphen, underscore, period (AC #7)."""
        request = CreateBindingRequest(
            source="my-exchange.v1_prod",
            destination="my-queue.v1_test",
        )

        assert request.source == "my-exchange.v1_prod"
        assert request.destination == "my-queue.v1_test"

    def test_invalid_vhost_empty(self):
        """Test validation error for empty vhost (Task 1)."""
        with pytest.raises(ValidationError) as exc_info:
            CreateBindingRequest(
                source="events",
                destination="orders-queue",
                vhost="",
            )

        errors = exc_info.value.errors()
        assert any("vhost" in str(err).lower() for err in errors)

    def test_extra_fields_ignored(self):
        """Test extra fields are ignored (forward compatibility)."""
        request = CreateBindingRequest(
            **{
                "source": "events",
                "destination": "orders-queue",
                "unknown_param": "ignored",
            }
        )

        assert request.source == "events"
        assert not hasattr(request, "unknown_param")


class TestDeleteBindingRequest:
    """Test DeleteBindingRequest schema validation (Story 3.10, AC #1, #4, Task 1)."""

    def test_exchange_to_queue_binding_valid(self):
        """Test valid exchange-to-queue binding deletion (AC #1)."""
        request = DeleteBindingRequest(
            source="events",
            destination="orders-queue",
            routing_key="order.*",
            vhost="/",
        )

        assert request.source == "events"
        assert request.destination == "orders-queue"
        assert request.routing_key == "order.*"
        assert request.vhost == "/"
        assert request.destination_type == "queue"

    def test_exchange_to_exchange_binding_valid(self):
        """Test valid exchange-to-exchange binding deletion (AC #1)."""
        request = DeleteBindingRequest(
            source="upstream",
            destination="downstream",
            destination_type="exchange",
            routing_key="events.*",
            vhost="/",
        )

        assert request.source == "upstream"
        assert request.destination == "downstream"
        assert request.destination_type == "exchange"
        assert request.routing_key == "events.*"

    def test_default_destination_type(self):
        """Test default destination_type is queue (AC #1)."""
        request = DeleteBindingRequest(
            source="events",
            destination="orders-queue",
            routing_key="order.created",
        )

        assert request.destination_type == "queue"
        assert request.vhost == "/"

    def test_routing_key_cannot_be_empty(self):
        """Test validation error for empty routing key (AC #4, Task 1)."""
        with pytest.raises(ValidationError) as exc_info:
            DeleteBindingRequest(
                source="events",
                destination="orders-queue",
                routing_key="",
            )

        errors = exc_info.value.errors()
        assert any("routing key cannot be empty" in str(err).lower() for err in errors)

    def test_invalid_source_name_empty(self):
        """Test validation error for empty source name (AC #4)."""
        with pytest.raises(ValidationError) as exc_info:
            DeleteBindingRequest(
                source="",
                destination="orders-queue",
                routing_key="order.*",
            )

        errors = exc_info.value.errors()
        assert any("source" in str(err).lower() for err in errors)

    def test_invalid_destination_name_empty(self):
        """Test validation error for empty destination name (AC #4)."""
        with pytest.raises(ValidationError) as exc_info:
            DeleteBindingRequest(
                source="events",
                destination="",
                routing_key="order.*",
            )

        errors = exc_info.value.errors()
        assert any("destination" in str(err).lower() for err in errors)

    def test_invalid_name_too_long(self):
        """Test validation error for name exceeding 255 characters (AC #4)."""
        long_name = "a" * 256

        with pytest.raises(ValidationError) as exc_info:
            DeleteBindingRequest(
                source=long_name,
                destination="orders-queue",
                routing_key="order.*",
            )

        errors = exc_info.value.errors()
        assert any("255" in str(err) for err in errors)

    def test_invalid_name_special_characters(self):
        """Test validation error for invalid characters in name (AC #4)."""
        with pytest.raises(ValidationError) as exc_info:
            DeleteBindingRequest(
                source="events@invalid",
                destination="orders-queue",
                routing_key="order.*",
            )

        errors = exc_info.value.errors()
        assert any("invalid characters" in str(err).lower() for err in errors)

    def test_valid_name_characters(self):
        """Test valid name characters: alphanumeric, hyphen, underscore, period (AC #4)."""
        request = DeleteBindingRequest(
            source="my-exchange.v1_prod",
            destination="my-queue.v1_test",
            routing_key="order.created",
        )

        assert request.source == "my-exchange.v1_prod"
        assert request.destination == "my-queue.v1_test"

    def test_invalid_vhost_empty(self):
        """Test validation error for empty vhost (Task 1)."""
        with pytest.raises(ValidationError) as exc_info:
            DeleteBindingRequest(
                source="events",
                destination="orders-queue",
                routing_key="order.*",
                vhost="",
            )

        errors = exc_info.value.errors()
        assert any("vhost" in str(err).lower() for err in errors)

    def test_extra_fields_ignored(self):
        """Test extra fields are ignored (forward compatibility)."""
        request = DeleteBindingRequest(
            **{
                "source": "events",
                "destination": "orders-queue",
                "routing_key": "order.*",
                "unknown_param": "ignored",
            }
        )

        assert request.source == "events"
        assert not hasattr(request, "unknown_param")


class TestDeleteBindingResponse:
    """Test DeleteBindingResponse schema (Story 3.10, AC #3)."""

    def test_success_response(self):
        """Test successful deletion response (AC #3)."""
        response = DeleteBindingResponse(
            status="success",
            message="Binding deleted",
            source="events",
            destination="orders-queue",
            routing_key="order.*",
            vhost="/",
            destination_type="queue",
            correlation_id="abc-123",
            duration_ms=45.2,
        )

        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 == "abc-123"
        assert response.duration_ms == 45.2

    def test_error_response_not_found(self):
        """Test error response for binding not found (AC #4)."""
        response = DeleteBindingResponse(
            status="error",
            message="Binding not found: source='events', destination='orders-queue', routing_key='order.invalid'",
            source="events",
            destination="orders-queue",
            routing_key="order.invalid",
            vhost="/",
            destination_type="queue",
            correlation_id="abc-123",
        )

        assert response.status == "error"
        assert "Binding not found" in response.message
        assert response.duration_ms is None

    def test_duration_optional(self):
        """Test duration_ms is optional in response."""
        response = DeleteBindingResponse(
            status="success",
            message="Binding deleted",
            source="events",
            destination="orders-queue",
            routing_key="order.*",
            vhost="/",
            destination_type="queue",
            correlation_id="abc-123",
        )

        assert response.duration_ms is None
