import json
import logging
from dataclasses import dataclass
from typing import Any
from unittest.mock import MagicMock

import pytest

from eventspype.broker.redis import RedisBroker
from eventspype.broker.serializer import JsonEventSerializer
from eventspype.sub.subscriber import EventSubscriber


@dataclass
class SampleEvent:
    message: str


@dataclass
class AnotherEvent:
    value: int


class MockSubscriber(EventSubscriber):
    def __init__(self) -> None:
        self.received_messages: list[Any] = []
        self.received_tags: list[int] = []
        self.received_callers: list[Any] = []

    def call(self, arg: Any, current_event_tag: int, current_event_caller: Any) -> None:
        self.received_messages.append(arg)
        self.received_tags.append(current_event_tag)
        self.received_callers.append(current_event_caller)


class ErrorSubscriber(EventSubscriber):
    def call(self, arg: Any, current_event_tag: int, current_event_caller: Any) -> None:
        raise ValueError("subscriber boom")


@pytest.fixture
def mock_redis() -> MagicMock:
    client = MagicMock()
    pubsub = MagicMock()
    client.pubsub.return_value = pubsub
    thread = MagicMock()
    thread.is_alive.return_value = True
    pubsub.run_in_thread.return_value = thread
    return client


@pytest.fixture
def broker(mock_redis: MagicMock) -> RedisBroker:
    return RedisBroker(mock_redis, allow_unregistered_classes=True)


@pytest.fixture
def subscriber() -> MockSubscriber:
    return MockSubscriber()


# --- __init__ tests ---


def test_init_defaults(mock_redis: MagicMock) -> None:
    broker = RedisBroker(mock_redis)
    assert broker._redis is mock_redis
    assert isinstance(broker._serializer, JsonEventSerializer)
    assert broker._channel_prefix == "eventspype:"
    assert broker._pubsub is None
    assert broker._subscribers == {}
    assert broker._listener_thread is None
    assert broker._logger is None


def test_init_custom_serializer_and_prefix(mock_redis: MagicMock) -> None:
    custom_serializer = MagicMock()
    broker = RedisBroker(
        mock_redis, serializer=custom_serializer, channel_prefix="custom:"
    )
    assert broker._serializer is custom_serializer
    assert broker._channel_prefix == "custom:"


# --- logger property tests ---


def test_logger_lazy_init(broker: RedisBroker) -> None:
    assert broker._logger is None
    logger = broker.logger
    assert logger is not None
    assert isinstance(logger, logging.Logger)


def test_logger_cached(broker: RedisBroker) -> None:
    logger1 = broker.logger
    logger2 = broker.logger
    assert logger1 is logger2


# --- _prefixed_channel tests ---


def test_prefixed_channel_default(broker: RedisBroker) -> None:
    assert broker._prefixed_channel("events") == "eventspype:events"


def test_prefixed_channel_custom_prefix(mock_redis: MagicMock) -> None:
    broker = RedisBroker(mock_redis, channel_prefix="app:")
    assert broker._prefixed_channel("test") == "app:test"


# --- publish tests ---


def test_publish_serializes_and_sends(
    broker: RedisBroker, mock_redis: MagicMock
) -> None:
    event = SampleEvent(message="hello")
    broker.publish("chan", event, 42, None)

    mock_redis.publish.assert_called_once()
    call_args = mock_redis.publish.call_args
    assert call_args[0][0] == "eventspype:chan"

    payload = json.loads(call_args[0][1])
    assert payload["event_tag"] == 42
    assert payload["event_class"] == "SampleEvent"
    assert payload["event_module"] == __name__
    assert "message" in payload["payload"]


def test_publish_uses_custom_serializer(mock_redis: MagicMock) -> None:
    serializer = MagicMock()
    serializer.serialize.return_value = b'{"message": "hi"}'
    broker = RedisBroker(mock_redis, serializer=serializer)

    event = SampleEvent(message="hi")
    broker.publish("chan", event, 1, None)

    serializer.serialize.assert_called_once_with(event)
    mock_redis.publish.assert_called_once()


# --- subscribe tests ---


def test_subscribe_creates_pubsub_on_first_channel(
    broker: RedisBroker, mock_redis: MagicMock, subscriber: MockSubscriber
) -> None:
    broker.subscribe("chan1", subscriber)

    mock_redis.pubsub.assert_called_once()
    pubsub = mock_redis.pubsub.return_value
    pubsub.subscribe.assert_called_once()
    # Check the keyword arg is the prefixed channel
    call_kwargs = pubsub.subscribe.call_args[1]
    assert "eventspype:chan1" in call_kwargs


def test_subscribe_reuses_pubsub_for_same_channel(
    broker: RedisBroker, mock_redis: MagicMock
) -> None:
    sub1 = MockSubscriber()
    sub2 = MockSubscriber()
    broker.subscribe("chan1", sub1)
    broker.subscribe("chan1", sub2)

    # pubsub.subscribe called only once for the channel
    pubsub = mock_redis.pubsub.return_value
    assert pubsub.subscribe.call_count == 1
    assert len(broker._subscribers["chan1"]) == 2


def test_subscribe_new_channel_subscribes_again(
    broker: RedisBroker, mock_redis: MagicMock
) -> None:
    sub1 = MockSubscriber()
    sub2 = MockSubscriber()
    broker.subscribe("chan1", sub1)
    broker.subscribe("chan2", sub2)

    pubsub = mock_redis.pubsub.return_value
    assert pubsub.subscribe.call_count == 2


def test_subscribe_starts_listener(
    broker: RedisBroker, mock_redis: MagicMock, subscriber: MockSubscriber
) -> None:
    broker.subscribe("chan1", subscriber)

    pubsub = mock_redis.pubsub.return_value
    pubsub.run_in_thread.assert_called_once_with(sleep_time=0.01, daemon=True)


# --- unsubscribe tests ---


def test_unsubscribe_nonexistent_channel(
    broker: RedisBroker, subscriber: MockSubscriber
) -> None:
    # Should not raise
    broker.unsubscribe("nonexistent", subscriber)


def test_unsubscribe_removes_subscriber(
    broker: RedisBroker, mock_redis: MagicMock
) -> None:
    sub1 = MockSubscriber()
    sub2 = MockSubscriber()
    broker.subscribe("chan1", sub1)
    broker.subscribe("chan1", sub2)

    broker.unsubscribe("chan1", sub1)
    assert len(broker._subscribers["chan1"]) == 1
    assert broker._subscribers["chan1"][0] is sub2


def test_unsubscribe_last_subscriber_removes_channel(
    broker: RedisBroker, mock_redis: MagicMock, subscriber: MockSubscriber
) -> None:
    broker.subscribe("chan1", subscriber)
    broker.unsubscribe("chan1", subscriber)

    assert "chan1" not in broker._subscribers
    pubsub = mock_redis.pubsub.return_value
    pubsub.unsubscribe.assert_called_once_with("eventspype:chan1")


def test_unsubscribe_last_subscriber_when_pubsub_none(
    mock_redis: MagicMock, subscriber: MockSubscriber
) -> None:
    broker = RedisBroker(mock_redis)
    # Manually set up state without pubsub
    broker._subscribers["chan1"] = [subscriber]
    broker.unsubscribe("chan1", subscriber)

    assert "chan1" not in broker._subscribers
    # pubsub was None, so no unsubscribe call on it
    mock_redis.pubsub.return_value.unsubscribe.assert_not_called()


# --- close tests ---


def test_close_cleans_up_pubsub(
    broker: RedisBroker, mock_redis: MagicMock, subscriber: MockSubscriber
) -> None:
    broker.subscribe("chan1", subscriber)
    pubsub = mock_redis.pubsub.return_value

    broker.close()

    pubsub.unsubscribe.assert_called()
    pubsub.close.assert_called_once()
    assert broker._pubsub is None
    assert broker._listener_thread is None


def test_close_when_pubsub_is_none(broker: RedisBroker) -> None:
    # Should not raise
    broker.close()
    assert broker._pubsub is None
    assert broker._listener_thread is None


# --- _ensure_pubsub tests ---


def test_ensure_pubsub_creates_once(broker: RedisBroker, mock_redis: MagicMock) -> None:
    broker._ensure_pubsub()
    broker._ensure_pubsub()

    mock_redis.pubsub.assert_called_once()
    assert broker._pubsub is mock_redis.pubsub.return_value


# --- _ensure_listener tests ---


def test_ensure_listener_creates_thread(
    broker: RedisBroker, mock_redis: MagicMock
) -> None:
    broker._ensure_pubsub()
    pubsub = mock_redis.pubsub.return_value
    broker._ensure_listener()

    pubsub.run_in_thread.assert_called_once_with(sleep_time=0.01, daemon=True)
    assert broker._listener_thread is pubsub.run_in_thread.return_value


def test_ensure_listener_restarts_dead_thread(
    broker: RedisBroker, mock_redis: MagicMock
) -> None:
    broker._ensure_pubsub()
    pubsub = mock_redis.pubsub.return_value

    # First call creates thread
    dead_thread = MagicMock()
    dead_thread.is_alive.return_value = False
    pubsub.run_in_thread.return_value = dead_thread
    broker._ensure_listener()

    # Thread is dead, so next call should create a new one
    new_thread = MagicMock()
    new_thread.is_alive.return_value = True
    pubsub.run_in_thread.return_value = new_thread
    broker._ensure_listener()

    assert pubsub.run_in_thread.call_count == 2
    assert broker._listener_thread is new_thread


def test_ensure_listener_skips_if_alive(
    broker: RedisBroker, mock_redis: MagicMock
) -> None:
    broker._ensure_pubsub()
    pubsub = mock_redis.pubsub.return_value

    alive_thread = MagicMock()
    alive_thread.is_alive.return_value = True
    pubsub.run_in_thread.return_value = alive_thread
    broker._ensure_listener()
    broker._ensure_listener()

    # Only created once since thread is alive
    assert pubsub.run_in_thread.call_count == 1


# --- _make_handler tests ---


def test_handler_skips_non_message_type(broker: RedisBroker) -> None:
    handler = broker._make_handler("chan1")
    # Should not raise for non-message types
    handler({"type": "subscribe", "data": 1})
    handler({"type": "unsubscribe", "data": 0})


def test_handler_dispatches_event_to_subscribers(
    broker: RedisBroker, subscriber: MockSubscriber
) -> None:
    broker._subscribers["chan1"] = [subscriber]
    handler = broker._make_handler("chan1")

    event = SampleEvent(message="test")
    serializer = JsonEventSerializer()
    payload = serializer.serialize(event).decode("utf-8")

    message = {
        "type": "message",
        "data": json.dumps(
            {
                "event_tag": 7,
                "event_class": "SampleEvent",
                "event_module": __name__,
                "payload": payload,
            }
        ),
    }
    handler(message)

    assert len(subscriber.received_messages) == 1
    assert subscriber.received_messages[0] == SampleEvent(message="test")
    assert subscriber.received_tags[0] == 7


def test_handler_dispatches_to_multiple_subscribers(broker: RedisBroker) -> None:
    sub1 = MockSubscriber()
    sub2 = MockSubscriber()
    broker._subscribers["chan1"] = [sub1, sub2]
    handler = broker._make_handler("chan1")

    event = SampleEvent(message="multi")
    serializer = JsonEventSerializer()
    payload = serializer.serialize(event).decode("utf-8")

    message = {
        "type": "message",
        "data": json.dumps(
            {
                "event_tag": 1,
                "event_class": "SampleEvent",
                "event_module": __name__,
                "payload": payload,
            }
        ),
    }
    handler(message)

    assert len(sub1.received_messages) == 1
    assert len(sub2.received_messages) == 1


def test_handler_catches_subscriber_error(broker: RedisBroker, caplog: Any) -> None:
    error_sub = ErrorSubscriber()
    broker._subscribers["chan1"] = [error_sub]
    handler = broker._make_handler("chan1")

    event = SampleEvent(message="fail")
    serializer = JsonEventSerializer()
    payload = serializer.serialize(event).decode("utf-8")

    message = {
        "type": "message",
        "data": json.dumps(
            {
                "event_tag": 1,
                "event_class": "SampleEvent",
                "event_module": __name__,
                "payload": payload,
            }
        ),
    }

    with caplog.at_level(logging.ERROR):
        handler(message)

    assert any(
        "Error dispatching event on channel chan1" in r.message for r in caplog.records
    )


def test_handler_continues_after_subscriber_error(broker: RedisBroker) -> None:
    """Verify that an error in one subscriber doesn't prevent others from receiving the event."""
    error_sub = ErrorSubscriber()
    good_sub = MockSubscriber()
    broker._subscribers["chan1"] = [error_sub, good_sub]
    handler = broker._make_handler("chan1")

    event = SampleEvent(message="partial")
    serializer = JsonEventSerializer()
    payload = serializer.serialize(event).decode("utf-8")

    message = {
        "type": "message",
        "data": json.dumps(
            {
                "event_tag": 1,
                "event_class": "SampleEvent",
                "event_module": __name__,
                "payload": payload,
            }
        ),
    }
    handler(message)

    assert len(good_sub.received_messages) == 1


def test_handler_catches_deserialization_error(
    broker: RedisBroker, caplog: Any
) -> None:
    handler = broker._make_handler("chan1")

    message = {
        "type": "message",
        "data": "not valid json {{{",
    }

    with caplog.at_level(logging.ERROR):
        handler(message)

    assert any(
        "Error processing Redis message on channel chan1" in r.message
        for r in caplog.records
    )


def test_handler_catches_bad_module_error(broker: RedisBroker, caplog: Any) -> None:
    handler = broker._make_handler("chan1")

    message = {
        "type": "message",
        "data": json.dumps(
            {
                "event_tag": 1,
                "event_class": "NonExistentClass",
                "event_module": "nonexistent.module.that.does.not.exist",
                "payload": "{}",
            }
        ),
    }

    with caplog.at_level(logging.ERROR):
        handler(message)

    assert any(
        "Error processing Redis message on channel chan1" in r.message
        for r in caplog.records
    )


# --- _resolve_class tests ---


def test_resolve_class_dataclass(broker: RedisBroker) -> None:
    cls = broker._resolve_class(__name__, "SampleEvent")
    assert cls is SampleEvent


def test_resolve_class_builtin(broker: RedisBroker) -> None:
    cls = broker._resolve_class("builtins", "dict")
    assert cls is dict


def test_resolve_class_nested(broker: RedisBroker) -> None:
    cls = broker._resolve_class("json", "JSONEncoder")
    import json as json_mod

    assert cls is json_mod.JSONEncoder


def test_resolve_class_invalid_module(broker: RedisBroker) -> None:
    with pytest.raises(ModuleNotFoundError):
        broker._resolve_class("nonexistent_module_xyz", "Foo")


def test_resolve_class_invalid_attr(broker: RedisBroker) -> None:
    with pytest.raises(AttributeError):
        broker._resolve_class(__name__, "ClassThatDoesNotExist")


# --- Integration-style test ---


def test_publish_subscribe_roundtrip(mock_redis: MagicMock) -> None:
    """Simulate a full publish-subscribe cycle by capturing the handler and invoking it."""
    broker = RedisBroker(mock_redis, allow_unregistered_classes=True)
    subscriber = MockSubscriber()

    broker.subscribe("events", subscriber)

    # Capture the handler that was registered with pubsub.subscribe
    pubsub = mock_redis.pubsub.return_value
    call_kwargs = pubsub.subscribe.call_args[1]
    handler = call_kwargs["eventspype:events"]

    # Now publish an event
    event = SampleEvent(message="roundtrip")
    broker.publish("events", event, 99, None)

    # Get the message that was published
    publish_call = mock_redis.publish.call_args
    published_data = publish_call[0][1]

    # Feed it back through the handler as Redis would
    handler({"type": "message", "data": published_data})

    assert len(subscriber.received_messages) == 1
    assert subscriber.received_messages[0] == SampleEvent(message="roundtrip")
    assert subscriber.received_tags[0] == 99


# --- Class allowlist tests ---


def test_resolve_class_rejects_unregistered_by_default(mock_redis: MagicMock) -> None:
    """Default broker rejects unregistered classes."""
    broker = RedisBroker(mock_redis)
    with pytest.raises(ValueError, match="not registered"):
        broker._resolve_class(__name__, "SampleEvent")


def test_resolve_class_allows_registered(mock_redis: MagicMock) -> None:
    """Registered classes are resolved successfully."""
    broker = RedisBroker(mock_redis)
    broker.register_event_class(SampleEvent)
    cls = broker._resolve_class(__name__, "SampleEvent")
    assert cls is SampleEvent


def test_register_event_classes_bulk(mock_redis: MagicMock) -> None:
    """register_event_classes registers multiple classes."""
    broker = RedisBroker(mock_redis)
    broker.register_event_classes(SampleEvent, AnotherEvent)
    assert broker._resolve_class(__name__, "SampleEvent") is SampleEvent
    assert broker._resolve_class(__name__, "AnotherEvent") is AnotherEvent


def test_handler_dispatches_registered_class(mock_redis: MagicMock) -> None:
    """Handler works with registered classes (default safe mode)."""
    broker = RedisBroker(mock_redis)
    broker.register_event_class(SampleEvent)
    subscriber = MockSubscriber()
    broker._subscribers["chan1"] = [subscriber]
    handler = broker._make_handler("chan1")

    serializer = JsonEventSerializer()
    payload = serializer.serialize(SampleEvent(message="safe")).decode("utf-8")
    handler(
        {
            "type": "message",
            "data": json.dumps(
                {
                    "event_tag": 1,
                    "event_class": "SampleEvent",
                    "event_module": __name__,
                    "payload": payload,
                }
            ),
        }
    )

    assert len(subscriber.received_messages) == 1
    assert subscriber.received_messages[0] == SampleEvent(message="safe")


def test_handler_rejects_unregistered_class(mock_redis: MagicMock, caplog: Any) -> None:
    """Handler drops messages for unregistered classes in default mode."""
    broker = RedisBroker(mock_redis)
    subscriber = MockSubscriber()
    broker._subscribers["chan1"] = [subscriber]
    handler = broker._make_handler("chan1")

    handler(
        {
            "type": "message",
            "data": json.dumps(
                {
                    "event_tag": 1,
                    "event_class": "SampleEvent",
                    "event_module": __name__,
                    "payload": '{"message": "attack"}',
                }
            ),
        }
    )

    assert len(subscriber.received_messages) == 0


# --- Context manager tests ---


def test_context_manager(mock_redis: MagicMock) -> None:
    """Broker can be used as a context manager for cleanup."""
    sub = MockSubscriber()
    with RedisBroker(mock_redis) as broker:
        broker.subscribe("chan1", sub)
        pubsub = mock_redis.pubsub.return_value

    pubsub.unsubscribe.assert_called()
    pubsub.close.assert_called_once()
    assert broker._pubsub is None


# --- Duplicate subscriber tests ---


def test_subscribe_deduplicates(mock_redis: MagicMock) -> None:
    """Same subscriber is not added twice."""
    broker = RedisBroker(mock_redis)
    sub = MockSubscriber()
    broker.subscribe("chan1", sub)
    broker.subscribe("chan1", sub)

    assert len(broker._subscribers["chan1"]) == 1
