import gc
import unittest
from dataclasses import dataclass
from unittest.mock import Mock

from strongbus import Event, EventBus


@dataclass(frozen=True)
class MyEvent(Event):
    value: str


@dataclass(frozen=True)
class AnotherEvent(Event):
    number: int


@dataclass(frozen=True)
class SubEvent(MyEvent):
    pass


class TestEventBus(unittest.TestCase):
    def setUp(self):
        self.bus = EventBus()

    def test_subscribe_and_publish(self):
        callback = Mock()
        self.bus.subscribe(MyEvent, callback)
        event = MyEvent("test")
        self.bus.publish(event)
        callback.assert_called_once_with(event)

    def test_no_subscribers(self):
        event = MyEvent("test")
        # Should not raise any error
        self.bus.publish(event)

    def test_multiple_subscribers(self):
        callback1 = Mock()
        callback2 = Mock()
        self.bus.subscribe(MyEvent, callback1)
        self.bus.subscribe(MyEvent, callback2)
        event = MyEvent("test")
        self.bus.publish(event)
        callback1.assert_called_once_with(event)
        callback2.assert_called_once_with(event)

    def test_different_event_types(self):
        callback_my = Mock()
        callback_another = Mock()
        self.bus.subscribe(MyEvent, callback_my)
        self.bus.subscribe(AnotherEvent, callback_another)
        event_my = MyEvent("test")
        event_another = AnotherEvent(42)
        self.bus.publish(event_my)
        self.bus.publish(event_another)
        callback_my.assert_called_once_with(event_my)
        callback_another.assert_called_once_with(event_another)

    def test_unsubscribe(self):
        callback = Mock()
        self.bus.subscribe(MyEvent, callback)
        event = MyEvent("test")
        self.bus.publish(event)
        callback.assert_called_once_with(event)
        self.bus.unsubscribe(MyEvent, callback)
        callback.reset_mock()
        self.bus.publish(event)
        callback.assert_not_called()

    def test_unsubscribe_non_existent(self):
        callback = Mock()
        # Should not raise error
        self.bus.unsubscribe(MyEvent, callback)
        event = MyEvent("test")
        self.bus.publish(event)
        callback.assert_not_called()

    def test_inheritance_not_propagated(self):
        callback_base = Mock()
        callback_sub = Mock()
        self.bus.subscribe(MyEvent, callback_base)
        self.bus.subscribe(SubEvent, callback_sub)
        event_base = MyEvent("base")
        event_sub = SubEvent("sub")
        self.bus.publish(event_base)
        self.bus.publish(event_sub)
        callback_base.assert_called_once_with(event_base)
        callback_sub.assert_called_once_with(event_sub)
        # Ensure base callback not called for sub event
        self.assertEqual(callback_base.call_count, 1)

    def test_weak_reference_for_methods(self):
        class Subscriber:
            def __init__(self):
                self.called = 0

            def on_event(self, event: MyEvent):
                self.called += 1

        sub = Subscriber()
        self.bus.subscribe(MyEvent, sub.on_event)
        self.assertEqual(len(self.bus._subscribers.get(MyEvent, [])), 1)  # pyright: ignore[reportPrivateUsage]

        event = MyEvent("test")
        self.bus.publish(event)
        self.assertEqual(sub.called, 1)

        del sub
        gc.collect()

        self.bus.publish(event)  # Should clean up dead weak ref
        self.assertEqual(len(self.bus._subscribers.get(MyEvent, [])), 0)  # pyright: ignore[reportPrivateUsage]

    def test_strong_reference_for_functions(self):
        def on_event(event: MyEvent):
            pass

        self.bus.subscribe(MyEvent, on_event)
        self.assertEqual(len(self.bus._subscribers.get(MyEvent, [])), 1)  # pyright: ignore[reportPrivateUsage]

        # Simulate GC, but since strong ref, should remain
        gc.collect()
        self.bus.publish(MyEvent("test"))
        self.assertEqual(len(self.bus._subscribers.get(MyEvent, [])), 1)  # pyright: ignore[reportPrivateUsage]

    def test_unsubscribe_weak_method(self):
        class Subscriber:
            def on_event(self, event: MyEvent):
                pass

        sub = Subscriber()
        self.bus.subscribe(MyEvent, sub.on_event)
        self.assertEqual(len(self.bus._subscribers.get(MyEvent, [])), 1)  # pyright: ignore[reportPrivateUsage]

        self.bus.unsubscribe(MyEvent, sub.on_event)
        self.assertEqual(len(self.bus._subscribers.get(MyEvent, [])), 0)  # pyright: ignore[reportPrivateUsage]


if __name__ == "__main__":
    unittest.main()
