"""Tests for the Navi Code terminal client."""

from __future__ import annotations

import json
from pathlib import Path

import pytest
from click.testing import CliRunner

from clients.terminal.cli import main, _resolve_session_id, _resume_hint
from clients.terminal.config import Settings
from clients.terminal.render import Renderer
from clients.terminal.state import StateManager


class TestStateManager:
    def test_load_missing_returns_empty(self, tmp_path: Path) -> None:
        mgr = StateManager(tmp_path)
        assert mgr.load() == {}
        assert mgr.get_session_id() is None

    def test_roundtrip_session_id(self, tmp_path: Path) -> None:
        mgr = StateManager(tmp_path)
        mgr.set_session_id("sess-123")
        assert mgr.get_session_id() == "sess-123"
        assert (tmp_path / "state.json").exists()
        data = json.loads((tmp_path / "state.json").read_text())
        assert data == {"session_id": "sess-123"}

    def test_clear_session_id(self, tmp_path: Path) -> None:
        mgr = StateManager(tmp_path)
        mgr.set_session_id("sess-123")
        mgr.clear_session_id()
        assert mgr.get_session_id() is None


class TestRenderer:
    def test_stream_delta_prints_inline(self, capsys) -> None:
        renderer = Renderer()
        renderer.render({"type": "stream_delta", "delta": "hello"})
        captured = capsys.readouterr()
        assert "hello" in captured.out

    def test_error_prints_red(self, capsys) -> None:
        renderer = Renderer()
        renderer.render({"type": "error", "message": "boom"})
        captured = capsys.readouterr()
        assert "boom" in captured.out

    def test_tool_started_shown_when_events_enabled(self, capsys) -> None:
        renderer = Renderer(show_events=True)
        renderer.render({"type": "tool_started", "tool": "terminal", "args": {"cmd": "ls"}})
        captured = capsys.readouterr()
        assert "terminal" in captured.out

    def test_tool_started_hidden_when_events_disabled(self, capsys) -> None:
        renderer = Renderer(show_events=False)
        renderer.render({"type": "tool_started", "tool": "terminal", "args": {"cmd": "ls"}})
        captured = capsys.readouterr()
        assert captured.out == ""


class TestSettings:
    def test_websocket_url_converts_http_to_ws(self) -> None:
        s = Settings(base_url="http://localhost:8000")
        assert s.websocket_url("abc") == "ws://localhost:8000/ws/sessions/abc"

    def test_websocket_url_converts_https_to_wss(self) -> None:
        s = Settings(base_url="https://navi.example.com")
        assert s.websocket_url("abc") == "wss://navi.example.com/ws/sessions/abc"

    def test_websocket_url_uses_explicit_ws_url(self) -> None:
        s = Settings(ws_url="ws://custom:9000")
        assert s.websocket_url("abc") == "ws://custom:9000/ws/sessions/abc"


class TestCliRunner:
    def test_help_shows_usage(self) -> None:
        runner = CliRunner()
        result = runner.invoke(main, ["--help"])
        assert result.exit_code == 0
        assert "Navi Code" in result.output

    def test_help_short_flag_shows_usage(self) -> None:
        """-h must show help, not fall through to the prompt argument.

        Regression: with ignore_unknown_options=True, -h was not recognized as
        the help flag and was sent to Navi as a prompt instead.
        """
        runner = CliRunner()
        result = runner.invoke(main, ["-h"])
        assert result.exit_code == 0
        assert "Navi Code" in result.output

    def test_version_shows_version(self) -> None:
        runner = CliRunner()
        result = runner.invoke(main, ["--version"])
        assert result.exit_code == 0
        assert "0.1.0" in result.output

    def test_raw_mode_uses_session_id_and_name_fields(
        self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
    ) -> None:
        """Raw CLI must read session_id/name/preview from the server API."""

        class FakeWsClient:
            def __init__(self, session_id: str, renderer=None, cwd=None) -> None:
                self.session_id = session_id
                self.cwd = cwd

            async def run_one_shot(self, prompt: str) -> None:
                pass

        monkeypatch.setattr("clients.terminal.cli.NaviWebSocketClient", FakeWsClient)

        from clients.terminal import config

        original_state_dir = config.settings.state_dir
        config.settings.state_dir = tmp_path

        def fake_create_session(profile_id: str | None = None) -> dict:
            return {
                "session_id": "sess-raw-1234",
                "profile_id": profile_id or "navi_code",
            }

        def fake_get_session(session_id: str) -> dict:
            return {
                "session_id": session_id,
                "profile_id": "navi_code",
                "name": "Raw session",
            }

        monkeypatch.setattr("clients.terminal.api.create_session", fake_create_session)
        monkeypatch.setattr("clients.terminal.api.get_session", fake_get_session)

        state = StateManager(tmp_path)
        state.set_session_id("sess-raw-1234")
        runner = CliRunner()
        try:
            result = runner.invoke(main, ["--raw", "--base-url", "http://localhost:8000", "hello"])
            assert result.exit_code == 0
            assert "Resumed session sess-raw" in result.output
        finally:
            config.settings.state_dir = original_state_dir

    def test_resume_hint_format(self) -> None:
        assert _resume_hint("sess-123-abc") == "navi-code --resume sess-123-abc"

    def test_profile_command_uses_session_id(
        self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture
    ) -> None:
        """``/profile`` must read ``session_id`` from the server, not ``id``
        (the backend never sends ``id``). Regression for a KeyError crash."""
        from clients.terminal.cli import _handle_command

        monkeypatch.setattr(
            "clients.terminal.api.get_session",
            lambda sid: {"session_id": "sess-abc-123", "profile_id": "navi_code"},
        )
        state = StateManager(tmp_path)
        state.set_session_id("sess-abc-123")
        import asyncio

        asyncio.run(_handle_command("/profile", state, client=object()))
        out = capsys.readouterr().out
        assert "Profile: navi_code" in out
        assert "Session: sess-abc-123" in out

    def test_resume_flag_resumes_given_session(
        self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
    ) -> None:
        """--resume <id> resolves that exact session and persists it to state."""
        import clients.terminal.api as api_module

        monkeypatch.setattr(api_module, "get_session", lambda sid: {
            "session_id": sid,
            "profile_id": "navi_code",
            "name": "Resumed",
        })

        from clients.terminal import config

        original_state_dir = config.settings.state_dir
        config.settings.state_dir = tmp_path
        try:
            state = StateManager(tmp_path)
            session_id = _resolve_session_id(state, force_new=False, profile_id=None, resume_session_id="sess-resume-xyz")
            assert session_id == "sess-resume-xyz"
            assert state.get_session_id() == "sess-resume-xyz"
        finally:
            config.settings.state_dir = original_state_dir

    def test_resume_flag_with_bad_session_errors(
        self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
    ) -> None:
        """A --resume id that the server can't find is a hard error, not a silent new session."""
        import clients.terminal.api as api_module

        def boom(_sid: str) -> dict:
            raise RuntimeError("session not found")

        monkeypatch.setattr(api_module, "get_session", boom)
        monkeypatch.setattr(api_module, "create_session", lambda *_a, **_k: {"session_id": "NEW", "profile_id": "navi_code"})

        import click
        state = StateManager(tmp_path)
        with pytest.raises(click.ClickException, match="Failed to resume"):
            _resolve_session_id(state, force_new=False, profile_id=None, resume_session_id="sess-bad")
        # Must NOT have created/persisted a new session behind the user's back.
        assert state.get_session_id() is None

    def test_resume_and_new_session_are_mutually_exclusive(
        self, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        import clients.terminal.api as api_module

        monkeypatch.setattr(api_module, "create_session", lambda *_a, **_k: {"session_id": "x", "profile_id": "navi_code"})
        monkeypatch.setattr(api_module, "get_session", lambda sid: {"session_id": sid, "profile_id": "navi_code"})

        runner = CliRunner()
        result = runner.invoke(main, ["--resume", "sess-x", "--new-session"])
        assert result.exit_code != 0
        assert "mutually exclusive" in result.output
