"""
Tests for Phase 38: Internal Pub/Sub Event Bus.
"""

import json
import time
import pytest


@pytest.fixture(autouse=True)
def _isolate(tmp_path, monkeypatch):
    """Redirect all event bus storage to tmp_path and reset singleton."""
    events_dir = tmp_path / "events"
    events_dir.mkdir(parents=True, exist_ok=True)

    import brynq.server as srv
    monkeypatch.setattr(srv, "BROKER_DIR", tmp_path)

    import brynq.event_bus as eb
    monkeypatch.setattr(eb, "EVENTS_DIR", events_dir)
    monkeypatch.setattr(eb, "EVENT_LOG_FILE", events_dir / "event_log.jsonl")
    monkeypatch.setattr(eb, "DEFERRED_FILE", events_dir / "deferred_events.jsonl")

    # Reset the singleton bus so subscriptions don't leak between tests
    eb.reset_bus()


# ── Subscribe & Receive ──────────────────────────────────────────────────

class TestSubscribeReceive:
    def test_subscribe_and_receive(self):
        from brynq.event_bus import subscribe, emit
        received = []
        subscribe("task.created", lambda e: received.append(e))
        emit("task.created", {"task_id": "t1"})
        assert len(received) == 1
        assert received[0]["event_type"] == "task.created"

    def test_subscribe_returns_id(self):
        from brynq.event_bus import subscribe
        sub_id = subscribe("task.created", lambda e: None)
        assert isinstance(sub_id, str)
        assert len(sub_id) > 0

    def test_event_data_passed_correctly(self):
        from brynq.event_bus import subscribe, emit
        received = []
        subscribe("task.created", lambda e: received.append(e))
        emit("task.created", {"task_id": "t1", "title": "Test Task"}, source="test-module")
        event = received[0]
        assert event["data"]["task_id"] == "t1"
        assert event["data"]["title"] == "Test Task"
        assert event["source"] == "test-module"
        assert "event_id" in event
        assert "timestamp" in event

    def test_event_id_unique(self):
        from brynq.event_bus import emit
        e1 = emit("task.created")
        e2 = emit("task.created")
        assert e1["event_id"] != e2["event_id"]


# ── Multiple Subscribers ─────────────────────────────────────────────────

class TestMultipleSubscribers:
    def test_multiple_subscribers_same_event(self):
        from brynq.event_bus import subscribe, emit
        received_a = []
        received_b = []
        subscribe("task.completed", lambda e: received_a.append(e))
        subscribe("task.completed", lambda e: received_b.append(e))
        emit("task.completed", {"task_id": "t2"})
        assert len(received_a) == 1
        assert len(received_b) == 1

    def test_different_events_independent(self):
        from brynq.event_bus import subscribe, emit
        received = []
        subscribe("task.created", lambda e: received.append(e))
        emit("task.completed", {"task_id": "t3"})
        assert len(received) == 0


# ── Unsubscribe ──────────────────────────────────────────────────────────

class TestUnsubscribe:
    def test_unsubscribe_stops_delivery(self):
        from brynq.event_bus import subscribe, unsubscribe, emit
        received = []
        sub_id = subscribe("task.created", lambda e: received.append(e))
        emit("task.created")
        assert len(received) == 1
        unsubscribe(sub_id)
        emit("task.created")
        assert len(received) == 1  # no new delivery

    def test_unsubscribe_returns_true(self):
        from brynq.event_bus import subscribe, unsubscribe
        sub_id = subscribe("task.created", lambda e: None)
        assert unsubscribe(sub_id) is True

    def test_unsubscribe_unknown_returns_false(self):
        from brynq.event_bus import unsubscribe
        assert unsubscribe("nonexistent") is False

    def test_unsubscribe_all_by_subscriber(self):
        from brynq.event_bus import subscribe, unsubscribe_all, emit
        received = []
        subscribe("task.created", lambda e: received.append(e), subscriber_id="bot-A")
        subscribe("task.completed", lambda e: received.append(e), subscriber_id="bot-A")
        subscribe("task.failed", lambda e: received.append(e), subscriber_id="bot-B")
        removed = unsubscribe_all("bot-A")
        assert removed == 2
        emit("task.created")
        emit("task.completed")
        emit("task.failed")
        assert len(received) == 1  # only bot-B's handler fired

    def test_unsubscribe_all_none_found(self):
        from brynq.event_bus import unsubscribe_all
        assert unsubscribe_all("ghost") == 0


# ── Wildcard Matching ────────────────────────────────────────────────────

class TestWildcard:
    def test_wildcard_prefix(self):
        from brynq.event_bus import subscribe, emit
        received = []
        subscribe("task.*", lambda e: received.append(e))
        emit("task.created")
        emit("task.completed")
        emit("task.failed")
        assert len(received) == 3

    def test_wildcard_does_not_match_other(self):
        from brynq.event_bus import subscribe, emit
        received = []
        subscribe("task.*", lambda e: received.append(e))
        emit("agent.registered")
        assert len(received) == 0

    def test_global_wildcard_catches_all(self):
        from brynq.event_bus import subscribe, emit
        received = []
        subscribe("*", lambda e: received.append(e))
        emit("task.created")
        emit("agent.registered")
        emit("payment.made")
        assert len(received) == 3

    def test_exact_match_no_wildcard(self):
        from brynq.event_bus import subscribe, emit
        received = []
        subscribe("task.created", lambda e: received.append(e))
        emit("task.completed")
        assert len(received) == 0


# ── Filter Functions ─────────────────────────────────────────────────────

class TestFilter:
    def test_filter_narrows_delivery(self):
        from brynq.event_bus import subscribe, emit
        received = []

        def only_urgent(event):
            return event["data"].get("priority") == "urgent"

        subscribe("task.created", lambda e: received.append(e), filter_fn=only_urgent)
        emit("task.created", {"priority": "low"})
        emit("task.created", {"priority": "urgent"})
        assert len(received) == 1
        assert received[0]["data"]["priority"] == "urgent"

    def test_filter_none_allows_all(self):
        from brynq.event_bus import subscribe, emit
        received = []
        subscribe("task.created", lambda e: received.append(e), filter_fn=None)
        emit("task.created", {"x": 1})
        emit("task.created", {"x": 2})
        assert len(received) == 2


# ── Handler Exceptions ───────────────────────────────────────────────────

class TestHandlerExceptions:
    def test_exception_does_not_crash_emit(self):
        from brynq.event_bus import subscribe, emit

        def bad_handler(e):
            raise ValueError("kaboom")

        received = []
        subscribe("task.created", bad_handler)
        subscribe("task.created", lambda e: received.append(e))
        # Should not raise — bad handler is caught
        event = emit("task.created")
        assert event["event_type"] == "task.created"
        assert len(received) == 1

    def test_exception_increments_error_count(self):
        from brynq.event_bus import subscribe, emit, get_stats

        def bad(e):
            raise RuntimeError("fail")

        subscribe("task.created", bad)
        emit("task.created")
        stats = get_stats()
        assert stats["handler_errors"] >= 1


# ── Event Log Persistence ───────────────────────────────────────────────

class TestEventLog:
    def test_events_persisted_to_log(self):
        from brynq.event_bus import emit, get_event_log
        emit("task.created", {"task_id": "t1"})
        emit("agent.registered", {"agent_id": "a1"})
        log = get_event_log()
        assert len(log) == 2

    def test_query_by_event_type(self):
        from brynq.event_bus import emit, get_event_log
        emit("task.created", {"task_id": "t1"})
        emit("task.completed", {"task_id": "t1"})
        emit("agent.registered", {"agent_id": "a1"})
        log = get_event_log(event_type="task.created")
        assert len(log) == 1
        assert log[0]["event_type"] == "task.created"

    def test_query_with_limit(self):
        from brynq.event_bus import emit, get_event_log
        for i in range(10):
            emit("task.created", {"i": i})
        log = get_event_log(limit=3)
        assert len(log) == 3

    def test_query_with_since(self):
        from brynq.event_bus import emit, get_event_log
        e1 = emit("task.created", {"phase": "early"})
        ts = e1["timestamp"]
        # Emit another event — should have a timestamp >= ts
        emit("task.completed", {"phase": "later"})
        log = get_event_log(since=ts)
        # Both should appear since both are >= ts
        assert len(log) >= 1

    def test_log_returns_most_recent_first(self):
        from brynq.event_bus import emit, get_event_log
        emit("task.created", {"order": 1})
        emit("task.created", {"order": 2})
        log = get_event_log()
        assert log[0]["data"]["order"] == 2
        assert log[1]["data"]["order"] == 1


# ── Empty Emit ───────────────────────────────────────────────────────────

class TestEmptyEmit:
    def test_emit_with_no_subscribers(self):
        from brynq.event_bus import emit
        event = emit("task.created", {"task_id": "t1"})
        assert event["event_type"] == "task.created"
        assert event["data"]["task_id"] == "t1"

    def test_emit_with_no_data(self):
        from brynq.event_bus import emit
        event = emit("task.created")
        assert event["data"] == {}
        assert event["source"] is None


# ── Deferred Events ─────────────────────────────────────────────────────

class TestDeferredEvents:
    def test_emit_deferred_stores_event(self):
        import brynq.event_bus as eb
        deferred_id = eb.emit_deferred("task.created", {"task_id": "d1"}, delay_seconds=0)
        assert isinstance(deferred_id, str)
        assert len(deferred_id) > 0

    def test_deferred_fires_after_delay(self):
        import brynq.event_bus as eb
        received = []
        eb.subscribe("task.created", lambda e: received.append(e))
        eb.emit_deferred("task.created", {"task_id": "d1"}, delay_seconds=0)
        fired = eb.process_deferred()
        assert len(fired) == 1
        assert len(received) == 1
        assert received[0]["data"]["task_id"] == "d1"

    def test_deferred_not_yet_due(self):
        import brynq.event_bus as eb
        received = []
        eb.subscribe("task.created", lambda e: received.append(e))
        eb.emit_deferred("task.created", {"task_id": "d2"}, delay_seconds=9999)
        fired = eb.process_deferred()
        assert len(fired) == 0
        assert len(received) == 0

    def test_deferred_not_fired_twice(self):
        import brynq.event_bus as eb
        received = []
        eb.subscribe("task.created", lambda e: received.append(e))
        eb.emit_deferred("task.created", {"task_id": "d3"}, delay_seconds=0)
        eb.process_deferred()
        eb.process_deferred()  # second pass — should not re-fire
        assert len(received) == 1

    def test_process_deferred_empty(self):
        import brynq.event_bus as eb
        fired = eb.process_deferred()
        assert fired == []


# ── Stats ────────────────────────────────────────────────────────────────

class TestStats:
    def test_stats_initial(self):
        from brynq.event_bus import get_stats
        stats = get_stats()
        assert stats["total_emitted"] == 0
        assert stats["events_by_type"] == {}
        assert stats["active_subscriptions"] == 0
        assert stats["handler_errors"] == 0

    def test_stats_after_emit(self):
        from brynq.event_bus import subscribe, emit, get_stats
        subscribe("task.created", lambda e: None)
        emit("task.created")
        emit("task.created")
        emit("task.completed")
        stats = get_stats()
        assert stats["total_emitted"] == 3
        assert stats["events_by_type"]["task.created"] == 2
        assert stats["events_by_type"]["task.completed"] == 1
        assert stats["active_subscriptions"] == 1

    def test_stats_subscription_count(self):
        from brynq.event_bus import subscribe, unsubscribe, get_stats
        s1 = subscribe("a", lambda e: None)
        s2 = subscribe("b", lambda e: None)
        assert get_stats()["active_subscriptions"] == 2
        unsubscribe(s1)
        assert get_stats()["active_subscriptions"] == 1
        unsubscribe(s2)
        assert get_stats()["active_subscriptions"] == 0


# ── Event Type Constants ─────────────────────────────────────────────────

class TestEventTypes:
    def test_all_event_types_defined(self):
        from brynq.event_bus import ALL_EVENT_TYPES
        assert "agent.registered" in ALL_EVENT_TYPES
        assert "task.created" in ALL_EVENT_TYPES
        assert "payment.made" in ALL_EVENT_TYPES
        assert "swarm.formed" in ALL_EVENT_TYPES
        assert "workflow.started" in ALL_EVENT_TYPES

    def test_constants_match_strings(self):
        from brynq import event_bus as eb
        assert eb.AGENT_REGISTERED == "agent.registered"
        assert eb.TASK_COMPLETED == "task.completed"
        assert eb.PAYMENT_FAILED == "payment.failed"
        assert eb.SWARM_DISBANDED == "swarm.disbanded"


# ── Singleton Reset ──────────────────────────────────────────────────────

class TestSingleton:
    def test_get_bus_returns_instance(self):
        from brynq.event_bus import get_bus, EventBus
        bus = get_bus()
        assert isinstance(bus, EventBus)

    def test_reset_clears_state(self):
        from brynq.event_bus import subscribe, get_stats, reset_bus
        subscribe("task.created", lambda e: None)
        assert get_stats()["active_subscriptions"] == 1
        reset_bus()
        assert get_stats()["active_subscriptions"] == 0
