"""Tests for cpz.events — EventBus pub/sub with wildcards."""

from __future__ import annotations

import threading
from unittest.mock import MagicMock

from cpz.events.bus import EventBus, _topic_matches


class TestTopicMatching:
    def test_exact(self):
        assert _topic_matches("bar.AAPL.1Min", "bar.AAPL.1Min")

    def test_exact_mismatch(self):
        assert not _topic_matches("bar.AAPL.1Min", "bar.GOOG.1Min")

    def test_single_wildcard(self):
        assert _topic_matches("bar.AAPL.*", "bar.AAPL.1Min")
        assert _topic_matches("bar.AAPL.*", "bar.AAPL.1D")
        assert not _topic_matches("bar.AAPL.*", "bar.GOOG.1Min")

    def test_double_wildcard(self):
        assert _topic_matches("bar.**", "bar.AAPL.1Min")
        assert _topic_matches("bar.**", "bar.GOOG.1D")
        assert _topic_matches("order.**", "order.filled")
        assert not _topic_matches("bar.**", "quote.AAPL")

    def test_wildcard_middle(self):
        assert _topic_matches("bar.*.1Min", "bar.AAPL.1Min")
        assert not _topic_matches("bar.*.1Min", "bar.AAPL.1D")


class TestEventBus:
    def test_subscribe_publish(self):
        bus = EventBus()
        handler = MagicMock()
        bus.subscribe("test.topic", handler)
        bus.publish("test.topic", "data")
        handler.assert_called_once_with("data")

    def test_wildcard_subscribe(self):
        bus = EventBus()
        handler = MagicMock()
        bus.subscribe("bar.*.*", handler)
        bus.publish("bar.AAPL.1Min", "bar_data")
        handler.assert_called_once_with("bar_data")

    def test_double_wildcard(self):
        bus = EventBus()
        handler = MagicMock()
        bus.subscribe("order.**", handler)
        bus.publish("order.filled", "fill")
        bus.publish("order.rejected", "reject")
        assert handler.call_count == 2

    def test_unsubscribe(self):
        bus = EventBus()
        handler = MagicMock()
        bus.subscribe("test", handler)
        bus.unsubscribe("test", handler)
        bus.publish("test", "data")
        handler.assert_not_called()

    def test_no_duplicate_handlers(self):
        bus = EventBus()
        handler = MagicMock()
        bus.subscribe("test", handler)
        bus.subscribe("test", handler)
        bus.publish("test", "data")
        handler.assert_called_once()

    def test_has_subscribers(self):
        bus = EventBus()
        assert not bus.has_subscribers("test")
        handler = MagicMock()
        bus.subscribe("test", handler)
        assert bus.has_subscribers("test")

    def test_has_subscribers_wildcard(self):
        bus = EventBus()
        handler = MagicMock()
        bus.subscribe("bar.**", handler)
        assert bus.has_subscribers("bar.AAPL.1Min")

    def test_clear(self):
        bus = EventBus()
        handler = MagicMock()
        bus.subscribe("test", handler)
        bus.clear()
        assert not bus.has_subscribers("test")

    def test_publish_returns_count(self):
        bus = EventBus()
        h1, h2 = MagicMock(), MagicMock()
        bus.subscribe("test", h1)
        bus.subscribe("test", h2)
        count = bus.publish("test", "data")
        assert count == 2

    def test_handler_exception_doesnt_break_others(self):
        bus = EventBus()
        bad = MagicMock(side_effect=Exception("boom"))
        good = MagicMock()
        bus.subscribe("test", bad)
        bus.subscribe("test", good)
        bus.publish("test", "data")
        good.assert_called_once()

    def test_thread_safety(self):
        bus = EventBus()
        results = []

        def handler(data):
            results.append(data)

        bus.subscribe("test", handler)

        threads = [
            threading.Thread(target=bus.publish, args=("test", i))
            for i in range(100)
        ]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(results) == 100
