"""Tests for TopicRouter."""

import pytest

from neonlink.errors import PermanentError, is_permanent
from neonlink.record import Record
from neonlink.router import TopicRouter


class _StubHandler:
    """Stub handler that records calls."""

    def __init__(self):
        self.calls: list[Record] = []

    def handle_message(self, record: Record) -> None:
        self.calls.append(record)


def _make_record(topic: str, value: bytes = b"") -> Record:
    return Record(topic=topic, value=value)


class TestTopicRouterDispatch:
    def test_dispatches_to_correct_handler(self):
        handler_a = _StubHandler()
        handler_b = _StubHandler()

        router = TopicRouter()
        router.handle("topic-a", handler_a)
        router.handle("topic-b", handler_b)

        router.handle_message(_make_record("topic-a", b"hello"))
        router.handle_message(_make_record("topic-b", b"world"))

        assert len(handler_a.calls) == 1
        assert handler_a.calls[0].value == b"hello"
        assert len(handler_b.calls) == 1
        assert handler_b.calls[0].value == b"world"

    def test_handle_func_dispatches(self):
        called_with: list[Record] = []

        router = TopicRouter()
        router.handle_func("topic-a", lambda rec: called_with.append(rec))

        router.handle_message(_make_record("topic-a", b"payload"))

        assert len(called_with) == 1
        assert called_with[0].value == b"payload"


class TestTopicRouterUnregistered:
    def test_raises_permanent_error_for_unknown_topic(self):
        router = TopicRouter()
        router.handle("topic-a", _StubHandler())

        with pytest.raises(PermanentError):
            router.handle_message(_make_record("unknown"))

    def test_error_is_classified_as_permanent(self):
        router = TopicRouter()
        router.handle("topic-a", _StubHandler())

        try:
            router.handle_message(_make_record("unknown"))
        except PermanentError as exc:
            assert is_permanent(exc)


class TestTopicRouterErrorPropagation:
    def test_handler_error_propagates(self):
        class _FailHandler:
            def handle_message(self, record):
                raise RuntimeError("boom")

        router = TopicRouter()
        router.handle("topic-a", _FailHandler())

        with pytest.raises(RuntimeError, match="boom"):
            router.handle_message(_make_record("topic-a"))


class TestTopicRouterTopics:
    def test_returns_sorted_topics(self):
        router = TopicRouter()
        router.handle("zeta", _StubHandler())
        router.handle("alpha", _StubHandler())
        router.handle("mu", _StubHandler())

        assert router.topics() == ["alpha", "mu", "zeta"]

    def test_empty_router_returns_empty_list(self):
        router = TopicRouter()
        assert router.topics() == []


class TestTopicRouterValidation:
    def test_raises_on_empty_topic(self):
        router = TopicRouter()
        with pytest.raises(ValueError, match="empty topic"):
            router.handle("", _StubHandler())

    def test_raises_on_none_handler(self):
        router = TopicRouter()
        with pytest.raises(ValueError, match="None handler"):
            router.handle("topic-a", None)

    def test_raises_on_none_func(self):
        router = TopicRouter()
        with pytest.raises(ValueError, match="None fn"):
            router.handle_func("topic-a", None)

    def test_raises_on_duplicate_topic(self):
        router = TopicRouter()
        router.handle("topic-a", _StubHandler())
        with pytest.raises(ValueError, match="called twice"):
            router.handle("topic-a", _StubHandler())

    def test_raises_on_invalid_handler_type(self):
        router = TopicRouter()
        with pytest.raises(TypeError, match="must implement handle_message"):
            router.handle("topic-a", "not a handler")
