"""Tests for MessageResult, Subscription, and ReceivedMessage."""

import asyncio
import json
import pytest
from broker_core.message.result import (
    MessageResult,
    ReceivedMessage,
    Subscription,
)


class TestMessageResult:
    def test_construction(self):
        result = MessageResult(
            topic="orders",
            partition=0,
            offset=42,
            timestamp=1718053200000,
        )
        assert result.topic == "orders"
        assert result.partition == 0
        assert result.offset == 42
        assert result.timestamp == 1718053200000
        assert result.success is True
        assert result.failed() is False

    def test_failed(self):
        result = MessageResult(
            topic="t", partition=0, offset=0, timestamp=0, success=False
        )
        assert result.failed() is True

    def test_repr(self):
        result = MessageResult(topic="t", partition=0, offset=1, timestamp=1000)
        r = repr(result)
        assert "MessageResult" in r
        assert "t" in r


class TestReceivedMessage:
    def test_construction(self):
        ack_called = False

        async def _ack(msg):
            nonlocal ack_called
            ack_called = True

        msg = ReceivedMessage(
            topic="orders",
            partition=0,
            offset=5,
            timestamp=1000,
            key="k1",
            payload={"status": "ok"},
            headers={"source": "web"},
            _ack_callback=_ack,
        )
        assert msg.topic == "orders"
        assert msg.key == "k1"
        assert msg.payload["status"] == "ok"
        assert msg.headers["source"] == "web"

    @pytest.mark.asyncio
    async def test_ack_calls_callback(self):
        ack_called = False

        async def _ack(msg):
            nonlocal ack_called
            ack_called = True

        msg = ReceivedMessage(
            topic="t", partition=0, offset=0, timestamp=0,
            key=None, payload={}, headers={}, _ack_callback=_ack,
        )
        await msg.ack()
        assert ack_called is True

    @pytest.mark.asyncio
    async def test_nack_calls_callback(self):
        nack_called = False

        async def _nack(msg):
            nonlocal nack_called
            nack_called = True

        msg = ReceivedMessage(
            topic="t", partition=0, offset=0, timestamp=0,
            key=None, payload={}, headers={}, _nack_callback=_nack,
        )
        await msg.nack()
        assert nack_called is True

    @pytest.mark.asyncio
    async def test_ack_no_callback_does_not_raise(self):
        msg = ReceivedMessage(
            topic="t", partition=0, offset=0, timestamp=0,
            key=None, payload={}, headers={},
        )
        await msg.ack()  # should not raise

    def test_repr_truncates_long_payload(self):
        msg = ReceivedMessage(
            topic="t", partition=0, offset=0, timestamp=0,
            key="k", payload={"data": "x" * 100}, headers={},
        )
        r = repr(msg)
        assert "..." in r


class TestSubscription:
    """Test Subscription lifecycle with a mock consumer."""

    @pytest.mark.asyncio
    async def test_async_context_manager(self):
        """Subscription can be used as an async context manager."""
        consumer = _MockConsumer([])
        sub = Subscription(consumer, "test-topic")

        async with sub as s:
            assert s is sub

        # After exit, consumer loop should be stopped
        assert sub._stopped is True

    @pytest.mark.asyncio
    async def test_async_iterator_yields_messages(self):
        """Subscription yields ReceivedMessage via async iteration."""
        records = [
            _MockRecord(topic="t", partition=0, offset=0, timestamp=1000, key="k1",
                        value={"msg": "hello"}, headers=[]),
            _MockRecord(topic="t", partition=0, offset=1, timestamp=1001, key="k2",
                        value={"msg": "world"}, headers=[("h", b"v")]),
        ]
        consumer = _MockConsumer(records)
        sub = Subscription(consumer, "test-topic")

        received = []
        async with sub:
            async for msg in sub:
                received.append(msg)
                if len(received) >= 2:
                    break

        assert len(received) == 2
        assert received[0].key == "k1"
        assert received[0].payload == {"msg": "hello"}
        assert received[1].key == "k2"
        assert received[1].payload == {"msg": "world"}
        assert received[1].headers == {"h": "v"}

    @pytest.mark.asyncio
    async def test_stop_unblocks_iterator(self):
        """Calling stop() causes the iterator to finish."""
        consumer = _MockConsumer([])  # empty — will block on getone()
        sub = Subscription(consumer, "test-topic")

        async def _stop_after_delay():
            await asyncio.sleep(0.1)
            await sub.stop()

        async def _iterate():
            messages = []
            async with sub:
                async for msg in sub:
                    messages.append(msg)
            return messages

        # Run both concurrently
        results = await asyncio.gather(_iterate(), _stop_after_delay())
        assert results[0] == []  # No messages before stop


# ---------------------------------------------------------------------------
# Mock helpers
# ---------------------------------------------------------------------------


class _MockRecord:
    """Simulates an aiokafka ConsumerRecord."""

    def __init__(self, topic, partition, offset, timestamp, key, value, headers):
        self.topic = topic
        self.partition = partition
        self.offset = offset
        self.timestamp = timestamp
        self.key = key.encode() if isinstance(key, str) else key
        self.value = json.dumps(value).encode() if isinstance(value, dict) else value
        self.headers = [
            (k.encode() if isinstance(k, str) else k,
             v.encode() if isinstance(v, str) else v)
            for k, v in headers
        ]


class _MockConsumer:
    """Simulates an AIOKafkaConsumer for Subscription testing."""

    def __init__(self, records):
        self._records = list(records)
        self._index = 0
        self._committed = False

    async def getone(self):
        await asyncio.sleep(0.01)  # small yield
        if self._index >= len(self._records):
            # Simulate blocking — will be interrupted by timeout in consume loop
            raise asyncio.TimeoutError()
        record = self._records[self._index]
        self._index += 1
        return record

    async def commit(self):
        self._committed = True

    async def seek(self, partition, offset):
        pass
