"""Tests for the event envelope, emitter and state machine (spens/events.py)."""

from datetime import datetime

from spens import events
from spens.events import (
    SCHEMA_VERSION,
    TERMINAL_STATES,
    TRANSITIONS,
    Emitter,
    Event,
    is_terminal,
    is_valid_transition,
    utc_now_iso,
)


def test_event_envelope_schema() -> None:
    event = Event(
        event="warning",
        session_id="abc123",
        timestamp=utc_now_iso(),
        data={"message": "[spens] Warning: something"},
    )
    payload = event.to_json()
    assert payload == {
        "schema_version": SCHEMA_VERSION,
        "event": "warning",
        "session_id": "abc123",
        "timestamp": event.timestamp,
        "data": {"message": "[spens] Warning: something"},
    }
    assert SCHEMA_VERSION == 1
    # to_line is the canonical one-line serialization
    import json

    assert json.loads(event.to_line()) == payload


def test_timestamps_are_iso8601_utc() -> None:
    ts = utc_now_iso()
    parsed = datetime.fromisoformat(ts)
    assert parsed.utcoffset().total_seconds() == 0
    # every event built by the emitter carries such a timestamp
    emitter = Emitter("s1")
    event = emitter.emit("started")
    datetime.fromisoformat(event.timestamp)


def test_emitter_fans_out_to_sinks_in_order() -> None:
    order: list[str] = []

    class SinkA:
        def on_event(self, event):
            order.append(f"a:{event.event}")

        def close(self):
            order.append("a:close")

    class SinkB:
        def on_event(self, event):
            order.append(f"b:{event.event}")

        def close(self):
            order.append("b:close")

    emitter = Emitter("sid", [SinkA(), SinkB()])
    emitter.emit("started")
    emitter.emit("warning", message="x")
    emitter.close()
    assert order == ["a:started", "b:started", "a:warning", "b:warning", "a:close", "b:close"]


def test_emit_requires_session_id_on_every_event() -> None:
    emitter = Emitter("sid-1")
    event = emitter.emit("build_started", image="img")
    assert event.session_id == "sid-1"
    assert event.data == {"image": "img"}


def test_module_emit_is_noop_without_configured_emitter() -> None:
    # pure generator functions (builder) call events.emit unconditionally
    events.reset()
    events.emit("warning", message="nobody is listening")  # must not raise


def test_module_emit_dispatches_to_configured_emitter(recording_sink) -> None:
    events.configure(events.Emitter("mod-sid", [recording_sink]))
    events.emit("started")
    events.emit("warning", message="hello")
    assert recording_sink.named() == ["started", "warning"]
    assert recording_sink.events[0].session_id == "mod-sid"


# -- state machine ----------------------------------------------------------


def test_legal_lifecycle_transitions_accepted() -> None:
    lifecycle = [
        (None, "building"),
        ("building", "interceptor_starting"),
        ("interceptor_starting", "running"),
        ("running", "exited"),
        ("exited", "finished"),
    ]
    for old, new in lifecycle:
        assert is_valid_transition(old, new), f"{old} -> {new} should be legal"


def test_error_and_canceled_reachable_from_any_nonterminal_state() -> None:
    for old in ("building", "interceptor_starting", "running", "exited"):
        assert is_valid_transition(old, "error")
        assert is_valid_transition(old, "canceled")


def test_illegal_transitions_rejected() -> None:
    for old, new in [
        (None, "running"),          # no session dir yet -> cannot run
        ("building", "running"),    # skips interceptor_starting
        ("building", "exited"),
        ("running", "finished"),    # skips agent_exited
        ("exited", "running"),      # no going back
        ("finished", "building"),   # terminal is terminal
        ("canceled", "finished"),
        ("error", "running"),
    ]:
        assert not is_valid_transition(old, new), f"{old} -> {new} should be illegal"


def test_terminal_states_have_no_outgoing_transitions() -> None:
    assert frozenset({"finished", "canceled", "error"}) == TERMINAL_STATES
    for state in TERMINAL_STATES:
        assert TRANSITIONS[state] == frozenset()
        assert is_terminal(state)
    assert not is_terminal("running")
    assert not is_terminal(None)


def test_event_state_mapping_covers_lifecycle_events() -> None:
    assert events.EVENT_STATES == {
        "started": "building",
        "interceptor_starting": "interceptor_starting",
        "agent_started": "running",
        "agent_exited": "exited",
        "finished": "finished",
        "canceled": "canceled",
        "error": "error",
    }
    # every mapped state is a real state
    for state in events.EVENT_STATES.values():
        assert state in events.STATES


def test_emitter_close_is_safe_on_empty() -> None:
    Emitter("x").close()
