"""
Unit tests for FastPluggyEventBus and Event.
"""
import asyncio
import pytest
from fastpluggy.core.events import Event, FastPluggyEventBus


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def make_bus() -> FastPluggyEventBus:
    return FastPluggyEventBus()


# ---------------------------------------------------------------------------
# Event dataclass
# ---------------------------------------------------------------------------

def test_event_defaults():
    e = Event(name="test.event")
    assert e.source == ""
    assert e.payload == {}
    assert e.timestamp is not None


def test_event_payload():
    e = Event(name="user.login", source="auth", payload={"user_id": 42})
    assert e.payload["user_id"] == 42


# ---------------------------------------------------------------------------
# Basic subscribe / emit
# ---------------------------------------------------------------------------

def test_emit_calls_listener():
    bus = make_bus()
    received: list[Event] = []
    bus.on("test.event", received.append)
    bus.emit("test.event", source="s", value=1)
    assert len(received) == 1
    assert received[0].name == "test.event"
    assert received[0].payload["value"] == 1


def test_emit_unknown_event_no_error():
    bus = make_bus()
    bus.emit("no.listeners")  # must not raise


def test_multiple_listeners_all_called():
    bus = make_bus()
    log: list[str] = []
    bus.on("ev", lambda e: log.append("a"))
    bus.on("ev", lambda e: log.append("b"))
    bus.emit("ev")
    assert sorted(log) == ["a", "b"]


# ---------------------------------------------------------------------------
# Decorator form
# ---------------------------------------------------------------------------

def test_decorator_form():
    bus = make_bus()
    received: list[Event] = []

    @bus.on("greet")
    def handle(event: Event):
        received.append(event)

    bus.emit("greet")
    assert len(received) == 1


# ---------------------------------------------------------------------------
# off / unsubscribe
# ---------------------------------------------------------------------------

def test_off_removes_listener():
    bus = make_bus()
    log: list[int] = []
    fn = lambda e: log.append(1)
    bus.on("ev", fn)
    bus.off("ev", fn)
    bus.emit("ev")
    assert log == []


def test_off_unknown_no_error():
    bus = make_bus()
    bus.off("nonexistent", lambda e: None)  # must not raise


# ---------------------------------------------------------------------------
# Priority
# ---------------------------------------------------------------------------

def test_priority_ordering():
    bus = make_bus()
    order: list[str] = []
    bus.on("ev", lambda e: order.append("low"), priority=0)
    bus.on("ev", lambda e: order.append("high"), priority=10)
    bus.on("ev", lambda e: order.append("mid"), priority=5)
    bus.emit("ev")
    assert order == ["high", "mid", "low"]


# ---------------------------------------------------------------------------
# once=True
# ---------------------------------------------------------------------------

def test_once_fires_only_once():
    bus = make_bus()
    log: list[int] = []
    bus.on("ev", lambda e: log.append(1), once=True)
    bus.emit("ev")
    bus.emit("ev")
    assert log == [1]


def test_once_does_not_affect_other_listeners():
    bus = make_bus()
    regular: list[int] = []
    once_log: list[int] = []
    bus.on("ev", lambda e: regular.append(1))
    bus.on("ev", lambda e: once_log.append(1), once=True)
    bus.emit("ev")
    bus.emit("ev")
    assert regular == [1, 1]
    assert once_log == [1]


# ---------------------------------------------------------------------------
# Predicate
# ---------------------------------------------------------------------------

def test_predicate_filters_listener():
    bus = make_bus()
    log: list[int] = []
    bus.on("ev", lambda e: log.append(e.payload["v"]), predicate=lambda e: e.payload.get("v", 0) > 5)
    bus.emit("ev", v=3)
    bus.emit("ev", v=10)
    assert log == [10]


def test_predicate_exception_treated_as_false():
    bus = make_bus()
    log: list[int] = []

    def bad_pred(e):
        raise RuntimeError("boom")

    bus.on("ev", lambda e: log.append(1), predicate=bad_pred)
    bus.emit("ev")
    assert log == []


# ---------------------------------------------------------------------------
# Error isolation
# ---------------------------------------------------------------------------

def test_listener_crash_does_not_block_others():
    bus = make_bus()
    log: list[str] = []

    def crashing(e):
        raise ValueError("crash!")

    bus.on("ev", lambda e: log.append("before"))
    bus.on("ev", crashing)
    bus.on("ev", lambda e: log.append("after"))
    bus.emit("ev")
    assert "before" in log
    assert "after" in log


def test_custom_error_handler_called():
    bus = make_bus()
    errors: list[tuple] = []
    bus.on_error(lambda exc, ev: errors.append((exc, ev)))
    bus.on("ev", lambda e: (_ for _ in ()).throw(RuntimeError("oops")))
    bus.emit("ev")
    assert len(errors) == 1
    assert isinstance(errors[0][0], RuntimeError)


# ---------------------------------------------------------------------------
# on_namespace
# ---------------------------------------------------------------------------

def test_namespace_catches_subtopic():
    bus = make_bus()
    log: list[str] = []
    bus.on_namespace("user", lambda e: log.append(e.name))
    bus.emit("user.login")
    bus.emit("user.logout")
    bus.emit("other.event")
    assert log == ["user.login", "user.logout"]


def test_namespace_exact_match():
    bus = make_bus()
    log: list[str] = []
    bus.on_namespace("fp.app_ready", lambda e: log.append(e.name))
    bus.emit("fp.app_ready")
    assert log == ["fp.app_ready"]


def test_off_namespace():
    bus = make_bus()
    log: list[str] = []
    fn = lambda e: log.append(e.name)
    bus.on_namespace("user", fn)
    bus.off_namespace("user", fn)
    bus.emit("user.login")
    assert log == []


def test_namespace_once():
    bus = make_bus()
    log: list[str] = []
    bus.on_namespace("user", lambda e: log.append(e.name), once=True)
    bus.emit("user.login")
    bus.emit("user.logout")
    assert log == ["user.login"]


# ---------------------------------------------------------------------------
# subscribed context manager
# ---------------------------------------------------------------------------

def test_subscribed_context_manager():
    bus = make_bus()
    log: list[int] = []
    fn = lambda e: log.append(1)
    with bus.subscribed("ev", fn):
        bus.emit("ev")
    bus.emit("ev")
    assert log == [1]


# ---------------------------------------------------------------------------
# Introspection
# ---------------------------------------------------------------------------

def test_listeners_returns_fns():
    bus = make_bus()
    fn = lambda e: None
    bus.on("ev", fn)
    assert fn in bus.listeners("ev")


def test_topics():
    bus = make_bus()
    bus.on("a.b", lambda e: None)
    bus.on("c.d", lambda e: None)
    assert set(bus.topics()) == {"a.b", "c.d"}


def test_listener_count():
    bus = make_bus()
    bus.on("ev", lambda e: None)
    bus.on("ev", lambda e: None)
    bus.on_namespace("user", lambda e: None)
    assert bus.listener_count() == 3


# ---------------------------------------------------------------------------
# Async listeners
# ---------------------------------------------------------------------------

def test_async_listener_no_running_loop():
    """Async listener runs to completion when no event loop is active."""
    bus = make_bus()
    log: list[int] = []

    async def async_handler(event: Event):
        log.append(1)

    bus.on("ev", async_handler)
    bus.emit("ev")
    assert log == [1]


@pytest.mark.asyncio
async def test_async_listener_in_running_loop():
    """Async listener is scheduled as a task when called inside a running loop."""
    bus = make_bus()
    log: list[int] = []

    async def async_handler(event: Event):
        log.append(1)

    bus.on("ev", async_handler)
    bus.emit("ev")
    # Give the scheduled task a chance to run
    await asyncio.sleep(0)
    assert log == [1]
