"""TUI helpers and headless App.run_test coverage."""
from __future__ import annotations

import asyncio
import threading

import pytest
from rich.text import Text

from nexusos_cli.tui_app import (
    _compact_status,
    _deny_tool_request,
    _escape,
    _status_line,
    format_assistant_line,
    format_user_line,
)


class _ApprovalResponse:
    def raise_for_status(self):
        return None


class _ApprovalClient:
    calls = []

    def __init__(self, **kwargs):
        self.kwargs = kwargs

    def __enter__(self):
        return self

    def __exit__(self, *args):
        return None

    def post(self, path, *, json):
        self.calls.append((path, json, self.kwargs))
        return _ApprovalResponse()


def test_status_line_mentions_services():
    snap = {
        "version": "1.0.0",
        "services": {
            "backend": {"running": True},
            "memory": {"running": False},
            "provider": {"reachable": True},
        },
        "api": {"online": True, "action_tool_policy": "ask"},
        "host": {"cpu_pct": 10.0},
        "toolchains": [{"lang": "python", "ready": True}],
    }
    line = _status_line(snap)
    assert "backend=UP" in line
    assert "memory=DOWN" in line
    assert "provider=UP" in line
    assert "tools=ask" in line
    assert "run=python" in line


def test_compact_status_handles_api_down():
    snap = {
        "host": {},
        "api": {"online": False},
        "recent_tools": [],
    }
    assert "api DOWN" in _compact_status(snap)


def test_escape_preserves_code_brackets_in_display():
    raw = "idx = arr[i] and rng = [a-z]+"
    plain = Text.from_markup(format_assistant_line(raw)).plain
    assert "arr[i]" in plain
    assert "[a-z]+" in plain
    # Unescaped markup would drop the bracket contents.
    assert plain != "nexus> idx = arr and rng = +"


def test_closing_tag_in_model_output_does_not_raise():
    raw = "close with [/] please"
    plain = Text.from_markup(format_assistant_line(raw)).plain
    assert "[/]" in plain


def test_user_line_escapes_markup():
    plain = Text.from_markup(format_user_line("use [bold] please")).plain
    assert "[bold]" in plain


def test_finish_stream_markup_does_not_wedge_busy():
    """A stray '[/]' used to raise before _busy=False and lock the TUI forever."""
    pytest.importorskip("textual")
    from nexusos_cli.tui_app import NexusTUI

    app = NexusTUI.build_app(api_url="http://127.0.0.1:9")

    async def _run():
        async with app.run_test():
            app._busy = True
            app._finish_stream("see [/] and arr[i]")
            assert app._busy is False
            assert app.history[-1]["content"] == "see [/] and arr[i]"

    asyncio.run(_run())


def test_stream_error_remains_visible_after_finish():
    pytest.importorskip("textual")
    from nexusos_cli.tui_app import NexusTUI

    app = NexusTUI.build_app(api_url="http://127.0.0.1:9")

    async def _run():
        async with app.run_test():
            app._busy = True
            app._show_error("[red]Backend not reachable[/]")
            app._finish_stream("")
            log = app.query_one("#log")
            assert any("Backend not reachable" in line.text for line in log.lines)
            assert app._busy is False

    asyncio.run(_run())


@pytest.mark.parametrize("key", ["ctrl+c", "ctrl+d"])
def test_priority_exit_bindings_reach_app_while_prompt_is_focused(key):
    pytest.importorskip("textual")
    from nexusos_cli.tui_app import NexusTUI

    app = NexusTUI.build_app(api_url="http://127.0.0.1:9")

    async def _run():
        async with app.run_test() as pilot:
            assert app.is_running
            await pilot.press(key)
            await pilot.pause()
            assert not app.is_running

    asyncio.run(_run())


def test_tool_request_is_denied_with_stream_token():
    _ApprovalClient.calls.clear()
    names = _deny_tool_request(
        api_url="http://localhost:8000",
        conversation_id="conversation-1",
        payload='{"token":"secret","actions":[{"name":"run_snippet"}]}',
        client_factory=_ApprovalClient,
    )

    assert names == ["run_snippet"]
    path, body, client_kwargs = _ApprovalClient.calls[-1]
    assert path == "/chat/approve"
    assert body == {
        "conversation_id": "conversation-1",
        "token": "secret",
        "decisions": {"run_snippet": False},
    }
    assert client_kwargs["base_url"] == "http://localhost:8000"


def test_inflight_tool_denial_uses_original_conversation_id(monkeypatch):
    pytest.importorskip("textual")
    import nexusos_cli.tui_app as tui_app

    stream_started = threading.Event()
    release_stream = threading.Event()
    denied_for = []

    class _StreamResponse:
        status_code = 200

        async def __aenter__(self):
            return self

        async def __aexit__(self, *args):
            return None

        async def aiter_lines(self):
            stream_started.set()
            await asyncio.to_thread(release_stream.wait, 2)
            yield "event: tool_request"
            yield 'data: {"token":"secret","actions":[{"name":"run_snippet"}]}'
            yield ""
            yield "event: done"
            yield "data: {}"

    class _StreamClient:
        def __init__(self, **kwargs):
            pass

        async def __aenter__(self):
            return self

        async def __aexit__(self, *args):
            return None

        def stream(self, *args, **kwargs):
            return _StreamResponse()

    def _capture_denial(*, conversation_id, **kwargs):
        denied_for.append(conversation_id)
        return ["run_snippet"]

    monkeypatch.setattr(tui_app.httpx, "AsyncClient", _StreamClient)
    monkeypatch.setattr(tui_app, "_deny_tool_request", _capture_denial)
    app = tui_app.NexusTUI.build_app(api_url="http://127.0.0.1:9")

    async def _run():
        async with app.run_test():
            app._start_chat("run it")
            assert await asyncio.to_thread(stream_started.wait, 2)
            original_id = app.conversation_id
            app._handle_slash("/new")
            assert app.conversation_id is None
            release_stream.set()
            for _ in range(200):
                if not app._busy:
                    break
                await asyncio.sleep(0.01)
            assert app._busy is False
            assert denied_for == [original_id]

    asyncio.run(_run())


def test_interrupt_cancels_silent_stream_and_accepts_next_message(monkeypatch):
    pytest.importorskip("textual")
    import nexusos_cli.tui_app as tui_app

    first_stream_started = threading.Event()

    class _StreamResponse:
        status_code = 200

        def __init__(self, call_number):
            self.call_number = call_number

        async def __aenter__(self):
            return self

        async def __aexit__(self, *args):
            return None

        async def aiter_lines(self):
            if self.call_number == 1:
                first_stream_started.set()
                await asyncio.Event().wait()
            yield "data: \"READY\""
            yield ""
            yield "event: done"
            yield "data: {}"

    class _StreamClient:
        calls = 0

        def __init__(self, **kwargs):
            pass

        async def __aenter__(self):
            return self

        async def __aexit__(self, *args):
            return None

        def stream(self, *args, **kwargs):
            type(self).calls += 1
            return _StreamResponse(type(self).calls)

    monkeypatch.setattr(tui_app.httpx, "AsyncClient", _StreamClient)
    app = tui_app.NexusTUI.build_app(api_url="http://127.0.0.1:9")

    async def _wait_until_idle():
        for _ in range(100):
            if not app._busy:
                return
            await asyncio.sleep(0.01)
        pytest.fail("stream did not become idle within one second")

    async def _run():
        async with app.run_test() as pilot:
            app._start_chat("first")
            assert await asyncio.to_thread(first_stream_started.wait, 2)
            await pilot.press("ctrl+c")
            await _wait_until_idle()

            log = app.query_one("#log")
            assert any("interrupt requested" in line.text for line in log.lines)
            assert not any("ReadTimeout" in line.text for line in log.lines)

            app._start_chat("second")
            await _wait_until_idle()
            assert app.history[-1] == {
                "role": "assistant",
                "content": "READY",
            }

    asyncio.run(_run())


def test_escape_round_trip_helper():
    assert "[" in _escape("x[y]") or "\\[" in _escape("x[y]")


def test_slash_tool_call_shape_forwards_to_start_chat(monkeypatch):
    """/tool_name(arg=val) isn't a local meta-command — it must reach the
    backend (synapse/slash_commands.py + chat_stream_endpoint dispatch it),
    not fall into the generic 'unknown command' branch."""
    pytest.importorskip("textual")
    from nexusos_cli.tui_app import NexusTUI

    app = NexusTUI.build_app(api_url="http://127.0.0.1:9")
    calls: list[str] = []
    monkeypatch.setattr(app, "_start_chat", lambda text: calls.append(text))

    async def _run():
        async with app.run_test():
            text = '/curry_call_function(name="double", version=1, args={"x": 21})'
            app._handle_slash(text)
            assert calls == [text]
            log = app.query_one("#log")
            assert not any("unknown command" in line.text for line in log.lines)

    asyncio.run(_run())


def test_slash_malformed_tool_call_still_forwards_for_the_backend_error(monkeypatch):
    """Even a malformed /tool(...) is forwarded rather than swallowed locally
    — the backend's parser gives a clearer, more specific error than the
    TUI's generic 'unknown command' would."""
    pytest.importorskip("textual")
    from nexusos_cli.tui_app import NexusTUI

    app = NexusTUI.build_app(api_url="http://127.0.0.1:9")
    calls: list[str] = []
    monkeypatch.setattr(app, "_start_chat", lambda text: calls.append(text))

    async def _run():
        async with app.run_test():
            text = "/curry_call_function(x=__import__('os'))"
            app._handle_slash(text)
            assert calls == [text]

    asyncio.run(_run())


def test_slash_local_meta_commands_still_handled_locally(monkeypatch):
    """A known local command must still be handled in-TUI, never forwarded —
    the new tool-call passthrough is strictly the fallback branch."""
    pytest.importorskip("textual")
    from nexusos_cli.tui_app import NexusTUI

    app = NexusTUI.build_app(api_url="http://127.0.0.1:9")
    calls: list[str] = []
    monkeypatch.setattr(app, "_start_chat", lambda text: calls.append(text))

    async def _run():
        async with app.run_test():
            app._handle_slash("/help")
            assert calls == []
            log = app.query_one("#log")
            assert any("this list" in line.text for line in log.lines)

    asyncio.run(_run())


def test_slash_unknown_bare_command_still_rejected(monkeypatch):
    """A genuinely unknown command (no parens, not a local command) keeps the
    existing 'unknown command' behavior rather than silently forwarding
    anything that starts with /."""
    pytest.importorskip("textual")
    from nexusos_cli.tui_app import NexusTUI

    app = NexusTUI.build_app(api_url="http://127.0.0.1:9")
    calls: list[str] = []
    monkeypatch.setattr(app, "_start_chat", lambda text: calls.append(text))

    async def _run():
        async with app.run_test():
            app._handle_slash("/frobnicate")
            assert calls == []
            log = app.query_one("#log")
            assert any("unknown command" in line.text for line in log.lines)

    asyncio.run(_run())
