"""Mock-based integration tests for RabbitMQ transport."""

from unittest.mock import MagicMock, patch

import pytest
from pydantic import BaseModel

from kurier.configurations.models.rabbitmq import RabbitMQConfig
from kurier.core.bus_types import BusType
from kurier.core.station import Station
from kurier.hermes import Hermes
from kurier.transports.rabbitmq.rabbitmqline import RabbitMQLine
from kurier.utilities.serializers import CloudEventSerializer

Hermes.is_enabled = False


class PaymentReceived(BaseModel):
    payment_id: str
    amount: float


def _make_rabbit_mocks():
    """Create a reusable set of pika mocks."""
    mock_pika = MagicMock()
    mock_connection = MagicMock()
    mock_channel = MagicMock()

    mock_pika.BlockingConnection.return_value = mock_connection
    mock_connection.channel.return_value = mock_channel
    mock_connection.is_open = True

    return mock_pika, mock_connection, mock_channel


def _make_rabbit_config(name: str = "rabbit_test", **kwargs) -> RabbitMQConfig:
    defaults = {
        "host": "localhost",
        "port": 5672,
        "username": "guest",
        "password": "guest",
        "virtual_host": "/",
    }
    defaults.update(kwargs)
    return RabbitMQConfig(name=name, **defaults)


# -- Topology setup tests ------------------------------------------------------


class TestRabbitMQTopology:
    @pytest.mark.asyncio
    async def test_exchange_and_queue_declared(self):
        """When exchange_name and queue_name are set, both are declared and bound."""
        mock_pika, mock_connection, mock_channel = _make_rabbit_mocks()

        with patch("kurier.transports.rabbitmq.rabbitmqline.pika", mock_pika):
            config = _make_rabbit_config("rabbit_topo", exchange_name="orders.exchange", exchange_type="topic", queue_name="orders.queue", routing_key="order.created")
            line = RabbitMQLine(config=config)

            mock_channel.exchange_declare.assert_called_with(exchange="orders.exchange", exchange_type="topic", durable=True)
            mock_channel.queue_declare.assert_called_with(queue="orders.queue", durable=True)
            mock_channel.queue_bind.assert_called_with(exchange="orders.exchange", queue="orders.queue", routing_key="order.created")

            await line.close()

    @pytest.mark.asyncio
    async def test_queue_only_no_exchange(self):
        """When only queue_name is set, no exchange is declared."""
        mock_pika, _, mock_channel = _make_rabbit_mocks()

        with patch("kurier.transports.rabbitmq.rabbitmqline.pika", mock_pika):
            config = _make_rabbit_config("rabbit_queue_only", queue_name="direct.queue")
            line = RabbitMQLine(config=config)

            mock_channel.exchange_declare.assert_not_called()
            mock_channel.queue_declare.assert_called_with(queue="direct.queue", durable=True)
            mock_channel.queue_bind.assert_not_called()

            await line.close()

    @pytest.mark.asyncio
    async def test_exchange_only_no_queue(self):
        """When only exchange_name is set, no queue is declared."""
        mock_pika, _, mock_channel = _make_rabbit_mocks()

        with patch("kurier.transports.rabbitmq.rabbitmqline.pika", mock_pika):
            config = _make_rabbit_config("rabbit_exchange_only", exchange_name="events.exchange")
            line = RabbitMQLine(config=config)

            mock_channel.exchange_declare.assert_called_with(exchange="events.exchange", exchange_type="topic", durable=True)
            mock_channel.queue_declare.assert_not_called()

            await line.close()


# -- Publish tests -------------------------------------------------------------


class TestRabbitMQPublish:
    @pytest.mark.asyncio
    async def test_publish_to_exchange(self):
        """Publish routes to the configured exchange and routing key."""
        mock_pika, _, mock_channel = _make_rabbit_mocks()

        with patch("kurier.transports.rabbitmq.rabbitmqline.pika", mock_pika):
            config = _make_rabbit_config("rabbit_pub_ex", exchange_name="test.exchange", routing_key="payment.received")
            line = RabbitMQLine(config=config)

            await line.publish_async(PaymentReceived(payment_id="p1", amount=100.0))

            mock_channel.basic_publish.assert_called_once()
            _, kwargs = mock_channel.basic_publish.call_args
            assert kwargs["exchange"] == "test.exchange"
            assert kwargs["routing_key"] == "payment.received"
            assert b"p1" in kwargs["body"]
            assert b"100.0" in kwargs["body"]

            await line.close()

    @pytest.mark.asyncio
    async def test_publish_to_queue_directly(self):
        """When no exchange is set, publish uses default exchange with queue name as routing key."""
        mock_pika, _, mock_channel = _make_rabbit_mocks()

        with patch("kurier.transports.rabbitmq.rabbitmqline.pika", mock_pika):
            config = _make_rabbit_config("rabbit_pub_q", queue_name="direct.queue")
            line = RabbitMQLine(config=config)

            await line.publish_async(PaymentReceived(payment_id="p2", amount=50.0))

            _, kwargs = mock_channel.basic_publish.call_args
            assert kwargs["exchange"] == ""
            assert kwargs["routing_key"] == "direct.queue"

            await line.close()

    @pytest.mark.asyncio
    async def test_publish_batch(self):
        """Batch publish sends individual envelopes."""
        mock_pika, _, mock_channel = _make_rabbit_mocks()

        with patch("kurier.transports.rabbitmq.rabbitmqline.pika", mock_pika):
            config = _make_rabbit_config("rabbit_batch", exchange_name="batch.exchange", routing_key="batch.key")
            line = RabbitMQLine(config=config)

            messages = [PaymentReceived(payment_id=f"b{i}", amount=10.0 * i) for i in range(4)]
            await line.publish_batch_async(messages, {}, batch_size=2)

            assert mock_channel.basic_publish.call_count == 4

            await line.close()

    @pytest.mark.asyncio
    async def test_publish_message_body_is_utf8(self):
        """Published body is UTF-8 encoded."""
        mock_pika, _, mock_channel = _make_rabbit_mocks()

        with patch("kurier.transports.rabbitmq.rabbitmqline.pika", mock_pika):
            config = _make_rabbit_config("rabbit_utf8", queue_name="q")
            line = RabbitMQLine(config=config)

            await line.publish_async("hello")

            _, kwargs = mock_channel.basic_publish.call_args
            assert isinstance(kwargs["body"], bytes)

            await line.close()


# -- Close / lifecycle tests ---------------------------------------------------


class TestRabbitMQLifecycle:
    @pytest.mark.asyncio
    async def test_close_closes_publisher_connection(self):
        """close() closes the publisher connection."""
        mock_pika, mock_connection, _ = _make_rabbit_mocks()

        with patch("kurier.transports.rabbitmq.rabbitmqline.pika", mock_pika):
            config = _make_rabbit_config("rabbit_close", queue_name="q")
            line = RabbitMQLine(config=config)

            await line.close()

            mock_connection.close.assert_called()

    @pytest.mark.asyncio
    async def test_reconnect_on_closed_connection(self):
        """If publisher connection is closed, publish reconnects."""
        mock_pika, mock_connection, mock_channel = _make_rabbit_mocks()

        with patch("kurier.transports.rabbitmq.rabbitmqline.pika", mock_pika):
            config = _make_rabbit_config("rabbit_reconnect", queue_name="q")
            line = RabbitMQLine(config=config)

            # Simulate connection closed
            mock_connection.is_open = False

            await line.publish_async("reconnect test")

            # BlockingConnection should have been called again for reconnect
            assert mock_pika.BlockingConnection.call_count >= 2

            await line.close()


# -- CloudEvents integration ---------------------------------------------------


class TestRabbitMQCloudEvents:
    @pytest.mark.asyncio
    async def test_publish_with_cloudevents_serializer(self):
        """CloudEventSerializer produces CloudEvents envelope on the wire."""
        mock_pika, _, mock_channel = _make_rabbit_mocks()

        with patch("kurier.transports.rabbitmq.rabbitmqline.pika", mock_pika):
            config = _make_rabbit_config("rabbit_ce", exchange_name="ce.exchange", routing_key="ce.key")
            line = RabbitMQLine(config=config, serializer=CloudEventSerializer(source="kurier://rabbit-test"))

            await line.publish_async(PaymentReceived(payment_id="ce1", amount=200.0))

            _, kwargs = mock_channel.basic_publish.call_args
            body = kwargs["body"].decode("utf-8")
            assert "specversion" in body
            assert "kurier://rabbit-test" in body

            await line.close()


# -- Dispatch tests (consumer simulation) --------------------------------------


class TestRabbitMQDispatch:
    @pytest.mark.asyncio
    async def test_dispatch_received_message(self):
        """Simulate receiving a message and dispatching through Station."""
        mock_pika, _, _ = _make_rabbit_mocks()

        received = []

        @Station.add_handler(BusType.RABBIT_MQ)
        def handle_payment(payment: PaymentReceived, *ctx):
            received.append(payment)

        with patch("kurier.transports.rabbitmq.rabbitmqline.pika", mock_pika):
            config = _make_rabbit_config("rabbit_dispatch", queue_name="q")
            line = RabbitMQLine(config=config)

            envelope = line._serialize_to_envelope(PaymentReceived(payment_id="d1", amount=75.0), {})
            raw = envelope.model_dump_json()

            result = await Station.dispatch(line, raw)

            assert result is True
            assert len(received) == 1
            assert received[0].payment_id == "d1"
            assert received[0].amount == 75.0

            await line.close()

    @pytest.mark.asyncio
    async def test_dispatch_by_line_name(self):
        """Handler registered by line name only fires for that line."""
        mock_pika, _, _ = _make_rabbit_mocks()

        specific = []
        generic = []

        with patch("kurier.transports.rabbitmq.rabbitmqline.pika", mock_pika):
            config = _make_rabbit_config("rabbit_named", queue_name="q")
            line = RabbitMQLine(config=config)

            @Station.add_handler("rabbit_named")
            def handle_specific(payment: PaymentReceived, *ctx):
                specific.append(payment)

            @Station.add_handler(BusType.RABBIT_MQ)
            def handle_generic(payment: PaymentReceived, *ctx):
                generic.append(payment)

            envelope = line._serialize_to_envelope(PaymentReceived(payment_id="named", amount=10.0), {})
            await Station.dispatch(line, envelope.model_dump_json())

            # Both should fire: line-name + bus-type
            assert len(specific) == 1
            assert len(generic) == 1

            await line.close()

    @pytest.mark.asyncio
    async def test_dispatch_with_event_handler(self):
        """CloudEvents event-type routing works through RabbitMQ line."""
        mock_pika, _, _ = _make_rabbit_mocks()

        received = []

        with patch("kurier.transports.rabbitmq.rabbitmqline.pika", mock_pika):
            config = _make_rabbit_config("rabbit_event", queue_name="q")
            line = RabbitMQLine(config=config, serializer=CloudEventSerializer(source="kurier://rabbit"))

            @Station.add_event_handler("com.payments.received", "rabbit_event")
            def handle_event(payment: PaymentReceived, ctx):
                received.append(payment)

            envelope = line._serialize_to_envelope(PaymentReceived(payment_id="ev1", amount=300.0), {"cloudevents_type": "com.payments.received"})
            raw = line.serializer.serialize_envelope(envelope)

            result = await Station.dispatch(line, raw)

            assert result is True
            assert received[0].payment_id == "ev1"

            await line.close()

    @pytest.mark.asyncio
    async def test_dispatch_unknown_type_returns_false(self):
        """Dispatch with no matching handler returns False."""
        mock_pika, _, _ = _make_rabbit_mocks()

        with patch("kurier.transports.rabbitmq.rabbitmqline.pika", mock_pika):
            config = _make_rabbit_config("rabbit_unknown", queue_name="q")
            line = RabbitMQLine(config=config)

            envelope = line._serialize_to_envelope({"unregistered": True}, {})
            result = await Station.dispatch(line, envelope.model_dump_json())

            assert result is False

            await line.close()
