"""Tests for GameEventBus — publish, subscribe, unsubscribe, thread safety."""

import threading


from wled_controller.core.game_integration.event_bus import GameEventBus
from wled_controller.core.game_integration.events import GameEvent


def _make_event(event_type: str = "health", value: float = 0.5) -> GameEvent:
    return GameEvent(
        adapter_id="test_adapter",
        event_type=event_type,
        value=value,
        raw_data={"test": True},
    )


class TestPublishSubscribe:
    """Basic publish/subscribe behavior."""

    def test_subscribe_receives_matching_events(self) -> None:
        bus = GameEventBus()
        received: list[GameEvent] = []
        bus.subscribe("health", received.append)

        event = _make_event("health", 0.8)
        bus.publish(event)

        assert len(received) == 1
        assert received[0] is event

    def test_subscribe_ignores_non_matching_events(self) -> None:
        bus = GameEventBus()
        received: list[GameEvent] = []
        bus.subscribe("health", received.append)

        bus.publish(_make_event("kill"))

        assert len(received) == 0

    def test_subscribe_all_receives_every_event(self) -> None:
        bus = GameEventBus()
        received: list[GameEvent] = []
        bus.subscribe_all(received.append)

        bus.publish(_make_event("health"))
        bus.publish(_make_event("kill"))
        bus.publish(_make_event("death"))

        assert len(received) == 3

    def test_multiple_subscribers_same_type(self) -> None:
        bus = GameEventBus()
        received_a: list[GameEvent] = []
        received_b: list[GameEvent] = []
        bus.subscribe("kill", received_a.append)
        bus.subscribe("kill", received_b.append)

        bus.publish(_make_event("kill"))

        assert len(received_a) == 1
        assert len(received_b) == 1

    def test_type_and_wildcard_both_receive(self) -> None:
        bus = GameEventBus()
        type_received: list[GameEvent] = []
        wild_received: list[GameEvent] = []
        bus.subscribe("health", type_received.append)
        bus.subscribe_all(wild_received.append)

        bus.publish(_make_event("health"))

        assert len(type_received) == 1
        assert len(wild_received) == 1


class TestUnsubscribe:
    """Unsubscribe behavior."""

    def test_unsubscribe_stops_delivery(self) -> None:
        bus = GameEventBus()
        received: list[GameEvent] = []
        sub_id = bus.subscribe("health", received.append)

        bus.publish(_make_event("health"))
        assert len(received) == 1

        result = bus.unsubscribe(sub_id)
        assert result is True

        bus.publish(_make_event("health"))
        assert len(received) == 1  # No new events

    def test_unsubscribe_wildcard(self) -> None:
        bus = GameEventBus()
        received: list[GameEvent] = []
        sub_id = bus.subscribe_all(received.append)

        bus.publish(_make_event("health"))
        assert len(received) == 1

        bus.unsubscribe(sub_id)
        bus.publish(_make_event("health"))
        assert len(received) == 1

    def test_unsubscribe_unknown_id_returns_false(self) -> None:
        bus = GameEventBus()
        assert bus.unsubscribe("nonexistent_id") is False


class TestRecentEvents:
    """Recent events deque."""

    def test_get_recent_events_returns_published(self) -> None:
        bus = GameEventBus()
        bus.publish(_make_event("health", 0.5))
        bus.publish(_make_event("kill", 1.0))

        recent = bus.get_recent_events()
        assert len(recent) == 2
        assert recent[0].event_type == "health"
        assert recent[1].event_type == "kill"

    def test_get_recent_events_respects_limit(self) -> None:
        bus = GameEventBus()
        for i in range(10):
            bus.publish(_make_event("health", i / 10.0))

        recent = bus.get_recent_events(limit=3)
        assert len(recent) == 3

    def test_recent_events_bounded_by_maxlen(self) -> None:
        bus = GameEventBus(recent_maxlen=5)
        for i in range(10):
            bus.publish(_make_event("health", i / 10.0))

        recent = bus.get_recent_events(limit=100)
        assert len(recent) == 5


class TestStats:
    """Event statistics."""

    def test_stats_counts_per_type(self) -> None:
        bus = GameEventBus()
        bus.publish(_make_event("health"))
        bus.publish(_make_event("health"))
        bus.publish(_make_event("kill"))

        stats = bus.get_stats()
        assert stats["event_counts"]["health"] == 2
        assert stats["event_counts"]["kill"] == 1

    def test_stats_last_timestamp(self) -> None:
        bus = GameEventBus()
        assert bus.get_stats()["last_event_timestamp"] is None

        event = _make_event("health")
        bus.publish(event)

        stats = bus.get_stats()
        assert stats["last_event_timestamp"] == event.timestamp


class TestThreadSafety:
    """Concurrent publish/subscribe must not crash."""

    def test_concurrent_publish(self) -> None:
        bus = GameEventBus()
        received: list[GameEvent] = []
        lock = threading.Lock()

        def safe_append(event: GameEvent) -> None:
            with lock:
                received.append(event)

        bus.subscribe_all(safe_append)

        num_threads = 10
        events_per_thread = 50
        threads = []

        def publisher(thread_id: int) -> None:
            for i in range(events_per_thread):
                bus.publish(_make_event("health", i / events_per_thread))

        for t_id in range(num_threads):
            t = threading.Thread(target=publisher, args=(t_id,))
            threads.append(t)
            t.start()

        for t in threads:
            t.join(timeout=10)

        assert len(received) == num_threads * events_per_thread

    def test_concurrent_subscribe_unsubscribe(self) -> None:
        """Subscribe and unsubscribe from multiple threads simultaneously."""
        bus = GameEventBus()
        sub_ids: list[str] = []
        lock = threading.Lock()

        def subscriber() -> None:
            sid = bus.subscribe("health", lambda e: None)
            with lock:
                sub_ids.append(sid)

        threads = [threading.Thread(target=subscriber) for _ in range(20)]
        for t in threads:
            t.start()
        for t in threads:
            t.join(timeout=10)

        assert len(sub_ids) == 20

        # Unsubscribe all
        for sid in sub_ids:
            assert bus.unsubscribe(sid) is True


class TestCallbackError:
    """Callback errors must not crash the bus."""

    def test_error_in_callback_does_not_block_others(self) -> None:
        bus = GameEventBus()
        received: list[GameEvent] = []

        def bad_callback(event: GameEvent) -> None:
            raise RuntimeError("boom")

        bus.subscribe("health", bad_callback)
        bus.subscribe("health", received.append)

        # Should not raise
        bus.publish(_make_event("health"))

        # Second subscriber still receives the event
        assert len(received) == 1
