from dataclasses import dataclass
from typing import Any

import pytest

from lyricore import ActorContext, ActorSystem, on
from lyricore.eventbus import EventBus
from lyricore.tests.conftest import actor_system


@pytest.mark.asyncio
async def test_event_bus(actor_system):
    event_bus = EventBus()

    @dataclass
    class Event:
        data: Any

    class EventListener:
        def __init__(self):
            self.event_data = None

        @on
        async def handle_event(self, event_data: Any, ctx: ActorContext):
            self.event_data = event_data

        async def curr_state(self):
            return self.event_data

    listener_ref = await actor_system.spawn(EventListener, "event_listener")
    await event_bus.subscribe(listener_ref)

    test_data = Event(data="test_event")
    await event_bus.publish(test_data)

    state = await listener_ref.curr_state.ask()
    assert state == test_data, "Event data should be received by the listener"

    stats = await event_bus.get_stats()
    assert stats.total_events_published == 1
    assert stats.total_subscriptions == 1
    assert stats.failed_deliveries == 0


@pytest.mark.asyncio
async def test_event_bus_with_topic(actor_system):
    event_bus = EventBus()

    @dataclass
    class Event:
        value: int

    @dataclass
    class DummyEvent:
        value: int

    class TopicListener:
        def __init__(self):
            self.value = 0

        @on(Event)
        async def handle_event(self, event_data: Event, ctx: ActorContext):
            self.value += event_data.value

        @on(DummyEvent)
        async def handle_dummy_event(self, event_data: DummyEvent, ctx: ActorContext):
            self.value += event_data.value

        async def curr_state(self) -> int:
            return self.value

    listener_ref = await actor_system.spawn(TopicListener, "topic_listener")
    await event_bus.subscribe(listener_ref, topic="my_topic")

    for i in range(5):
        await event_bus.publish(Event(value=1), topic="my_topic")
        await event_bus.publish(Event(value=3), topic="unsubscribed_topic")

    state = await listener_ref.curr_state.ask()
    assert state == 5, (
        "Listener should have received events from subscribed topics only"
    )


@pytest.mark.asyncio
async def test_duplicate_subscriptions_same_actor_no_topic(actor_system):
    """
    Case1: Same Actor subscribes multiple times without topic
    Expected Behavior: Deduplication should occur, and the Actor should receive the message only once
    """
    event_bus = EventBus()

    @dataclass
    class TestEvent:
        value: int

    class CountingActor:
        def __init__(self):
            self.count = 0
            self.received_values = []

        @on(TestEvent)
        async def handle_event(self, event_data: TestEvent, ctx: ActorContext):
            self.count += 1
            self.received_values.append(event_data.value)

        async def get_count(self) -> int:
            return self.count

        async def get_values(self) -> list:
            return self.received_values

    actor_ref = await actor_system.spawn(CountingActor, "duplicate_test_actor")

    # Subscribe the same actor multiple times without a topic
    sub1 = await event_bus.subscribe(actor_ref)
    sub2 = await event_bus.subscribe(actor_ref)

    # Verify that the same subscription ID is returned (deduplication successful)
    assert sub1 == sub2, (
        "Duplicate subscriptions should return the same subscription ID"
    )

    # Publish an event
    await event_bus.publish(TestEvent(value=100))

    import asyncio

    await asyncio.sleep(0.1)

    # Verify that the event is received only once
    count = await actor_ref.get_count.ask()
    assert count == 1, (
        f"Actor should receive event only once, but received {count} times"
    )

    values = await actor_ref.get_values.ask()
    assert values == [100], f"Expected [100], got {values}"

    # Verify statistics
    stats = await event_bus.get_stats()
    assert stats.total_subscriptions == 1, (
        "Should have only 1 subscription after deduplication"
    )
    assert stats.total_events_published == 1


@pytest.mark.asyncio
async def test_duplicate_subscriptions_same_actor_same_topic(actor_system):
    """
    Case2: Same Actor subscribes multiple times to the same topic
    Expected Behavior: Deduplication should occur, and the Actor should receive the message only once
    """
    event_bus = EventBus()

    @dataclass
    class TopicEvent:
        message: str

    class TopicCountingActor:
        def __init__(self):
            self.count = 0
            self.messages = []

        @on(TopicEvent)
        async def handle_event(self, event_data: TopicEvent, ctx: ActorContext):
            self.count += 1
            self.messages.append(event_data.message)

        async def get_count(self) -> int:
            return self.count

        async def get_messages(self) -> list:
            return self.messages

    actor_ref = await actor_system.spawn(TopicCountingActor, "topic_duplicate_actor")

    # Subscribe the same actor multiple times to the same topic
    sub1 = await event_bus.subscribe(actor_ref, topic="test_topic")
    sub2 = await event_bus.subscribe(actor_ref, topic="test_topic")
    sub3 = await event_bus.subscribe(actor_ref, topic="test_topic")

    # All subscriptions should return the same ID
    assert sub1 == sub2 == sub3, "All duplicate subscriptions should return the same ID"

    # Publish an event
    await event_bus.publish(TopicEvent(message="hello"), topic="test_topic")

    import asyncio

    await asyncio.sleep(0.1)

    count = await actor_ref.get_count.ask()
    assert count == 1, (
        f"Actor should receive event only once, but received {count} times"
    )

    messages = await actor_ref.get_messages.ask()
    assert messages == ["hello"], f"Expected ['hello'], got {messages}"


@pytest.mark.asyncio
async def test_duplicate_subscriptions_different_topics(actor_system):
    """
    Case 3: Same Actor subscribes to different topics
    Expected Behavior: Each subscription should be unique per topic
    """
    event_bus = EventBus()

    @dataclass
    class MultiTopicEvent:
        data: str

    class MultiTopicActor:
        def __init__(self):
            self.events = []

        @on(MultiTopicEvent)
        async def handle_event(self, event_data: MultiTopicEvent, ctx: ActorContext):
            self.events.append(event_data.data)

        async def get_events(self) -> list:
            return self.events

    actor_ref = await actor_system.spawn(MultiTopicActor, "multi_topic_actor")

    # Subscribe to topic_a, topic_b, topic_c
    sub1 = await event_bus.subscribe(actor_ref, topic="topic_a")
    sub2 = await event_bus.subscribe(actor_ref, topic="topic_b")
    sub3 = await event_bus.subscribe(actor_ref, topic="topic_c")

    # Verify that different topics yield different subscription IDs
    assert sub1 != sub2, "Different topics should have different subscription IDs"
    assert sub2 != sub3, "Different topics should have different subscription IDs"
    assert sub1 != sub3, "Different topics should have different subscription IDs"

    # Publish to different topics
    await event_bus.publish(MultiTopicEvent(data="from_a"), topic="topic_a")
    await event_bus.publish(MultiTopicEvent(data="from_b"), topic="topic_b")
    await event_bus.publish(MultiTopicEvent(data="from_c"), topic="topic_c")

    import asyncio

    await asyncio.sleep(0.1)

    # Should receive all 3 events
    events = await actor_ref.get_events.ask()
    assert len(events) == 3, f"Should receive 3 events, got {len(events)}"
    assert sorted(events) == ["from_a", "from_b", "from_c"]


@pytest.mark.asyncio
async def test_multiple_actors_same_subscription(actor_system):
    """
    Case4: Different Actors subscribe to the same event
    Expected Behavior: Each Actor should receive the event
    """
    event_bus = EventBus()

    @dataclass
    class SharedEvent:
        value: int

    class SimpleActor:
        def __init__(self, name: str):
            self.name = name
            self.value = 0

        @on(SharedEvent)
        async def handle_event(self, event_data: SharedEvent, ctx: ActorContext):
            self.value += event_data.value

        async def get_value(self) -> int:
            return self.value

    # Create multiple Actors
    actor1 = await actor_system.spawn(SimpleActor, "actor1", name="Actor1")
    actor2 = await actor_system.spawn(SimpleActor, "actor2", name="Actor2")
    actor3 = await actor_system.spawn(SimpleActor, "actor3", name="Actor3")

    # Every actor should get its own subscription ID
    sub1_a = await event_bus.subscribe(actor1)
    sub1_b = await event_bus.subscribe(actor1)
    sub2 = await event_bus.subscribe(actor2)
    sub3 = await event_bus.subscribe(actor3)

    # Actor1's duplicate subscription should return same ID
    assert sub1_a == sub1_b, "Actor1's duplicate subscription should return same ID"

    # Different Actors should have different subscription IDs
    assert sub1_a != sub2, "Different actors should have different subscription IDs"
    assert sub2 != sub3, "Different actors should have different subscription IDs"

    # Publish event
    await event_bus.publish(SharedEvent(value=10))

    import asyncio

    await asyncio.sleep(0.1)

    # Every actor should receive the event once
    value1 = await actor1.get_value.ask()
    value2 = await actor2.get_value.ask()
    value3 = await actor3.get_value.ask()

    assert value1 == 10, f"Actor1 should receive value 10, got {value1}"
    assert value2 == 10, f"Actor2 should receive value 10, got {value2}"
    assert value3 == 10, f"Actor3 should receive value 10, got {value3}"

    # Verify statistics
    stats = await event_bus.get_stats()
    assert stats.total_subscriptions == 3, "Should have 3 unique subscriptions"
    assert stats.total_events_published == 1


@pytest.mark.asyncio
async def test_unsubscribe_then_resubscribe(actor_system):
    """
    Case5: Unsubscribe and then resubscribe
    Expected Behavior: A new subscription ID should be created
    """
    event_bus = EventBus()

    @dataclass
    class ResubEvent:
        count: int

    class ResubActor:
        def __init__(self):
            self.total = 0

        @on(ResubEvent)
        async def handle_event(self, event_data: ResubEvent, ctx: ActorContext):
            self.total += event_data.count

        async def get_total(self) -> int:
            return self.total

    actor_ref = await actor_system.spawn(ResubActor, "resub_actor")

    # First subscription
    sub1 = await event_bus.subscribe(actor_ref)

    # Publish event - should receive
    await event_bus.publish(ResubEvent(count=1))

    import asyncio

    await asyncio.sleep(0.1)

    total1 = await actor_ref.get_total.ask()
    assert total1 == 1, "Should receive first event"

    # Unsubscribe
    unsubscribed = await event_bus.unsubscribe(sub1)
    assert unsubscribed, "Unsubscribe should succeed"

    # Publish event - should not receive
    await event_bus.publish(ResubEvent(count=10))
    await asyncio.sleep(0.1)

    total2 = await actor_ref.get_total.ask()
    assert total2 == 1, "Should not receive event after unsubscribe"

    # Resubscribe
    sub2 = await event_bus.subscribe(actor_ref)

    # Verify that a new subscription ID is created
    assert sub1 != sub2, "Resubscription should create new subscription ID"

    # Publish event - should receive
    await event_bus.publish(ResubEvent(count=100))
    await asyncio.sleep(0.1)

    total3 = await actor_ref.get_total.ask()
    assert total3 == 101, f"Should receive event after resubscribe, got {total3}"


@pytest.mark.asyncio
async def test_duplicate_subscriptions_with_mixed_topics(actor_system):
    """
    Case6: Mixed subscriptions with and without topics
    Expected Behavior: Subscriptions with and without topics should be treated independently
    """
    event_bus = EventBus()

    @dataclass
    class MixedEvent:
        label: str

    class MixedTopicActor:
        def __init__(self):
            self.labels = []

        @on(MixedEvent)
        async def handle_event(self, event_data: MixedEvent, ctx: ActorContext):
            print(f"Received event with label: {event_data}")
            self.labels.append(event_data.label)

        async def get_labels(self) -> list:
            return self.labels

    actor_ref = await actor_system.spawn(MixedTopicActor, "mixed_actor")

    # Subscription without topic
    sub_global = await event_bus.subscribe(actor_ref)

    # Subscribe to topic_a and topic_b
    sub_topic_a = await event_bus.subscribe(actor_ref, topic="topic_a")
    sub_topic_b = await event_bus.subscribe(actor_ref, topic="topic_b")

    # Duplicate subscriptions
    sub_global_dup = await event_bus.subscribe(actor_ref)
    sub_topic_a_dup = await event_bus.subscribe(actor_ref, topic="topic_a")

    # Verify deduplication
    assert sub_global == sub_global_dup, "Global subscription should be deduplicated"
    assert sub_topic_a == sub_topic_a_dup, "Topic A subscription should be deduplicated"

    # Verify that different types of subscriptions have different IDs
    assert sub_global != sub_topic_a, (
        "Global and topic subscriptions should be different"
    )
    assert sub_topic_a != sub_topic_b, (
        "Different topic subscriptions should be different"
    )

    # Publish events to global and specific topics
    await event_bus.publish(MixedEvent(label="global"))  # No topic
    await event_bus.publish(MixedEvent(label="topic_a"), topic="topic_a")
    await event_bus.publish(MixedEvent(label="topic_b"), topic="topic_b")

    import asyncio

    await asyncio.sleep(0.1)

    labels = await actor_ref.get_labels.ask()

    assert "global" in labels, "Should receive global event"
    assert "topic_a" in labels, "Should receive topic_a event"
    assert "topic_b" in labels, "Should receive topic_b event"
    assert len(labels) == 5, (
        f"Should receive exactly 3 events, got {len(labels)}: {labels}"
    )


@pytest.mark.asyncio
async def test_subscription_stats_with_deduplication(actor_system):
    """
    Case7: Verify statistics correctness with deduplication
    """
    event_bus = EventBus()

    @dataclass
    class StatsEvent:
        data: str

    class StatsActor:
        def __init__(self):
            pass

        @on(StatsEvent)
        async def handle_event(self, event_data: StatsEvent, ctx: ActorContext):
            pass

    actor1 = await actor_system.spawn(StatsActor, "stats_actor1")
    actor2 = await actor_system.spawn(StatsActor, "stats_actor2")

    # Verify initial stats
    stats = await event_bus.get_stats()
    initial_subs = stats.total_subscriptions

    # Subscribe actor1 three times
    await event_bus.subscribe(actor1)
    await event_bus.subscribe(actor1)
    await event_bus.subscribe(actor1)

    stats = await event_bus.get_stats()
    assert stats.total_subscriptions == initial_subs + 1, (
        "Duplicate subscriptions should not increment count"
    )

    # Subscribe actor2 two times
    await event_bus.subscribe(actor2)
    await event_bus.subscribe(actor2)

    stats = await event_bus.get_stats()
    assert stats.total_subscriptions == initial_subs + 2, (
        "Should have 2 unique subscriptions"
    )

    # Verify that there are exactly 2 subscriptions for our test actors
    subscriptions = await event_bus.list_subscriptions()

    # Filter out subscriptions for this test (by actor name)
    test_subs = [s for s in subscriptions if "stats_actor" in str(s)]
    assert len(test_subs) == 2, (
        f"Should have exactly 2 subscriptions, got {len(test_subs)}"
    )


@pytest.mark.asyncio
async def test_deduplication_preserves_first_subscription(actor_system):
    """
    Case8: Verify that deduplication preserves the first subscription
    """
    event_bus = EventBus()

    @dataclass
    class PreserveEvent:
        value: int

    class PreserveActor:
        def __init__(self):
            self.values = []

        @on(PreserveEvent)
        async def handle_event(self, event_data: PreserveEvent, ctx: ActorContext):
            self.values.append(event_data.value)

        async def get_values(self) -> list:
            return self.values

    actor_ref = await actor_system.spawn(PreserveActor, "preserve_actor")

    # First subscription
    sub1 = await event_bus.subscribe(actor_ref, topic="preserve_topic")

    await event_bus.publish(PreserveEvent(value=1), topic="preserve_topic")

    import asyncio

    await asyncio.sleep(0.1)

    values1 = await actor_ref.get_values.ask()
    assert values1 == [1], "Should receive first event"

    # Duplicate subscriptions
    sub2 = await event_bus.subscribe(actor_ref, topic="preserve_topic")
    sub3 = await event_bus.subscribe(actor_ref, topic="preserve_topic")

    # Should all return the first subscription ID
    assert sub1 == sub2 == sub3, "All should return the original subscription ID"

    # Publish event - should receive
    await event_bus.publish(PreserveEvent(value=2), topic="preserve_topic")
    await asyncio.sleep(0.1)

    values2 = await actor_ref.get_values.ask()
    assert values2 == [1, 2], f"Should receive exactly 2 events total, got {values2}"

    # Unsubscribe
    unsubscribed = await event_bus.unsubscribe(sub1)
    assert unsubscribed, "Should successfully unsubscribe"

    await event_bus.publish(PreserveEvent(value=3), topic="preserve_topic")
    await asyncio.sleep(0.1)

    values3 = await actor_ref.get_values.ask()
    assert values3 == [1, 2], (
        f"Should not receive event after unsubscribe, got {values3}"
    )
