"""Golden-string tests for TtySink (spens/sinks/tty.py).

Each event's literal ``[spens] ...`` line is pinned byte-for-byte: TtySink
renders today's output verbatim, and events without a message render nothing
(there was no print for them before either).
"""

from __future__ import annotations

import io

from spens.events import Emitter, Event
from spens.sinks.tty import TtySink


def _render(events: list[Event]) -> str:
    stream = io.StringIO()
    sink = TtySink(stream)
    for event in events:
        sink.on_event(event)
    return stream.getvalue()


def _event(name: str, **data) -> Event:
    return Event(
        event=name, session_id="sid", timestamp="2026-01-01T00:00:00+00:00", data=data
    )


def test_warning_renders_verbatim() -> None:
    line = (
        "[spens] Warning: inject_headers env var 'KEY' is not set on the host. "
        "The placeholder will NOT be replaced by the interceptor."
    )
    assert _render([_event("warning", message=line)]) == line + "\n"


def test_events_without_message_render_nothing() -> None:
    # started, build_finished and agent_exited had no tty line before
    assert _render([
        _event("started", session_dir="/a", traces_dir="/b", audit_dir="/c"),
        _event("build_finished", image="img"),
        _event("agent_exited", exit_code=0),
    ]) == ""


def test_build_started_renders_its_literal_line() -> None:
    assert _render([
        _event("build_started", image="spens-agent:node-20-opencode",
               message="[spens] Building agent image spens-agent:node-20-opencode ...")
    ]) == "[spens] Building agent image spens-agent:node-20-opencode ...\n"


def test_lifecycle_lines_render_verbatim_in_order() -> None:
    out = _render([
        _event("interceptor_starting",
               message="[spens] Starting interceptor for session abc123 ..."),
        _event("info", message="[spens] Waiting for interceptor addon and DNS forwarder ..."),
        _event("interceptor_ready",
               message="[spens] Interceptor ready (addon + DNS forwarder up, proxy listening)."),
        _event("agent_started", container="spens-agent-abc123",
               message="[spens] Starting agent opencode in node-20 ..."),
    ])
    assert out == (
        "[spens] Starting interceptor for session abc123 ...\n"
        "[spens] Waiting for interceptor addon and DNS forwarder ...\n"
        "[spens] Interceptor ready (addon + DNS forwarder up, proxy listening).\n"
        "[spens] Starting agent opencode in node-20 ...\n"
    )


def test_error_renders_verbatim() -> None:
    assert _render([_event("error", message="[spens] Error: boom")]) == "[spens] Error: boom\n"


def test_canceled_keeps_leading_newline() -> None:
    # today's SIGINT notice prints a blank line before the message
    assert _render([_event("canceled", reason="sigint",
                           message="\n[spens] Interrupted, cleaning up ...")]) == \
        "\n[spens] Interrupted, cleaning up ...\n"


def test_agent_output_renders_the_chunk() -> None:
    assert _render([_event("agent_output", chunk="hello from the agent")]) == \
        "hello from the agent\n"


def test_multi_line_message_renders_verbatim() -> None:
    # a multi-line message (e.g. a legacy recap) is still printed as-is
    recap = "line one\nline two"
    final = "[spens] Session sid finished. Traces: /t  Audit: /a  Summary: /s"
    out = _render([_event("finished", exit_code=0, summary_path="/s",
                          message=recap + "\n" + final)])
    assert out == recap + "\n" + final + "\n"


def test_finished_renders_the_summary_panel_from_the_summary_dict() -> None:
    # the finished event carries the recap as machine-readable JSON
    # (data["summary"]); the tty sink renders the panel from that dict.
    summary = {
        "session_id": "sid",
        "duration_seconds": 90,
        "llm": {"turns": 2, "tool_calls": 3, "tokens_in": 1000, "tokens_out": 200,
                "estimated_cost_usd": 1.2345,
                "models": {"claude-opus-4": {"calls": 2, "tokens_in": 1000,
                                             "tokens_out": 200, "cost_usd": 1.2345}}},
        "network": {"http_calls": 4, "request_log_entries": 9},
        "audit": {"event_count": 7},
    }
    message = "[spens] Session sid finished. Traces: /t  Audit: /a  Summary: /s"
    out = _render([_event("finished", exit_code=0, summary_path="/s",
                          summary=summary, message=message)])
    assert "Session recap" in out
    assert "session    sid" in out
    assert "1m 30s" in out
    assert "claude-opus-4" in out
    assert "$1.2345" in out
    # the trailing status line is still the literal message
    assert out.rstrip("\n").endswith(message)
    # captured streams never get ANSI codes
    assert "\x1b" not in out


def test_finished_without_summary_renders_only_the_message() -> None:
    # summary generation failed upstream: the message is still printed
    out = _render([_event("finished", exit_code=1,
                          message="[spens] Session sid finished. Traces: /t")])
    assert out == "[spens] Session sid finished. Traces: /t\n"


def test_finished_falls_back_to_the_event_exit_code() -> None:
    # a summary that could not read the exit code back from the audit
    # state still renders the verdict, taken from the event itself
    out = _render([_event("finished", exit_code=3, summary={"session_id": "sid"},
                          message="[spens] done")])
    assert "FAILED (3)" in out


def test_terminal_stream_styles_the_spens_prefix(monkeypatch) -> None:
    # on a real terminal the [spens] token is highlighted (yellow for
    # warnings) -- and lines are never wrapped at the terminal width
    import spens.sinks.tty as tty_mod
    import spens.ui as ui_mod

    buffer = io.StringIO()
    forced = ui_mod.new_console(buffer, force_terminal=True, width=40)
    monkeypatch.setattr(tty_mod.ui, "new_console", lambda stream=None, **kw: forced)

    long_warning = "[spens] Warning: " + "x" * 60
    sink = tty_mod.TtySink(buffer)
    sink.on_event(_event("warning", message=long_warning))
    out = buffer.getvalue()
    assert "\x1b[1;33m[spens]" in out  # bold yellow prefix on warnings
    assert "x" * 60 in out              # never cropped or wrapped
    assert out.count("\n") == 1


def test_emitter_driven_rendering() -> None:
    # wiring through a real emitter (as run_session does) renders the same
    stream = io.StringIO()
    from spens.sinks import get_sink

    assert isinstance(get_sink("tty"), TtySink)
    emitter = Emitter("sid", [TtySink(stream)])
    emitter.emit("build_started", image="i",
                 message="[spens] Building agent image i ...")
    emitter.close()
    assert stream.getvalue() == "[spens] Building agent image i ...\n"
