from argparse import Namespace
import os
from pathlib import Path
import subprocess
import sys
import textwrap
import types

import pytest


def _args(**overrides):
    base = {
        "continue_last": None,
        "model": None,
        "provider": None,
        "resume": None,
        "toolsets": None,
        "tui": True,
        "tui_dev": False,
    }
    base.update(overrides)
    return Namespace(**base)


def _raise_exit(rc):
    raise SystemExit(rc)


@pytest.fixture
def main_mod(monkeypatch):
    import hermes_cli.main as mod

    monkeypatch.setattr(mod, "_has_any_provider_configured", lambda: True)
    # Reset the idempotency guard so each test starts fresh.
    monkeypatch.setattr(mod, "_oneshot_cleanup_done", False)
    return mod


def test_cmd_chat_tui_continue_uses_latest_tui_session(monkeypatch, main_mod):
    calls = []
    captured = {}

    def fake_resolve_last(source="cli"):
        calls.append(source)
        return "20260408_235959_a1b2c3" if source == "tui" else None

    def fake_launch(
        resume_session_id=None,
        tui_dev=False,
        model=None,
        provider=None,
        toolsets=None,
        **kwargs,
    ):
        captured["resume"] = resume_session_id
        raise SystemExit(0)

    monkeypatch.setattr(main_mod, "_resolve_last_session", fake_resolve_last)
    monkeypatch.setattr(main_mod, "_resolve_session_by_name_or_id", lambda val: val)
    monkeypatch.setattr(main_mod, "_launch_tui", fake_launch)

    with pytest.raises(SystemExit):
        main_mod.cmd_chat(_args(continue_last=True))

    assert calls == ["tui"]
    assert captured["resume"] == "20260408_235959_a1b2c3"


def test_cmd_chat_tui_continue_falls_back_to_latest_cli_session(monkeypatch, main_mod):
    calls = []
    captured = {}

    def fake_resolve_last(source="cli"):
        calls.append(source)
        if source == "tui":
            return None
        if source == "cli":
            return "20260408_235959_d4e5f6"
        return None

    def fake_launch(
        resume_session_id=None,
        tui_dev=False,
        model=None,
        provider=None,
        toolsets=None,
        **kwargs,
    ):
        captured["resume"] = resume_session_id
        raise SystemExit(0)

    monkeypatch.setattr(main_mod, "_resolve_last_session", fake_resolve_last)
    monkeypatch.setattr(main_mod, "_resolve_session_by_name_or_id", lambda val: val)
    monkeypatch.setattr(main_mod, "_launch_tui", fake_launch)

    with pytest.raises(SystemExit):
        main_mod.cmd_chat(_args(continue_last=True))

    assert calls == ["tui", "cli"]
    assert captured["resume"] == "20260408_235959_d4e5f6"


def test_cmd_chat_tui_resume_resolves_title_before_launch(monkeypatch, main_mod):
    captured = {}

    def fake_launch(
        resume_session_id=None,
        tui_dev=False,
        model=None,
        provider=None,
        toolsets=None,
        **kwargs,
    ):
        captured["resume"] = resume_session_id
        raise SystemExit(0)

    monkeypatch.setattr(
        main_mod, "_resolve_session_by_name_or_id", lambda val: "20260409_000000_aa11bb"
    )
    monkeypatch.setattr(main_mod, "_launch_tui", fake_launch)

    with pytest.raises(SystemExit):
        main_mod.cmd_chat(_args(resume="my t0p session"))

    assert captured["resume"] == "20260409_000000_aa11bb"


def test_cmd_chat_tui_passes_model_and_provider(monkeypatch, main_mod):
    captured = {}

    def fake_launch(
        resume_session_id=None,
        tui_dev=False,
        model=None,
        provider=None,
        toolsets=None,
        **kwargs,
    ):
        captured.update(
            {
                "model": model,
                "provider": provider,
                "resume": resume_session_id,
                "toolsets": toolsets,
                "tui_dev": tui_dev,
            }
        )
        raise SystemExit(0)

    monkeypatch.setattr(main_mod, "_launch_tui", fake_launch)

    with pytest.raises(SystemExit):
        main_mod.cmd_chat(
            _args(model="anthropic/claude-sonnet-4.6", provider="anthropic")
        )

    assert captured == {
        "model": "anthropic/claude-sonnet-4.6",
        "provider": "anthropic",
        "resume": None,
        "toolsets": None,
        "tui_dev": False,
    }


def test_cmd_chat_tui_passes_toolsets(monkeypatch, main_mod):
    captured = {}

    def fake_launch(
        resume_session_id=None,
        tui_dev=False,
        model=None,
        provider=None,
        toolsets=None,
        **kwargs,
    ):
        captured["toolsets"] = toolsets
        raise SystemExit(0)

    monkeypatch.setattr(main_mod, "_launch_tui", fake_launch)

    with pytest.raises(SystemExit):
        main_mod.cmd_chat(_args(toolsets="web,terminal"))

    assert captured["toolsets"] == "web,terminal"


def test_cmd_chat_tui_forwards_chat_flags(monkeypatch, main_mod):
    captured = {}

    def fake_launch(resume_session_id=None, **kwargs):
        captured["resume_session_id"] = resume_session_id
        captured.update(kwargs)
        raise SystemExit(0)

    monkeypatch.setattr(main_mod, "_launch_tui", fake_launch)

    with pytest.raises(SystemExit):
        main_mod.cmd_chat(
            _args(
                skills=["foo,bar"],
                verbose=True,
                quiet=True,
                query="hello",
                image="/tmp/cat.png",
                worktree=True,
                checkpoints=True,
                pass_session_id=True,
                max_turns=7,
                accept_hooks=True,
            )
        )

    assert captured["skills"] == ["foo,bar"]
    assert captured["verbose"] is True
    assert captured["quiet"] is True
    assert captured["query"] == "hello"
    assert captured["image"] == "/tmp/cat.png"
    assert captured["worktree"] is True
    assert captured["checkpoints"] is True
    assert captured["pass_session_id"] is True
    assert captured["max_turns"] == 7
    assert captured["accept_hooks"] is True


def test_main_top_level_tui_accepts_toolsets(monkeypatch, main_mod):
    captured = {}

    import hermes_cli.config as config_mod

    monkeypatch.setattr(sys, "argv", ["hermes", "--tui", "--toolsets", "web,terminal"])
    monkeypatch.setitem(
        sys.modules,
        "hermes_cli.plugins",
        types.SimpleNamespace(discover_plugins=lambda: None),
    )
    monkeypatch.setitem(
        sys.modules,
        "tools.mcp_tool",
        types.SimpleNamespace(discover_mcp_tools=lambda: None),
    )
    monkeypatch.setattr(config_mod, "load_config", lambda: {})
    monkeypatch.setattr(config_mod, "get_container_exec_info", lambda: None)
    monkeypatch.setitem(
        sys.modules,
        "agent.shell_hooks",
        types.SimpleNamespace(
            register_from_config=lambda _cfg, accept_hooks=False: None
        ),
    )
    monkeypatch.setattr(
        main_mod,
        "cmd_chat",
        lambda args: captured.update({"toolsets": args.toolsets, "tui": args.tui}),
    )

    main_mod.main()

    assert captured == {"toolsets": "web,terminal", "tui": True}


def test_termux_fast_tui_launch_uses_light_parser(monkeypatch, main_mod):
    captured = {}

    monkeypatch.setenv("TERMUX_VERSION", "1")
    monkeypatch.setattr(
        sys, "argv", ["hermes", "--tui", "--toolsets", "web,terminal"]
    )
    monkeypatch.setattr(
        main_mod,
        "cmd_chat",
        lambda args: captured.update({"toolsets": args.toolsets, "tui": args.tui}),
    )

    assert main_mod._try_termux_fast_tui_launch() is True
    assert captured == {"toolsets": "web,terminal", "tui": True}


def test_termux_fast_tui_launch_skips_help(monkeypatch, main_mod):
    monkeypatch.setenv("TERMUX_VERSION", "1")
    monkeypatch.setattr(sys, "argv", ["hermes", "--tui", "--help"])

    assert main_mod._try_termux_fast_tui_launch() is False


def test_fast_tui_launch_is_termux_only(monkeypatch, main_mod):
    monkeypatch.delenv("TERMUX_VERSION", raising=False)
    monkeypatch.setenv("PREFIX", "/usr")
    monkeypatch.setattr(sys, "argv", ["hermes", "--tui"])

    assert main_mod._try_termux_fast_tui_launch() is False


def test_termux_fast_cli_launch_chat_uses_light_parser(monkeypatch, main_mod):
    captured = {}
    prepared = []

    monkeypatch.setenv("TERMUX_VERSION", "1")
    monkeypatch.delenv("HERMES_TUI", raising=False)
    monkeypatch.setattr(
        sys, "argv", ["hermes", "chat", "-q", "hello", "--toolsets", "web,terminal"]
    )
    monkeypatch.setattr(
        main_mod, "_prepare_agent_startup", lambda args: prepared.append(args.command)
    )
    monkeypatch.setattr(
        main_mod,
        "cmd_chat",
        lambda args: captured.update(
            {"query": args.query, "toolsets": args.toolsets, "command": args.command}
        ),
    )

    assert main_mod._try_termux_fast_cli_launch() is True
    assert prepared == ["chat"]
    assert captured == {
        "query": "hello",
        "toolsets": "web,terminal",
        "command": "chat",
    }


def test_termux_fast_cli_launch_bare_defers_agent_startup(monkeypatch, main_mod):
    captured = {}
    prepared = []

    monkeypatch.setenv("TERMUX_VERSION", "1")
    monkeypatch.delenv("HERMES_TUI", raising=False)
    monkeypatch.delenv("HERMES_DEFER_AGENT_STARTUP", raising=False)
    monkeypatch.delenv("HERMES_FAST_STARTUP_BANNER", raising=False)
    monkeypatch.setattr(sys, "argv", ["hermes"])
    monkeypatch.setattr(
        main_mod, "_prepare_agent_startup", lambda args: prepared.append(args.command)
    )
    monkeypatch.setattr(
        main_mod,
        "cmd_chat",
        lambda args: captured.update(
            {
                "query": args.query,
                "command": args.command,
                "compact": getattr(args, "compact", False),
            }
        ),
    )

    assert main_mod._try_termux_fast_cli_launch() is True
    assert prepared == []
    assert captured == {"query": None, "command": None, "compact": True}
    assert os.environ["HERMES_DEFER_AGENT_STARTUP"] == "1"
    assert os.environ["HERMES_FAST_STARTUP_BANNER"] == "1"


def test_termux_fast_cli_launch_oneshot_uses_light_parser(monkeypatch, main_mod):
    captured = {}
    prepared = []

    monkeypatch.setenv("TERMUX_VERSION", "1")
    monkeypatch.delenv("HERMES_TUI", raising=False)
    monkeypatch.setattr(
        sys,
        "argv",
        [
            "hermes",
            "-z",
            "hello",
            "--model",
            "gpt-test",
            "--provider",
            "openai",
            "--usage-file",
            "usage.json",
        ],
    )
    monkeypatch.setattr(
        main_mod, "_prepare_agent_startup", lambda args: prepared.append(args.command)
    )
    monkeypatch.setitem(
        sys.modules,
        "hermes_cli.oneshot",
        types.SimpleNamespace(
            run_oneshot=lambda prompt, **kwargs: captured.update(
                {"prompt": prompt, **kwargs}
            )
            or 17
        ),
    )
    monkeypatch.setattr(
        main_mod,
        "_exit_after_oneshot",
        _raise_exit,
    )

    with pytest.raises(SystemExit) as exc:
        main_mod._try_termux_fast_cli_launch()

    assert exc.value.code == 17
    assert prepared == [None]
    assert captured == {
        "prompt": "hello",
        "model": "gpt-test",
        "provider": "openai",
        "toolsets": None,
        "usage_file": "usage.json",
    }


def test_termux_fast_cli_launch_version_skips_update_check(monkeypatch, main_mod):
    captured = []

    monkeypatch.setenv("TERMUX_VERSION", "1")
    monkeypatch.delenv("HERMES_TUI", raising=False)
    monkeypatch.setattr(sys, "argv", ["hermes", "version"])
    monkeypatch.setattr(
        main_mod, "_print_version_info", lambda *, check_updates: captured.append(check_updates)
    )

    assert main_mod._try_termux_fast_cli_launch() is True
    assert captured == [False]


def test_termux_ultrafast_version_runs_before_heavy_startup(
    monkeypatch, capsys, main_mod
):
    monkeypatch.setenv("TERMUX_VERSION", "1")
    monkeypatch.delenv("HERMES_TERMUX_DISABLE_FAST_CLI", raising=False)
    monkeypatch.setattr(sys, "argv", ["hermes", "--version"])

    assert main_mod._try_termux_ultrafast_version() is True

    out = capsys.readouterr().out
    assert "Hermes Agent v" in out
    assert "Install directory:" in out
    assert "Python:" in out
    assert "OpenAI SDK:" in out


def test_read_openai_version_fast(monkeypatch, tmp_path, main_mod):
    package_dir = tmp_path / "openai"
    package_dir.mkdir()
    (package_dir / "_version.py").write_text(
        '__version__ = "9.8.7"  # x-release-please-version\n',
        encoding="utf-8",
    )
    monkeypatch.setattr(sys, "path", [str(tmp_path)])

    assert main_mod._read_openai_version_fast() == "9.8.7"


def test_termux_fast_cli_launch_skips_help(monkeypatch, main_mod):
    monkeypatch.setenv("TERMUX_VERSION", "1")
    monkeypatch.delenv("HERMES_TUI", raising=False)
    monkeypatch.setattr(sys, "argv", ["hermes", "chat", "--help"])

    assert main_mod._try_termux_fast_cli_launch() is False


def test_termux_fast_cli_launch_can_be_disabled(monkeypatch, main_mod):
    monkeypatch.setenv("TERMUX_VERSION", "1")
    monkeypatch.setenv("HERMES_TERMUX_DISABLE_FAST_CLI", "1")
    monkeypatch.delenv("HERMES_TUI", raising=False)
    monkeypatch.setattr(sys, "argv", ["hermes", "version"])

    assert main_mod._try_termux_fast_cli_launch() is False


def test_termux_bundled_skills_stamp_controls_sync(monkeypatch, tmp_path, main_mod):
    monkeypatch.setenv("TERMUX_VERSION", "1")
    monkeypatch.setattr(main_mod, "get_hermes_home", lambda: tmp_path)
    monkeypatch.setattr(main_mod, "_termux_bundled_skills_fingerprint", lambda: "fp1")

    assert main_mod._termux_bundled_skills_sync_needed() is True
    main_mod._mark_termux_bundled_skills_synced()
    assert main_mod._termux_bundled_skills_sync_needed() is False

    monkeypatch.setenv("HERMES_TERMUX_FORCE_SKILLS_SYNC", "1")
    assert main_mod._termux_bundled_skills_sync_needed() is True


def test_termux_skips_bundled_skill_sync_when_stamp_fresh(monkeypatch, tmp_path, main_mod):
    calls = []

    monkeypatch.setenv("TERMUX_VERSION", "1")
    monkeypatch.setattr(main_mod, "get_hermes_home", lambda: tmp_path)
    monkeypatch.setattr(main_mod, "_termux_bundled_skills_fingerprint", lambda: "fp1")
    main_mod._mark_termux_bundled_skills_synced()
    monkeypatch.setitem(
        sys.modules,
        "tools.skills_sync",
        types.SimpleNamespace(sync_skills=lambda quiet: calls.append(quiet)),
    )

    assert main_mod._sync_bundled_skills_for_startup() is False
    assert calls == []


def test_termux_forced_bundled_skill_sync_runs(monkeypatch, tmp_path, main_mod):
    calls = []

    monkeypatch.setenv("TERMUX_VERSION", "1")
    monkeypatch.setenv("HERMES_TERMUX_FORCE_SKILLS_SYNC", "1")
    monkeypatch.setattr(main_mod, "get_hermes_home", lambda: tmp_path)
    monkeypatch.setattr(main_mod, "_termux_bundled_skills_fingerprint", lambda: "fp1")
    monkeypatch.setitem(
        sys.modules,
        "tools.skills_sync",
        types.SimpleNamespace(sync_skills=lambda quiet: calls.append(quiet)),
    )

    assert main_mod._sync_bundled_skills_for_startup() is True
    assert calls == [True]


def test_read_git_revision_fingerprint_resolves_packed_refs(tmp_path, main_mod):
    repo = tmp_path / "repo"
    git_dir = repo / ".git"
    git_dir.mkdir(parents=True)
    (git_dir / "HEAD").write_text("ref: refs/heads/main\n", encoding="utf-8")
    packed_sha = "1234567890abcdef1234567890abcdef12345678"
    (git_dir / "packed-refs").write_text(
        "# pack-refs with: peeled fully-peeled sorted\n"
        f"{packed_sha} refs/heads/main\n"
        "abcdef0000000000000000000000000000000000 refs/tags/v1.0\n"
        "^99999999aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n",
        encoding="utf-8",
    )

    fingerprint = main_mod._read_git_revision_fingerprint(repo)

    assert fingerprint == f"git:refs/heads/main:{packed_sha}"


def test_read_git_revision_fingerprint_packed_refs_in_worktree_common_dir(
    tmp_path, main_mod
):
    main_repo = tmp_path / "repo"
    common_git = main_repo / ".git"
    common_git.mkdir(parents=True)
    packed_sha = "fedcba9876543210fedcba9876543210fedcba98"
    (common_git / "packed-refs").write_text(
        f"{packed_sha} refs/heads/main\n",
        encoding="utf-8",
    )

    worktree = tmp_path / "wt"
    worktree.mkdir()
    wt_gitdir = common_git / "worktrees" / "wt"
    wt_gitdir.mkdir(parents=True)
    (wt_gitdir / "HEAD").write_text("ref: refs/heads/main\n", encoding="utf-8")
    (wt_gitdir / "commondir").write_text("../..\n", encoding="utf-8")
    (worktree / ".git").write_text(f"gitdir: {wt_gitdir}\n", encoding="utf-8")

    fingerprint = main_mod._read_git_revision_fingerprint(worktree)

    assert fingerprint == f"git:refs/heads/main:{packed_sha}"


def test_read_git_revision_fingerprint_loose_ref_in_worktree_common_dir(
    tmp_path, main_mod
):
    """`git worktree add -b NAME` writes the new branch ref to the common dir,
    not the per-worktree gitdir. The fingerprint must still resolve it."""
    main_repo = tmp_path / "repo"
    common_git = main_repo / ".git"
    common_git.mkdir(parents=True)
    loose_sha = "0123456789abcdef0123456789abcdef01234567"
    (common_git / "refs" / "heads").mkdir(parents=True)
    (common_git / "refs" / "heads" / "feature").write_text(
        loose_sha + "\n", encoding="utf-8"
    )

    worktree = tmp_path / "wt"
    worktree.mkdir()
    wt_gitdir = common_git / "worktrees" / "wt"
    wt_gitdir.mkdir(parents=True)
    (wt_gitdir / "HEAD").write_text("ref: refs/heads/feature\n", encoding="utf-8")
    (wt_gitdir / "commondir").write_text("../..\n", encoding="utf-8")
    (worktree / ".git").write_text(f"gitdir: {wt_gitdir}\n", encoding="utf-8")

    fingerprint = main_mod._read_git_revision_fingerprint(worktree)

    assert fingerprint == f"git:refs/heads/feature:{loose_sha}"


def test_read_git_revision_fingerprint_unresolved_ref_is_stable(tmp_path, main_mod):
    repo = tmp_path / "repo"
    git_dir = repo / ".git"
    git_dir.mkdir(parents=True)
    (git_dir / "HEAD").write_text("ref: refs/heads/missing\n", encoding="utf-8")

    fingerprint = main_mod._read_git_revision_fingerprint(repo)

    assert fingerprint == "git:refs/heads/missing:unresolved"


def test_main_top_level_oneshot_accepts_toolsets(monkeypatch, main_mod):
    captured = {}

    import hermes_cli.config as config_mod

    monkeypatch.setattr(
        sys,
        "argv",
        [
            "hermes",
            "-z",
            "hello",
            "--toolsets",
            "web,terminal",
            "--usage-file",
            "usage.json",
        ],
    )
    monkeypatch.setitem(
        sys.modules,
        "hermes_cli.plugins",
        types.SimpleNamespace(discover_plugins=lambda: None),
    )
    monkeypatch.setitem(
        sys.modules,
        "tools.mcp_tool",
        types.SimpleNamespace(discover_mcp_tools=lambda: None),
    )
    monkeypatch.setattr(config_mod, "load_config", lambda: {})
    monkeypatch.setattr(config_mod, "get_container_exec_info", lambda: None)
    monkeypatch.setitem(
        sys.modules,
        "agent.shell_hooks",
        types.SimpleNamespace(
            register_from_config=lambda _cfg, accept_hooks=False: None
        ),
    )
    monkeypatch.setitem(
        sys.modules,
        "hermes_cli.oneshot",
        types.SimpleNamespace(
            run_oneshot=lambda prompt, **kwargs: captured.update(
                {"prompt": prompt, **kwargs}
            )
            or 0
        ),
    )
    monkeypatch.setattr(
        main_mod,
        "_exit_after_oneshot",
        _raise_exit,
    )

    with pytest.raises(SystemExit) as exc:
        main_mod.main()

    assert exc.value.code == 0
    assert captured == {
        "prompt": "hello",
        "model": None,
        "provider": None,
        "toolsets": "web,terminal",
        "usage_file": "usage.json",
    }


def test_exit_after_oneshot_flushes_stdio_and_calls_os_exit(
    monkeypatch, main_mod
):
    flushed = []
    exits = []

    class FakeStream:
        def __init__(self, name):
            self.name = name

        def flush(self):
            flushed.append(self.name)

    def fake_exit(rc):
        exits.append(rc)
        raise SystemExit(rc)

    monkeypatch.setattr(main_mod.sys, "stdout", FakeStream("stdout"))
    monkeypatch.setattr(main_mod.sys, "stderr", FakeStream("stderr"))
    monkeypatch.setattr(main_mod.os, "_exit", fake_exit)
    monkeypatch.setattr("logging.shutdown", lambda: None)

    with pytest.raises(SystemExit) as exc:
        main_mod._exit_after_oneshot(17)

    assert exc.value.code == 17
    assert exits == [17]
    assert flushed == ["stdout", "stderr"]


def test_exit_after_oneshot_invokes_logging_shutdown_in_order(
    monkeypatch, main_mod
):
    events = []

    class FakeStream:
        def __init__(self, name):
            self.name = name

        def flush(self):
            events.append(f"flush:{self.name}")

    def fake_exit(rc):
        events.append(f"exit:{rc}")
        raise SystemExit(rc)

    monkeypatch.setattr(main_mod.sys, "stdout", FakeStream("stdout"))
    monkeypatch.setattr(main_mod.sys, "stderr", FakeStream("stderr"))
    monkeypatch.setattr("logging.shutdown", lambda: events.append("shutdown"))
    monkeypatch.setattr(main_mod.os, "_exit", fake_exit)

    with pytest.raises(SystemExit) as exc:
        main_mod._exit_after_oneshot(0)

    assert exc.value.code == 0
    assert events == ["flush:stdout", "flush:stderr", "shutdown", "exit:0"]


def test_exit_after_oneshot_exits_even_if_logging_shutdown_raises(
    monkeypatch, main_mod
):
    exits = []

    def fake_exit(rc):
        exits.append(rc)
        raise SystemExit(rc)

    monkeypatch.setattr(
        main_mod.sys, "stdout", types.SimpleNamespace(flush=lambda: None)
    )
    monkeypatch.setattr(
        main_mod.sys, "stderr", types.SimpleNamespace(flush=lambda: None)
    )
    monkeypatch.setattr(
        "logging.shutdown",
        lambda: (_ for _ in ()).throw(RuntimeError("shutdown failed")),
    )
    monkeypatch.setattr(main_mod.os, "_exit", fake_exit)

    with pytest.raises(SystemExit) as exc:
        main_mod._exit_after_oneshot(1)

    assert exc.value.code == 1
    assert exits == [1]


def test_exit_after_oneshot_flushes_stderr_when_stdout_flush_fails(
    monkeypatch, main_mod
):
    flushed = []
    exits = []

    class BadStdout:
        def flush(self):
            raise BrokenPipeError("pipe closed")

    class FakeStderr:
        def flush(self):
            flushed.append("stderr")

    def fake_exit(rc):
        exits.append(rc)
        raise SystemExit(rc)

    monkeypatch.setattr(main_mod.sys, "stdout", BadStdout())
    monkeypatch.setattr(main_mod.sys, "stderr", FakeStderr())
    monkeypatch.setattr(main_mod.os, "_exit", fake_exit)
    monkeypatch.setattr("logging.shutdown", lambda: None)

    with pytest.raises(SystemExit) as exc:
        main_mod._exit_after_oneshot(2)

    assert exc.value.code == 2
    assert exits == [2]
    assert flushed == ["stderr"]


def test_exit_after_oneshot_normalizes_non_int_exit_code(monkeypatch, main_mod):
    exits = []

    def fake_exit(rc):
        exits.append(rc)
        raise SystemExit(rc)

    monkeypatch.setattr(main_mod.os, "_exit", fake_exit)
    monkeypatch.setattr("logging.shutdown", lambda: None)

    with pytest.raises(SystemExit) as exc:
        main_mod._exit_after_oneshot(None)

    assert exc.value.code == 0
    assert exits == [0]


def test_run_and_exit_oneshot_routes_system_exit_to_hard_exit(monkeypatch, main_mod):
    exits = []

    def fake_run_oneshot(*_args, **_kwargs):
        raise SystemExit(2)

    monkeypatch.setitem(
        sys.modules,
        "hermes_cli.oneshot",
        types.SimpleNamespace(run_oneshot=fake_run_oneshot),
    )
    monkeypatch.setattr(main_mod, "_cleanup_oneshot_runtime", lambda: None)
    monkeypatch.setattr(main_mod, "_exit_after_oneshot", lambda rc: exits.append(rc))

    main_mod._run_and_exit_oneshot("hello")

    assert exits == [2]


def test_run_and_exit_oneshot_routes_bare_system_exit_to_zero(monkeypatch, main_mod):
    exits = []

    def fake_run_oneshot(*_args, **_kwargs):
        raise SystemExit

    monkeypatch.setitem(
        sys.modules,
        "hermes_cli.oneshot",
        types.SimpleNamespace(run_oneshot=fake_run_oneshot),
    )
    monkeypatch.setattr(main_mod, "_cleanup_oneshot_runtime", lambda: None)
    monkeypatch.setattr(main_mod, "_exit_after_oneshot", lambda rc: exits.append(rc))

    main_mod._run_and_exit_oneshot("hello")

    assert exits == [None]


def test_run_and_exit_oneshot_prints_system_exit_message(
    monkeypatch, capsys, main_mod
):
    exits = []

    def fake_run_oneshot(*_args, **_kwargs):
        raise SystemExit("fatal")

    monkeypatch.setitem(
        sys.modules,
        "hermes_cli.oneshot",
        types.SimpleNamespace(run_oneshot=fake_run_oneshot),
    )
    monkeypatch.setattr(main_mod, "_cleanup_oneshot_runtime", lambda: None)
    monkeypatch.setattr(main_mod, "_exit_after_oneshot", lambda rc: exits.append(rc))

    main_mod._run_and_exit_oneshot("hello")

    assert exits == [1]
    captured = capsys.readouterr()
    assert captured.out == ""
    assert captured.err == "fatal\n"


def test_run_and_exit_oneshot_cleans_global_runtime_before_hard_exit(
    monkeypatch, main_mod
):
    events = []

    def _mod(name, **attrs):
        fake = types.ModuleType(name)
        for key, value in attrs.items():
            setattr(fake, key, value)
        return fake

    monkeypatch.setitem(
        sys.modules,
        "hermes_cli.oneshot",
        types.SimpleNamespace(run_oneshot=lambda *_args, **_kwargs: events.append("run") or 0),
    )
    monkeypatch.setitem(
        sys.modules,
        "tools.terminal_tool",
        _mod("tools.terminal_tool", cleanup_all_environments=lambda: events.append("terminal")),
    )
    monkeypatch.setitem(
        sys.modules,
        "tools.async_delegation",
        _mod("tools.async_delegation", interrupt_all=lambda **kw: events.append("delegation")),
    )
    monkeypatch.setitem(
        sys.modules,
        "tools.browser_tool",
        _mod("tools.browser_tool", _emergency_cleanup_all_sessions=lambda: events.append("browser")),
    )
    monkeypatch.setitem(
        sys.modules,
        "tools.mcp_tool",
        _mod("tools.mcp_tool", shutdown_mcp_servers=lambda: events.append("mcp")),
    )
    monkeypatch.setitem(
        sys.modules,
        "agent.auxiliary_client",
        _mod("agent.auxiliary_client", shutdown_cached_clients=lambda: events.append("aux")),
    )
    monkeypatch.setattr(
        main_mod, "_exit_after_oneshot", lambda rc: events.append(f"exit:{rc}")
    )

    main_mod._run_and_exit_oneshot("hello")

    assert events == ["run", "terminal", "delegation", "browser", "mcp", "aux", "exit:0"]


def test_run_and_exit_oneshot_still_exits_when_global_cleanup_raises(
    monkeypatch, main_mod
):
    events = []

    def _raise_mcp():
        raise RuntimeError("mcp boom")

    monkeypatch.setitem(
        sys.modules,
        "hermes_cli.oneshot",
        types.SimpleNamespace(run_oneshot=lambda *_args, **_kwargs: 0),
    )
    monkeypatch.setitem(
        sys.modules,
        "tools.terminal_tool",
        types.SimpleNamespace(cleanup_all_environments=lambda: events.append("terminal")),
    )
    monkeypatch.setitem(
        sys.modules,
        "tools.async_delegation",
        types.SimpleNamespace(interrupt_all=lambda **kw: events.append("delegation")),
    )
    monkeypatch.setitem(
        sys.modules,
        "tools.browser_tool",
        types.SimpleNamespace(_emergency_cleanup_all_sessions=lambda: events.append("browser")),
    )
    monkeypatch.setitem(
        sys.modules,
        "tools.mcp_tool",
        types.SimpleNamespace(shutdown_mcp_servers=_raise_mcp),
    )
    monkeypatch.setitem(
        sys.modules,
        "agent.auxiliary_client",
        types.SimpleNamespace(shutdown_cached_clients=lambda: events.append("aux")),
    )
    monkeypatch.setattr(
        main_mod, "_exit_after_oneshot", lambda rc: events.append(f"exit:{rc}")
    )

    main_mod._run_and_exit_oneshot("hello")

    assert events == ["terminal", "delegation", "browser", "aux", "exit:0"]


def test_run_and_exit_oneshot_hard_exits_when_cleanup_is_interrupted(
    monkeypatch, main_mod
):
    def _raise_keyboard_interrupt():
        raise KeyboardInterrupt

    monkeypatch.setitem(
        sys.modules,
        "hermes_cli.oneshot",
        types.SimpleNamespace(run_oneshot=lambda *_args, **_kwargs: 0),
    )
    monkeypatch.setattr(
        main_mod, "_cleanup_oneshot_runtime", _raise_keyboard_interrupt
    )
    monkeypatch.setattr(main_mod, "_exit_after_oneshot", _raise_exit)

    with pytest.raises(SystemExit) as exc:
        main_mod._run_and_exit_oneshot("hello")

    assert exc.value.code == 0


def test_run_and_exit_oneshot_routes_keyboard_interrupt_to_130(
    monkeypatch, main_mod
):
    exits = []

    def fake_run_oneshot(*_args, **_kwargs):
        raise KeyboardInterrupt

    monkeypatch.setitem(
        sys.modules,
        "hermes_cli.oneshot",
        types.SimpleNamespace(run_oneshot=fake_run_oneshot),
    )
    monkeypatch.setattr(main_mod, "_cleanup_oneshot_runtime", lambda: None)
    monkeypatch.setattr(main_mod, "_exit_after_oneshot", lambda rc: exits.append(rc))

    main_mod._run_and_exit_oneshot("hello")

    assert exits == [130]


def test_run_and_exit_oneshot_hard_exits_on_unexpected_exception(
    monkeypatch, main_mod, capsys
):
    # ``run_oneshot`` is contracted to convert agent failures into an int and
    # only re-raise KeyboardInterrupt / SystemExit. If it ever malfunctions and
    # lets another exception escape, the one-shot path must still hard-exit
    # (rc 1) rather than fall through to interpreter teardown — the exact path
    # that SIGABRTs on AL2023.
    exits = []
    cleaned = []

    def fake_run_oneshot(*_args, **_kwargs):
        raise RuntimeError("run_oneshot itself blew up")

    monkeypatch.setitem(
        sys.modules,
        "hermes_cli.oneshot",
        types.SimpleNamespace(run_oneshot=fake_run_oneshot),
    )
    monkeypatch.setattr(
        main_mod, "_cleanup_oneshot_runtime", lambda: cleaned.append(True)
    )
    monkeypatch.setattr(main_mod, "_exit_after_oneshot", lambda rc: exits.append(rc))

    main_mod._run_and_exit_oneshot("hello")

    assert exits == [1]
    # Global cleanup still runs on the defensive path — resources must not leak
    # just because run_oneshot malfunctioned.
    assert cleaned == [True]
    # The failure is surfaced on stderr, never swallowed silently.
    assert "run_oneshot itself blew up" in capsys.readouterr().err


def test_run_and_exit_oneshot_hard_exits_when_oneshot_import_fails(
    monkeypatch, main_mod, capsys
):
    import builtins

    exits = []
    cleaned = []
    real_import = builtins.__import__

    def fake_import(name, globals=None, locals=None, fromlist=(), level=0):
        if name == "hermes_cli.oneshot" and "run_oneshot" in (fromlist or ()):
            raise RuntimeError("oneshot import blew up")
        return real_import(name, globals, locals, fromlist, level)

    monkeypatch.setattr(builtins, "__import__", fake_import)
    monkeypatch.setattr(
        main_mod, "_cleanup_oneshot_runtime", lambda: cleaned.append(True)
    )
    monkeypatch.setattr(main_mod, "_exit_after_oneshot", lambda rc: exits.append(rc))

    main_mod._run_and_exit_oneshot("hello")

    assert exits == [1]
    assert cleaned == [True]
    assert "oneshot import blew up" in capsys.readouterr().err


def test_oneshot_subprocess_exits_without_teardown_abort():
    program = textwrap.dedent(
        """
        import hermes_cli.oneshot as oneshot
        from hermes_cli.main import _exit_after_oneshot

        oneshot._run_agent = lambda *args, **kwargs: ("ok", {"final_response": "ok"})
        _exit_after_oneshot(oneshot.run_oneshot("hello"))
        """
    )

    result = subprocess.run(
        [sys.executable, "-c", program],
        cwd=Path(__file__).resolve().parents[2],
        capture_output=True,
        timeout=10,
        check=False,
    )

    assert result.returncode == 0
    assert result.stdout == b"ok\n"
    # Don't demand byte-empty stderr — an import-time warning from the heavy
    # CLI import chain shouldn't fail this. What matters is no crash traceback.
    assert b"Traceback" not in result.stderr


def test_exit_after_oneshot_bypasses_late_atexit_abort():
    program = textwrap.dedent(
        """
        import atexit
        import os
        import sys
        from hermes_cli.main import _exit_after_oneshot

        atexit.register(os.abort)
        sys.stdout.write("done\\n")
        _exit_after_oneshot(0)
        """
    )

    result = subprocess.run(
        [sys.executable, "-c", program],
        cwd=Path(__file__).resolve().parents[2],
        capture_output=True,
        timeout=10,
        check=False,
    )

    assert result.returncode == 0
    assert result.stdout == b"done\n"


def test_run_and_exit_oneshot_passes_through_nonzero_return(monkeypatch, main_mod):
    # A non-zero rc from run_oneshot (e.g. provider-without-model → 2, or the
    # empty-response guard → 1) must reach os._exit unchanged.
    exits = []
    monkeypatch.setitem(
        sys.modules,
        "hermes_cli.oneshot",
        types.SimpleNamespace(run_oneshot=lambda *a, **k: 2),
    )
    monkeypatch.setattr(main_mod, "_cleanup_oneshot_runtime", lambda: None)
    monkeypatch.setattr(main_mod, "_exit_after_oneshot", lambda rc: exits.append(rc))

    main_mod._run_and_exit_oneshot("hi")

    assert exits == [2]


def test_main_oneshot_path_bypasses_late_atexit_abort():
    # End-to-end through the real top-level ``main()`` ``-z`` path: a valid
    # response prints, then a late atexit handler that would abort is bypassed
    # by the hard exit, so the process reports success (#43055).
    program = textwrap.dedent(
        """
        import atexit
        import os
        import sys
        import types

        import hermes_cli.main as main_mod

        sys.argv = ["hermes", "-z", "hello"]
        main_mod._prepare_agent_startup = lambda args: None

        def _fake_run_oneshot(prompt, **kwargs):
            print("ok")
            return 0

        sys.modules["hermes_cli.oneshot"] = types.SimpleNamespace(
            run_oneshot=_fake_run_oneshot
        )
        atexit.register(os.abort)
        main_mod.main()
        """
    )

    result = subprocess.run(
        [sys.executable, "-c", program],
        cwd=Path(__file__).resolve().parents[2],
        capture_output=True,
        timeout=30,
        check=False,
    )

    assert result.returncode == 0
    assert result.stdout == b"ok\n"
    assert b"Traceback" not in result.stderr


def test_oneshot_run_agent_closes_agent_after_chat(monkeypatch):
    import hermes_cli.oneshot as oneshot_mod

    closed = []
    shutdown_messages = []

    class FakeAgent:
        def __init__(self, **_kwargs):
            self.suppress_status_output = False
            self.stream_delta_callback = object()
            self.tool_gen_callback = object()
            self._session_messages = [{"role": "user", "content": "hello"}]

        def run_conversation(self, prompt, **_kwargs):
            assert prompt == "hello"
            return {"final_response": "done"}

        def shutdown_memory_provider(self, messages=None):
            shutdown_messages.append(messages)

        def close(self):
            closed.append(True)

    monkeypatch.setitem(
        sys.modules, "run_agent", types.SimpleNamespace(AIAgent=FakeAgent)
    )
    monkeypatch.setattr(
        "hermes_cli.config.load_config",
        lambda: {"model": {"default": "gpt-test", "provider": "openai"}},
    )
    monkeypatch.setattr(
        "hermes_cli.runtime_provider.resolve_runtime_provider",
        lambda **_kwargs: {
            "api_key": "key",
            "base_url": "https://example.invalid",
            "provider": "openai",
            "api_mode": "chat_completions",
            "credential_pool": None,
        },
    )
    monkeypatch.setattr(oneshot_mod, "_create_session_db_for_oneshot", lambda: None)

    assert (
        oneshot_mod._run_agent(
            "hello", model="gpt-test", provider="openai", use_config_toolsets=False
        )
        == ("done", {"final_response": "done"})
    )
    assert closed == [True]
    assert shutdown_messages == [[{"role": "user", "content": "hello"}]]


def test_oneshot_run_agent_closes_agent_when_chat_raises(monkeypatch):
    import hermes_cli.oneshot as oneshot_mod

    closed = []
    shutdowns = []

    class FakeAgent:
        def __init__(self, **_kwargs):
            self.suppress_status_output = False
            self.stream_delta_callback = object()
            self.tool_gen_callback = object()

        def run_conversation(self, _prompt, **_kwargs):
            raise RuntimeError("boom")

        def shutdown_memory_provider(self, messages=None):
            shutdowns.append(messages)

        def close(self):
            closed.append(True)

    monkeypatch.setitem(
        sys.modules, "run_agent", types.SimpleNamespace(AIAgent=FakeAgent)
    )
    monkeypatch.setattr(
        "hermes_cli.config.load_config",
        lambda: {"model": {"default": "gpt-test", "provider": "openai"}},
    )
    monkeypatch.setattr(
        "hermes_cli.runtime_provider.resolve_runtime_provider",
        lambda **_kwargs: {
            "api_key": "key",
            "base_url": "https://example.invalid",
            "provider": "openai",
            "api_mode": "chat_completions",
            "credential_pool": None,
        },
    )
    monkeypatch.setattr(oneshot_mod, "_create_session_db_for_oneshot", lambda: None)

    with pytest.raises(RuntimeError, match="boom"):
        oneshot_mod._run_agent(
            "hello", model="gpt-test", provider="openai", use_config_toolsets=False
        )
    assert closed == [True]
    assert shutdowns == [None]


def test_oneshot_run_agent_closes_session_db(monkeypatch):
    # The one-shot exit path hard-exits via os._exit and skips finalizers, so
    # the recall SQLite store it opens must be closed explicitly (checkpointing
    # its WAL) rather than left to interpreter teardown.
    import hermes_cli.oneshot as oneshot_mod

    db_closed = []

    class FakeAgent:
        def __init__(self, **_kwargs):
            self.suppress_status_output = False
            self.stream_delta_callback = object()
            self.tool_gen_callback = object()
            self._session_messages = []

        def run_conversation(self, _prompt, **_kwargs):
            return {"final_response": "done"}

        def shutdown_memory_provider(self, messages=None):
            pass

        def close(self):
            pass

    class FakeSessionDB:
        def close(self):
            db_closed.append(True)

    monkeypatch.setitem(
        sys.modules, "run_agent", types.SimpleNamespace(AIAgent=FakeAgent)
    )
    monkeypatch.setattr(
        "hermes_cli.config.load_config",
        lambda: {"model": {"default": "gpt-test", "provider": "openai"}},
    )
    monkeypatch.setattr(
        "hermes_cli.runtime_provider.resolve_runtime_provider",
        lambda **_kwargs: {
            "api_key": "key",
            "base_url": "https://example.invalid",
            "provider": "openai",
            "api_mode": "chat_completions",
            "credential_pool": None,
        },
    )
    monkeypatch.setattr(
        oneshot_mod, "_create_session_db_for_oneshot", lambda: FakeSessionDB()
    )

    assert (
        oneshot_mod._run_agent(
            "hello", model="gpt-test", provider="openai", use_config_toolsets=False
        )
        == ("done", {"final_response": "done"})
    )
    assert db_closed == [True]


def test_oneshot_run_agent_closes_session_db_when_agent_init_raises(monkeypatch):
    # The recall store is opened before AIAgent is constructed. If construction
    # raises (bad provider/config/model), the store must still be closed — the
    # one-shot exit hard-exits via os._exit and skips finalizers, so an
    # un-closed connection would leave a stale WAL behind.
    import hermes_cli.oneshot as oneshot_mod

    db_closed = []

    class FakeSessionDB:
        def close(self):
            db_closed.append(True)

    class FakeAgent:
        def __init__(self, **_kwargs):
            raise RuntimeError("init boom")

    monkeypatch.setitem(
        sys.modules, "run_agent", types.SimpleNamespace(AIAgent=FakeAgent)
    )
    monkeypatch.setattr(
        "hermes_cli.config.load_config",
        lambda: {"model": {"default": "gpt-test", "provider": "openai"}},
    )
    monkeypatch.setattr(
        "hermes_cli.runtime_provider.resolve_runtime_provider",
        lambda **_kwargs: {
            "api_key": "key",
            "base_url": "https://example.invalid",
            "provider": "openai",
            "api_mode": "chat_completions",
            "credential_pool": None,
        },
    )
    monkeypatch.setattr(
        oneshot_mod, "_create_session_db_for_oneshot", lambda: FakeSessionDB()
    )

    with pytest.raises(RuntimeError, match="init boom"):
        oneshot_mod._run_agent(
            "hello", model="gpt-test", provider="openai", use_config_toolsets=False
        )

    assert db_closed == [True]


def _stub_plugin_discovery(monkeypatch):
    monkeypatch.setitem(
        sys.modules,
        "hermes_cli.plugins",
        types.SimpleNamespace(discover_plugins=lambda: None),
    )


def test_oneshot_rejects_invalid_only_toolsets(monkeypatch, capsys):
    _stub_plugin_discovery(monkeypatch)
    from hermes_cli.oneshot import run_oneshot

    assert run_oneshot("hello", toolsets="nope") == 2
    err = capsys.readouterr().err
    assert "nope" in err
    assert "did not contain any valid toolsets" in err


def test_oneshot_fails_closed_on_empty_final_response(monkeypatch, capsys):
    _stub_plugin_discovery(monkeypatch)
    import hermes_cli.oneshot as oneshot_mod

    monkeypatch.setattr(oneshot_mod, "_run_agent", lambda *_args, **_kwargs: ("", {}))

    assert oneshot_mod.run_oneshot("hello") == 1
    captured = capsys.readouterr()
    assert captured.out == ""
    assert "no final response" in captured.err


def test_oneshot_prints_nonempty_final_response(monkeypatch, capsys):
    _stub_plugin_discovery(monkeypatch)
    import hermes_cli.oneshot as oneshot_mod

    monkeypatch.setattr(oneshot_mod, "_run_agent", lambda *_args, **_kwargs: ("done", {}))

    assert oneshot_mod.run_oneshot("hello") == 0
    captured = capsys.readouterr()
    assert captured.out == "done\n"
    assert captured.err == ""


def test_oneshot_fails_closed_on_agent_exception(monkeypatch, capsys):
    _stub_plugin_discovery(monkeypatch)
    import hermes_cli.oneshot as oneshot_mod

    def _boom(*_args, **_kwargs):
        raise OSError("not a TTY")

    monkeypatch.setattr(oneshot_mod, "_run_agent", _boom)

    assert oneshot_mod.run_oneshot("hello") == 1
    captured = capsys.readouterr()
    assert captured.out == ""
    assert "agent failed" in captured.err
    assert "not a TTY" in captured.err


def test_oneshot_exit_code_when_failed_without_response(monkeypatch):
    from hermes_cli.oneshot import run_oneshot

    monkeypatch.setattr(
        "hermes_cli.oneshot._run_agent",
        lambda *_a, **_k: ("", {"failed": True, "partial": False}),
    )
    assert run_oneshot("hi") == 2


def test_oneshot_exit_code_zero_when_failed_with_error_text(monkeypatch, capsys):
    from hermes_cli.oneshot import run_oneshot

    monkeypatch.setattr(
        "hermes_cli.oneshot._run_agent",
        lambda *_a, **_k: (
            "API call failed after 3 retries: HTTP 404: model not found",
            {"failed": True, "partial": False},
        ),
    )
    assert run_oneshot("hi") == 0
    assert "HTTP 404" in capsys.readouterr().out


def test_oneshot_reraises_keyboard_interrupt(monkeypatch):
    _stub_plugin_discovery(monkeypatch)
    import hermes_cli.oneshot as oneshot_mod
    import pytest as _pytest

    def _interrupt(*_args, **_kwargs):
        raise KeyboardInterrupt

    monkeypatch.setattr(oneshot_mod, "_run_agent", _interrupt)

    with _pytest.raises(KeyboardInterrupt):
        oneshot_mod.run_oneshot("hello")


def test_oneshot_filters_invalid_toolsets_before_redirect(monkeypatch, capsys):
    _stub_plugin_discovery(monkeypatch)
    from hermes_cli.oneshot import _validate_explicit_toolsets

    valid, error = _validate_explicit_toolsets("web,nope")

    assert valid == ["web"]
    assert error is None
    assert "nope" in capsys.readouterr().err


def test_oneshot_all_toolsets_means_all_not_configured_cli():
    from hermes_cli.oneshot import _validate_explicit_toolsets

    valid, error = _validate_explicit_toolsets("all")

    assert valid is None
    assert error is None


def test_oneshot_all_toolsets_warns_about_ignored_extra_entries(monkeypatch, capsys):
    _stub_plugin_discovery(monkeypatch)
    from hermes_cli.oneshot import _validate_explicit_toolsets

    valid, error = _validate_explicit_toolsets("all,nope")

    assert valid is None
    assert error is None
    assert "ignoring additional entries: nope" in capsys.readouterr().err


def test_oneshot_accepts_plugin_toolset_after_discovery(monkeypatch):
    import toolsets

    from hermes_cli.oneshot import _validate_explicit_toolsets

    discovered = {"ready": False}
    original_validate = toolsets.validate_toolset

    def fake_validate(name):
        return name == "plugin_demo" and discovered["ready"] or original_validate(name)

    monkeypatch.setattr(toolsets, "validate_toolset", fake_validate)
    monkeypatch.setitem(
        sys.modules,
        "hermes_cli.plugins",
        types.SimpleNamespace(
            discover_plugins=lambda: discovered.update({"ready": True})
        ),
    )

    valid, error = _validate_explicit_toolsets("plugin_demo")

    assert valid == ["plugin_demo"]
    assert error is None


def test_oneshot_rejects_disabled_mcp_toolset(monkeypatch, capsys):
    _stub_plugin_discovery(monkeypatch)
    import hermes_cli.config as config_mod

    from hermes_cli.oneshot import _validate_explicit_toolsets

    monkeypatch.setattr(
        config_mod,
        "read_raw_config",
        lambda: {"mcp_servers": {"mcp-off": {"enabled": False}}},
    )

    valid, error = _validate_explicit_toolsets("mcp-off")

    assert valid is None
    assert error == "hermes -z: --toolsets did not contain any valid toolsets.\n"
    err = capsys.readouterr().err
    assert "ignoring disabled MCP servers" in err
    assert "mcp-off" in err


def test_oneshot_distinguishes_disabled_mcp_from_unknown(monkeypatch, capsys):
    _stub_plugin_discovery(monkeypatch)
    import hermes_cli.config as config_mod

    from hermes_cli.oneshot import _validate_explicit_toolsets

    monkeypatch.setattr(
        config_mod,
        "read_raw_config",
        lambda: {"mcp_servers": {"mcp-off": {"enabled": False}}},
    )

    valid, error = _validate_explicit_toolsets("web,mcp-off,nope")

    assert valid == ["web"]
    assert error is None
    err = capsys.readouterr().err
    assert "ignoring unknown --toolsets entries: nope" in err
    assert "ignoring disabled MCP servers" in err
    assert "mcp-off" in err


def test_oneshot_wires_session_db_for_recall(monkeypatch):
    """hermes -z bypasses HermesCLI, but recall still needs SessionDB."""
    from hermes_cli.oneshot import _run_agent

    captured = {}
    sentinel_db = object()

    class FakeAgent:
        def __init__(self, **kwargs):
            captured.update(kwargs)
            self.suppress_status_output = False
            self.stream_delta_callback = object()
            self.tool_gen_callback = object()

        def run_conversation(self, prompt, **_kwargs):
            captured["prompt"] = prompt
            return {"final_response": "ok", "failed": False, "partial": False}

    class FakeSessionDB:
        def __new__(cls):
            return sentinel_db

    def mod(name, **attrs):
        module = types.ModuleType(name)
        for key, value in attrs.items():
            setattr(module, key, value)
        return module

    monkeypatch.setitem(sys.modules, "run_agent", mod("run_agent", AIAgent=FakeAgent))
    monkeypatch.setitem(sys.modules, "hermes_state", mod("hermes_state", SessionDB=FakeSessionDB))
    monkeypatch.setitem(
        sys.modules,
        "hermes_cli.config",
        mod("hermes_cli.config", load_config=lambda: {"model": {"default": "m"}}),
    )
    monkeypatch.setitem(
        sys.modules,
        "hermes_cli.models",
        mod("hermes_cli.models", detect_provider_for_model=lambda *_args, **_kwargs: None),
    )
    monkeypatch.setitem(
        sys.modules,
        "hermes_cli.runtime_provider",
        mod(
            "hermes_cli.runtime_provider",
            resolve_runtime_provider=lambda **_kwargs: {
                "api_key": "k",
                "base_url": "u",
                "provider": "p",
                "api_mode": "chat_completions",
                "credential_pool": None,
            },
        ),
    )
    monkeypatch.setitem(
        sys.modules,
        "hermes_cli.tools_config",
        mod("hermes_cli.tools_config", _get_platform_tools=lambda *_args, **_kwargs: {"session_search"}),
    )

    text, result = _run_agent("recall this")
    assert text == "ok"
    assert not result.get("failed")
    assert captured["session_db"] is sentinel_db
    assert captured["enabled_toolsets"] == ["session_search"]
    assert captured["prompt"] == "recall this"


def test_launch_tui_exports_model_provider_and_toolsets(monkeypatch, main_mod):
    captured = {}
    active_path_during_call = None

    monkeypatch.setattr(
        main_mod,
        "_make_tui_argv",
        lambda tui_dir, tui_dev: (["node", "dist/entry.js"], Path(".")),
    )

    def fake_call(argv, cwd=None, env=None):
        nonlocal active_path_during_call
        captured.update({"argv": argv, "cwd": cwd, "env": env})
        active_path_during_call = Path(env["HERMES_TUI_ACTIVE_SESSION_FILE"])
        assert active_path_during_call.exists()
        return 1

    monkeypatch.setattr(main_mod.subprocess, "call", fake_call)

    with pytest.raises(SystemExit):
        main_mod._launch_tui(
            model="nous/hermes-test", provider="nous", toolsets="web, terminal"
        )

    env = captured["env"]
    assert env["HERMES_MODEL"] == "nous/hermes-test"
    assert env["HERMES_INFERENCE_MODEL"] == "nous/hermes-test"
    assert env["HERMES_TUI_PROVIDER"] == "nous"
    assert env["HERMES_INFERENCE_PROVIDER"] == "nous"
    assert env["HERMES_TUI_TOOLSETS"] == "web,terminal"
    active_path = Path(env["HERMES_TUI_ACTIVE_SESSION_FILE"])
    assert active_path.name.startswith("hermes-tui-active-session-")
    assert active_path.suffix == ".json"
    assert active_path_during_call == active_path
    assert not active_path.exists()
    assert env["NODE_ENV"] == "production"


def test_launch_tui_worktree_validates_relative_python_against_final_cwd(
    monkeypatch, main_mod, tmp_path
):
    import cli as cli_mod

    parent_cwd = tmp_path / "parent"
    parent_cwd.mkdir()
    worktree = tmp_path / "worktree"
    relative_python = Path(".review-venv") / "bin" / Path(sys.executable).name
    python_path = worktree / relative_python
    python_path.parent.mkdir(parents=True)
    # copy2, not os.link: tmp_path may sit on a different filesystem than
    # the venv (tmpfs /tmp vs disk home) where hard links raise EXDEV.
    import shutil

    shutil.copy2(sys.executable, python_path)
    captured = {}

    monkeypatch.setenv("HERMES_CWD", str(parent_cwd))
    monkeypatch.setenv("HERMES_PYTHON", str(relative_python))
    monkeypatch.setattr(cli_mod, "_git_repo_root", lambda: None)
    monkeypatch.setattr(cli_mod, "_prune_stale_worktrees", lambda _repo: None)
    monkeypatch.setattr(cli_mod, "_setup_worktree", lambda: {"path": str(worktree)})
    monkeypatch.setattr(cli_mod, "_cleanup_worktree", lambda _info: None)
    monkeypatch.setattr(
        main_mod,
        "_make_tui_argv",
        lambda tui_dir, tui_dev: (["node", "dist/entry.js"], Path(".")),
    )
    monkeypatch.setattr(
        main_mod.subprocess,
        "call",
        lambda argv, cwd=None, env=None: captured.update({"env": env}) or 1,
    )

    with pytest.raises(SystemExit):
        main_mod._launch_tui(worktree=True)

    assert captured["env"]["HERMES_CWD"] == str(worktree)
    assert captured["env"]["HERMES_PYTHON"] == str(relative_python)


def test_launch_tui_applies_terminal_backend_config(
    monkeypatch, main_mod, _isolate_hermes_home
):
    captured = {}
    config_path = Path(os.environ["HERMES_HOME"]) / "config.yaml"
    config_path.write_text(
        "\n".join(
            [
                "terminal:",
                "  backend: docker",
                "  docker_image: example/hermes-tools:latest",
                "  docker_extra_args:",
                "    - --network=host",
            ]
        ),
        encoding="utf-8",
    )
    monkeypatch.delenv("TERMINAL_ENV", raising=False)
    monkeypatch.delenv("TERMINAL_DOCKER_IMAGE", raising=False)
    monkeypatch.delenv("TERMINAL_DOCKER_EXTRA_ARGS", raising=False)

    monkeypatch.setattr(
        main_mod,
        "_make_tui_argv",
        lambda tui_dir, tui_dev: (["node", "dist/entry.js"], Path(".")),
    )
    monkeypatch.setattr(
        main_mod.subprocess,
        "call",
        lambda argv, cwd=None, env=None: captured.update({"env": env}) or 1,
    )

    with pytest.raises(SystemExit):
        main_mod._launch_tui()

    assert captured["env"]["TERMINAL_ENV"] == "docker"
    assert captured["env"]["TERMINAL_DOCKER_IMAGE"] == "example/hermes-tools:latest"
    assert captured["env"]["TERMINAL_DOCKER_EXTRA_ARGS"] == '["--network=host"]'


def test_launch_tui_exit_code_42_relaunches_update(monkeypatch, main_mod):
    from unittest.mock import patch

    monkeypatch.setattr(
        main_mod,
        "_make_tui_argv",
        lambda tui_dir, tui_dev: (["node", "dist/entry.js"], Path(".")),
    )
    monkeypatch.setattr(main_mod.subprocess, "call", lambda *args, **kwargs: 42)

    with patch("hermes_cli.relaunch.relaunch") as mock_relaunch:
        with pytest.raises(SystemExit) as exc:
            main_mod._launch_tui()

    assert exc.value.code == 42
    mock_relaunch.assert_called_once_with(["update"], preserve_inherited=False)


def test_launch_tui_drops_stale_resume_env_without_resume_arg(monkeypatch, main_mod):
    captured = {}

    monkeypatch.setenv("HERMES_TUI_RESUME", "stale-missing-session")
    monkeypatch.setattr(
        main_mod,
        "_make_tui_argv",
        lambda tui_dir, tui_dev: (["node", "dist/entry.js"], Path(".")),
    )
    monkeypatch.setattr(
        main_mod.subprocess,
        "call",
        lambda argv, cwd=None, env=None: captured.update({"env": env}) or 1,
    )

    with pytest.raises(SystemExit):
        main_mod._launch_tui()

    assert "HERMES_TUI_RESUME" not in captured["env"]


def test_launch_tui_sets_resume_env_from_resume_arg(monkeypatch, main_mod):
    captured = {}

    monkeypatch.setenv("HERMES_TUI_RESUME", "stale-missing-session")
    monkeypatch.setattr(
        main_mod,
        "_make_tui_argv",
        lambda tui_dir, tui_dev: (["node", "dist/entry.js"], Path(".")),
    )
    monkeypatch.setattr(
        main_mod.subprocess,
        "call",
        lambda argv, cwd=None, env=None: captured.update({"env": env}) or 1,
    )

    with pytest.raises(SystemExit):
        main_mod._launch_tui(resume_session_id="20260518_000000_goodid")

    assert captured["env"]["HERMES_TUI_RESUME"] == "20260518_000000_goodid"


def test_make_tui_argv_dev_prebuilds_hermes_ink(monkeypatch, main_mod, tmp_path):
    tui_dir = tmp_path / "ui-tui"
    tsx = tui_dir / "node_modules" / ".bin" / "tsx"
    ink_dir = tui_dir / "packages" / "hermes-ink"
    tsx.parent.mkdir(parents=True)
    ink_dir.mkdir(parents=True)
    tsx.write_text("#!/usr/bin/env node\n", encoding="utf-8")

    monkeypatch.setattr(main_mod, "_ensure_tui_node", lambda: None)
    monkeypatch.setattr(main_mod, "_tui_need_npm_install", lambda _tui_dir: False)
    monkeypatch.delenv("HERMES_TUI_DIR", raising=False)
    monkeypatch.setattr(main_mod.shutil, "which", lambda bin_name: f"/usr/bin/{bin_name}")

    calls = []

    def fake_run(cmd, cwd=None, **_kwargs):
        calls.append((cmd, cwd))
        return types.SimpleNamespace(returncode=0, stdout="", stderr="")

    monkeypatch.setattr(main_mod.subprocess, "run", fake_run)

    argv, cwd = main_mod._make_tui_argv(tui_dir, tui_dev=True)

    assert argv == [str(tsx), "src/entry.tsx"]
    assert cwd == tui_dir
    assert calls == [(["/usr/bin/npm", "run", "build"], str(ink_dir))]


def test_print_tui_exit_summary_includes_resume_and_token_totals(monkeypatch, capsys):
    import hermes_cli.main as main_mod

    class _FakeDB:
        def get_session(self, session_id):
            assert session_id == "20260409_000001_abc123"
            return {
                "message_count": 2,
                "input_tokens": 10,
                "output_tokens": 6,
                "cache_read_tokens": 2,
                "cache_write_tokens": 2,
                "reasoning_tokens": 1,
            }

        def get_session_title(self, _session_id):
            return "demo title"

        def close(self):
            return None

    monkeypatch.setitem(
        sys.modules, "hermes_state", types.SimpleNamespace(SessionDB=lambda: _FakeDB())
    )

    main_mod._print_tui_exit_summary("20260409_000001_abc123")
    out = capsys.readouterr().out

    assert "Resume this session with:" in out
    assert "hermes --tui --resume 20260409_000001_abc123" in out
    assert 'hermes --tui -c "demo title"' in out
    assert "Tokens:         21 (in 10, out 6, cache 4, reasoning 1)" in out


def test_print_tui_exit_summary_prefers_actual_active_session_file(
    monkeypatch, capsys, tmp_path
):
    import hermes_cli.main as main_mod

    seen = []

    class _FakeDB:
        def get_session(self, session_id):
            seen.append(session_id)
            return {
                "message_count": 1,
                "input_tokens": 0,
                "output_tokens": 0,
                "cache_read_tokens": 0,
                "cache_write_tokens": 0,
                "reasoning_tokens": 0,
            }

        def get_session_title(self, _session_id):
            return "actual"

        def close(self):
            return None

    active = tmp_path / "active.json"
    active.write_text('{"session_id":"actual_session"}', encoding="utf-8")
    monkeypatch.setitem(
        sys.modules, "hermes_state", types.SimpleNamespace(SessionDB=lambda: _FakeDB())
    )

    main_mod._print_tui_exit_summary("startup_resume", str(active))
    out = capsys.readouterr().out

    assert seen == ["actual_session"]
    assert "hermes --tui --resume actual_session" in out
    assert "startup_resume" not in out
