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

from unittest.mock import MagicMock, patch

import pytest
from pydantic import BaseModel

from kurier.configurations.models.kafka import KafkaConfig
from kurier.core.bus_types import BusType
from kurier.core.station import Station
from kurier.hermes import Hermes
from kurier.transports.kafka.kafkaline import KafkaLine
from kurier.utilities.serializers import CloudEventSerializer

Hermes.is_enabled = False


class SensorReading(BaseModel):
    device_id: str
    temperature: float


def _make_kafka_mocks():
    """Create a reusable set of Kafka SDK mocks."""
    mock_producer_cls = MagicMock()
    mock_producer = MagicMock()
    mock_future = MagicMock()

    mock_producer_cls.return_value = mock_producer
    mock_producer.send.return_value = mock_future

    mock_metadata = MagicMock()
    mock_metadata.topic = "test-topic"
    mock_metadata.partition = 0
    mock_metadata.offset = 0
    mock_future.get.return_value = mock_metadata

    mock_consumer_cls = MagicMock()

    return mock_producer_cls, mock_producer, mock_future, mock_metadata, mock_consumer_cls


def _make_kafka_config(name: str = "kafka_test", topic: str = "test-topic", **kwargs) -> KafkaConfig:
    return KafkaConfig(name=name, bootstrap_servers=["localhost:9092"], topic_name=topic, **kwargs)


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


class TestKafkaPublish:
    @pytest.mark.asyncio
    async def test_publish_message(self):
        """Publish a Pydantic message to a topic."""
        mock_producer_cls, mock_producer, mock_future, mock_metadata, mock_consumer_cls = _make_kafka_mocks()

        with patch("kurier.transports.kafka.kafkaline.KafkaProducer", mock_producer_cls), patch("kurier.transports.kafka.kafkaline.KafkaConsumer", mock_consumer_cls):
            line = KafkaLine(config=_make_kafka_config())

            await line.publish_async(SensorReading(device_id="sensor-1", temperature=22.5))

            mock_producer.send.assert_called_once()
            args, _ = mock_producer.send.call_args
            assert args[0] == "test-topic"
            assert "sensor-1" in args[1]
            assert "22.5" in args[1]

            mock_future.get.assert_called()

            await line.close()

    @pytest.mark.asyncio
    async def test_publish_primitive(self):
        """Publish a plain string message."""
        mock_producer_cls, mock_producer, _, _, mock_consumer_cls = _make_kafka_mocks()

        with patch("kurier.transports.kafka.kafkaline.KafkaProducer", mock_producer_cls), patch("kurier.transports.kafka.kafkaline.KafkaConsumer", mock_consumer_cls):
            line = KafkaLine(config=_make_kafka_config("kafka_prim"))

            await line.publish_async("hello kafka")

            mock_producer.send.assert_called_once()
            args, _ = mock_producer.send.call_args
            assert "hello kafka" in args[1]

            await line.close()

    @pytest.mark.asyncio
    async def test_publish_batch(self):
        """Batch publish sends individual envelopes."""
        mock_producer_cls, mock_producer, _, _, mock_consumer_cls = _make_kafka_mocks()

        with patch("kurier.transports.kafka.kafkaline.KafkaProducer", mock_producer_cls), patch("kurier.transports.kafka.kafkaline.KafkaConsumer", mock_consumer_cls):
            line = KafkaLine(config=_make_kafka_config("kafka_batch"))

            messages = [SensorReading(device_id=f"d{i}", temperature=20.0 + i) for i in range(6)]
            await line.publish_batch_async(messages, {}, batch_size=3)

            assert mock_producer.send.call_count == 6

            await line.close()

    @pytest.mark.asyncio
    async def test_publish_with_metadata(self):
        """Metadata is passed through serialization."""
        mock_producer_cls, mock_producer, _, _, mock_consumer_cls = _make_kafka_mocks()

        with patch("kurier.transports.kafka.kafkaline.KafkaProducer", mock_producer_cls), patch("kurier.transports.kafka.kafkaline.KafkaConsumer", mock_consumer_cls):
            line = KafkaLine(config=_make_kafka_config("kafka_meta"))

            await line.publish_async(SensorReading(device_id="s1", temperature=21.0), {"source": "lab"})

            args, _ = mock_producer.send.call_args
            assert "lab" in args[1]

            await line.close()


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


class TestKafkaLifecycle:
    @pytest.mark.asyncio
    async def test_close_flushes_and_closes_producer(self):
        """close() calls flush and close on the producer."""
        mock_producer_cls, mock_producer, _, _, mock_consumer_cls = _make_kafka_mocks()

        with patch("kurier.transports.kafka.kafkaline.KafkaProducer", mock_producer_cls), patch("kurier.transports.kafka.kafkaline.KafkaConsumer", mock_consumer_cls):
            line = KafkaLine(config=_make_kafka_config("kafka_close"))

            await line.close()

            mock_producer.flush.assert_called()
            mock_producer.close.assert_called()

    @pytest.mark.asyncio
    async def test_publish_without_producer_does_not_crash(self):
        """Publishing with no producer logs an error but doesn't raise."""
        mock_producer_cls, _, _, _, mock_consumer_cls = _make_kafka_mocks()

        with patch("kurier.transports.kafka.kafkaline.KafkaProducer", mock_producer_cls), patch("kurier.transports.kafka.kafkaline.KafkaConsumer", mock_consumer_cls):
            line = KafkaLine(config=_make_kafka_config("kafka_no_prod"))
            line._producer = None

            await line.publish_async("test")
            await line.close()


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


class TestKafkaCloudEvents:
    @pytest.mark.asyncio
    async def test_publish_with_cloudevents_serializer(self):
        """CloudEventSerializer produces CloudEvents envelope on the wire."""
        mock_producer_cls, mock_producer, _, _, mock_consumer_cls = _make_kafka_mocks()

        with patch("kurier.transports.kafka.kafkaline.KafkaProducer", mock_producer_cls), patch("kurier.transports.kafka.kafkaline.KafkaConsumer", mock_consumer_cls):
            line = KafkaLine(config=_make_kafka_config("kafka_ce"), serializer=CloudEventSerializer(source="kurier://kafka-test"))

            await line.publish_async(SensorReading(device_id="ce-1", temperature=25.0))

            args, _ = mock_producer.send.call_args
            body = args[1]
            assert "specversion" in body
            assert "kurier://kafka-test" in body

            await line.close()


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


class TestKafkaDispatch:
    @pytest.mark.asyncio
    async def test_dispatch_received_message(self):
        """Simulate receiving a message and dispatching through Station."""
        mock_producer_cls, _, _, _, mock_consumer_cls = _make_kafka_mocks()

        received = []

        @Station.add_handler(BusType.KAFKA)
        def handle_sensor(reading: SensorReading, *ctx):
            received.append(reading)

        with patch("kurier.transports.kafka.kafkaline.KafkaProducer", mock_producer_cls), patch("kurier.transports.kafka.kafkaline.KafkaConsumer", mock_consumer_cls):
            line = KafkaLine(config=_make_kafka_config("kafka_dispatch"))

            envelope = line._serialize_to_envelope(SensorReading(device_id="d99", temperature=30.0), {})
            raw = envelope.model_dump_json()

            result = await Station.dispatch(line, raw)

            assert result is True
            assert len(received) == 1
            assert received[0].device_id == "d99"
            assert received[0].temperature == 30.0

            await line.close()

    @pytest.mark.asyncio
    async def test_dispatch_multiple_handlers(self):
        """Multiple handlers for the same type are all invoked."""
        mock_producer_cls, _, _, _, mock_consumer_cls = _make_kafka_mocks()

        log_a = []
        log_b = []

        @Station.add_handler(BusType.KAFKA)
        def handler_a(reading: SensorReading, *ctx):
            log_a.append(reading)

        @Station.add_handler(BusType.KAFKA)
        def handler_b(reading: SensorReading, *ctx):
            log_b.append(reading)

        with patch("kurier.transports.kafka.kafkaline.KafkaProducer", mock_producer_cls), patch("kurier.transports.kafka.kafkaline.KafkaConsumer", mock_consumer_cls):
            line = KafkaLine(config=_make_kafka_config("kafka_multi"))

            envelope = line._serialize_to_envelope(SensorReading(device_id="multi", temperature=15.0), {})
            result = await Station.dispatch(line, envelope.model_dump_json())

            assert result is True
            assert len(log_a) == 1
            assert len(log_b) == 1

            await line.close()

    @pytest.mark.asyncio
    async def test_dispatch_with_event_handler(self):
        """CloudEvents event-type routing works through Kafka line."""
        mock_producer_cls, _, _, _, mock_consumer_cls = _make_kafka_mocks()

        received = []

        with patch("kurier.transports.kafka.kafkaline.KafkaProducer", mock_producer_cls), patch("kurier.transports.kafka.kafkaline.KafkaConsumer", mock_consumer_cls):
            line = KafkaLine(config=_make_kafka_config("kafka_event"), serializer=CloudEventSerializer(source="kurier://kafka"))

            @Station.add_event_handler("com.iot.sensor.reading", "kafka_event")
            def handle_event(reading: SensorReading, ctx):
                received.append(reading)

            envelope = line._serialize_to_envelope(SensorReading(device_id="ev1", temperature=18.0), {"cloudevents_type": "com.iot.sensor.reading"})
            raw = line.serializer.serialize_envelope(envelope)

            result = await Station.dispatch(line, raw)

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

            await line.close()
