"""Tests for the CLI `/prompt` editor-compose command.

`/prompt` opens `$VISUAL`/`$EDITOR` on a temp markdown file so the user can
hand-edit a multi-line prompt, then queues the saved buffer as the next
agent turn via the one-shot `_pending_agent_seed` (same path `/blueprint`
uses). These drive a fake editor subprocess to verify read-back, header
stripping, seeding, and the empty-buffer cancel path.
"""

import sys
from pathlib import Path

import pytest

from hermes_cli.cli_commands_mixin import CLICommandsMixin


class _Stub(CLICommandsMixin):
    def __init__(self):
        self._pending_agent_seed = None


def _fake_editor(tmp_path, body: str, mode: str = "append", exit_code: int = 0) -> str:
    """Write a tiny python 'editor' that mutates the file it is handed."""
    script = tmp_path / "editor.py"
    script.write_text(
        "import sys\nfrom pathlib import Path\n"
        "path = Path(sys.argv[1])\n"
        + (f"path.write_text(path.read_text(encoding='utf-8') + {body!r}, encoding='utf-8')\n"
           if mode == "append" else "path.write_text('', encoding='utf-8')\n")
        + f"sys.exit({exit_code})\n", encoding="utf-8")
    return f'"{Path(sys.executable).as_posix()}" "{script.as_posix()}"'


@pytest.fixture(autouse=True)
def _no_visual(monkeypatch):
    monkeypatch.delenv("VISUAL", raising=False)
    monkeypatch.delenv("EDITOR", raising=False)


@pytest.mark.platforms("linux")
def test_compose_reads_and_strips_header(tmp_path, monkeypatch):
    monkeypatch.setenv("EDITOR", _fake_editor(tmp_path, "Refactor the auth module.\nUse pytest."))
    out = _Stub()._compose_in_editor("")
    assert "Refactor the auth module." in out
    assert "Use pytest." in out
    assert "#!" not in out  # the instructional header is stripped


@pytest.mark.platforms("linux")
def test_empty_buffer_does_not_seed(tmp_path, monkeypatch):
    monkeypatch.setenv("EDITOR", _fake_editor(tmp_path, "", mode="clear"))
    s = _Stub()
    s._handle_prompt_compose_command("/prompt")
    assert s._pending_agent_seed is None


def test_editor_failure_never_falls_back_to_shell(monkeypatch):
    """An editor the argv path cannot run must not retry through the shell.

    The old fallback re-invoked ``$EDITOR`` via ``shell=True`` with the editor
    string unquoted, so a crafted EDITOR value executed as shell code. Now a
    failed editor simply cancels the compose (#81364).
    """
    import subprocess as _sp

    calls = []

    def _record(args, **kwargs):
        calls.append((args, kwargs))
        raise OSError("editor not runnable")

    monkeypatch.setattr(_sp, "call", _record)
    monkeypatch.setenv("EDITOR", "nonexistent-editor; touch /tmp/pwned")
    assert _Stub()._compose_in_editor("") == ""
    assert calls, "argv invocation should have been attempted"
    for argv, kwargs in calls:
        assert kwargs.get("shell") is not True
    monkeypatch.setenv("EDITOR", '"unterminated')
    assert _Stub()._compose_in_editor("seed") == ""


def test_nonzero_editor_exit_cancels_even_with_buffer_content(tmp_path, monkeypatch):
    """A failed editor may leave seeded or abandoned text in the buffer — cancel."""
    monkeypatch.setenv("EDITOR", _fake_editor(tmp_path, "typed but abandoned\n", exit_code=3))
    assert _Stub()._compose_in_editor("seed text from the command line") == ""
    stub = _Stub()
    stub._handle_prompt_compose_command("/prompt draft this")
    assert stub._pending_agent_seed is None
