#!/usr/bin/env python3
"""Fake ``claude`` CLI for the conformance suite and ``tests/test_agent_claude.py``.

Two modes, both speaking the real 2.1.270 stream-json wire shapes:

``NVSH_FAKE_EVENTS``
    Path to a JSON list of ``{"kind": ..., "text": ..., "error": ...}`` dicts
    (see ``tests/_fake_adapters.py``). Each is emitted as the envelope the
    real CLI would use for it -- ``system`` for a status, a
    ``stream_event``/``content_block_delta``/``text_delta`` for a text delta,
    and a ``result`` line for done/error. This is what the shared conformance
    suite drives.

``NVSH_FAKE_TRANSCRIPT``
    Path to a recorded transcript (``tests/fakes/claude-2.1.270-*.jsonl``,
    carrying a ``# recorded-from:`` header). Its lines are replayed verbatim,
    except that a ``control_request`` line pauses the replay until a matching
    ``control_response`` arrives on stdin -- exactly what the real CLI does
    when it is started with ``--permission-prompt-tool stdio``.

Optional, in either mode:

``NVSH_FAKE_ARGV``       write this process's argv to that path as JSON.
``NVSH_FAKE_STDIN``      append every JSON line read from stdin to that path.
``NVSH_FAKE_UI_TIMEOUT`` seconds to wait for a control_response before giving
                         up with an error line (default 10) -- a broken host
                         fails the test quickly instead of hanging the suite.

Two more knobs, for the reliable-agent-stop tests:

``NVSH_FAKE_IGNORE_CANCEL``
    ``1`` -- once the scripted events are done, the process does not exit:
    it blocks reading stdin instead, silently dropping anything it reads,
    including a host's ``{"type": "control_request", "request":
    {"subtype": "interrupt"}}`` (the real CLI's stream-json cancel). This
    is what a harness that has stopped honouring its own cancel protocol
    looks like from the client's side.

``NVSH_FAKE_GRANDCHILD``
    ``1`` -- start a ``sleep 600`` child at launch and write
    ``{"harness": <this pid>, "grandchild": <sleep's pid>}`` to
    ``$NVSH_FAKE_PID_FILE``.
"""

from __future__ import annotations

import json
import os
import subprocess  # nosec B404 - fixed argv below, test fixture only
import sys
import threading
from pathlib import Path

DEFAULT_UI_TIMEOUT = 10.0


def spawn_grandchild_if_requested() -> None:
    """``NVSH_FAKE_GRANDCHILD=1``: start a sleeping child and record both pids."""
    if os.environ.get("NVSH_FAKE_GRANDCHILD") != "1":
        return
    pid_file = os.environ.get("NVSH_FAKE_PID_FILE")
    child = subprocess.Popen(["sleep", "600"])  # nosec B603 B607 - fixed argv
    if pid_file:
        Path(pid_file).write_text(
            json.dumps({"harness": os.getpid(), "grandchild": child.pid}), encoding="utf-8"
        )


def hang_if_ignoring_cancel() -> None:
    """``NVSH_FAKE_IGNORE_CANCEL=1``: never let the turn end on its own."""
    if os.environ.get("NVSH_FAKE_IGNORE_CANCEL") != "1":
        return
    while sys.stdin.readline():
        pass


def emit(obj: dict) -> None:
    sys.stdout.write(json.dumps(obj) + "\n")
    sys.stdout.flush()


def record(variable: str, payload: object) -> None:
    path = os.environ.get(variable)
    if not path:
        return
    with open(path, "a", encoding="utf-8") as handle:
        handle.write(json.dumps(payload) + "\n")


def read_transcript(path: str) -> list:
    lines = []
    with open(path, encoding="utf-8") as handle:
        for raw in handle:
            line = raw.strip()
            if not line or line.startswith("#"):
                continue
            lines.append(json.loads(line))
    return lines


def await_control_response(request_id: str) -> None:
    """Block until the host answers ``request_id`` (or the watchdog fires)."""
    timeout = float(os.environ.get("NVSH_FAKE_UI_TIMEOUT", DEFAULT_UI_TIMEOUT))

    def give_up() -> None:
        error = f"no control_response in {timeout:g}s"
        emit({"type": "result", "subtype": "error", "error": error})
        os._exit(1)

    watchdog = threading.Timer(timeout, give_up)
    watchdog.daemon = True
    watchdog.start()
    try:
        while True:
            raw = sys.stdin.readline()
            if not raw:
                return
            line = raw.strip()
            if not line:
                continue
            try:
                message = json.loads(line)
            except json.JSONDecodeError:
                continue
            record("NVSH_FAKE_STDIN", message)
            response = message.get("response") or {}
            if message.get("type") == "control_response":
                if response.get("request_id") == request_id:
                    return
    finally:
        watchdog.cancel()


def replay_transcript(path: str) -> int:
    for obj in read_transcript(path):
        emit(obj)
        if obj.get("type") == "control_request":
            await_control_response(str(obj.get("request_id", "")))
        if obj.get("type") == "result":
            return 0 if obj.get("subtype") == "success" else 1
    return 0


def replay_events(path: str) -> int:
    events = json.loads(open(path, encoding="utf-8").read())
    for event in events:
        kind = event.get("kind")
        if kind == "status":
            emit({"type": "system", "subtype": "status", "text": event.get("text", "")})
        elif kind == "text_delta":
            emit(
                {
                    "type": "stream_event",
                    "event": {
                        "type": "content_block_delta",
                        "index": 0,
                        "delta": {"type": "text_delta", "text": event.get("text", "")},
                    },
                }
            )
        elif kind == "thinking":
            emit(
                {
                    "type": "stream_event",
                    "event": {
                        "type": "content_block_delta",
                        "index": 0,
                        "delta": {"type": "thinking_delta", "thinking": event.get("text", "")},
                    },
                }
            )
        elif kind == "error":
            emit({"type": "result", "subtype": "error", "error": event.get("error", "")})
            return 1
        elif kind == "done":
            emit({"type": "result", "subtype": "success"})
            return 0
    return 0


def main() -> int:
    record("NVSH_FAKE_ARGV", sys.argv)
    spawn_grandchild_if_requested()
    transcript = os.environ.get("NVSH_FAKE_TRANSCRIPT")
    if transcript:
        rc = replay_transcript(transcript)
    else:
        events_path = os.environ.get("NVSH_FAKE_EVENTS")
        rc = replay_events(events_path) if events_path else 0
    hang_if_ignoring_cancel()
    return rc


if __name__ == "__main__":
    sys.exit(main())
