"""Subprocess smoke tests for interactive CLI commands.

Spec source: vectl step ``testability.add-subprocess-smoke-tests``.
"""

from __future__ import annotations

import json
import os
import socket
import subprocess
import sys
import time
from pathlib import Path


def _state_env(tmp_path: Path) -> dict[str, str]:
    """Create isolated ANIMA_STATE env for subprocess smoke tests."""
    state_dir = tmp_path / ".anima"
    (state_dir / "agents").mkdir(parents=True, exist_ok=True)

    env = os.environ.copy()
    env["ANIMA_STATE"] = str(state_dir)
    return env


def _anima_cmd(*args: str) -> list[str]:
    """Build a reliable command for invoking anima CLI."""
    return [sys.executable, "-m", "anima", *args]


def _pick_free_port() -> int:
    """Return an available localhost TCP port."""
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.bind(("127.0.0.1", 0))
        return int(sock.getsockname()[1])


def _read_json_line(
    process: subprocess.Popen[bytes], timeout: float
) -> dict[str, object]:
    """Read and decode one JSON envelope from process stdout."""
    assert process.stdout is not None
    os.set_blocking(process.stdout.fileno(), False)
    deadline = time.monotonic() + timeout
    chunks = bytearray()

    while time.monotonic() < deadline:
        try:
            chunk = os.read(process.stdout.fileno(), 4096)
            if chunk:
                chunks.extend(chunk)
        except BlockingIOError:
            pass

        text = chunks.decode("utf-8", errors="replace").strip()
        if not text:
            time.sleep(0.05)
            continue

        try:
            loaded = json.loads(text)
            if isinstance(loaded, dict):
                return loaded
            raise AssertionError(f"Expected JSON object envelope, got: {loaded!r}")
        except json.JSONDecodeError:
            time.sleep(0.05)
            continue

    raise AssertionError("No JSON envelope within timeout")


def test_monitor_once(tmp_path: Path) -> None:
    """`anima top --once` exits successfully and renders status output."""
    result = subprocess.run(
        _anima_cmd("top", "--once"),
        env=_state_env(tmp_path),
        capture_output=True,
        text=True,
        timeout=5,
        check=False,
    )

    output = f"{result.stdout}\n{result.stderr}"
    assert result.returncode == 0, output
    assert ("ID" in output and "State" in output) or ("No agents" in output), output


def test_serve_json_startup(tmp_path: Path) -> None:
    """`anima serve --json` emits a startup JSON envelope."""
    port = _pick_free_port()
    process = subprocess.Popen(
        _anima_cmd("serve", "--json", "--host", "127.0.0.1", "--port", str(port)),
        env=_state_env(tmp_path),
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=False,
    )

    try:
        payload = _read_json_line(process, timeout=10)
        assert payload.get("status") == "listening", payload
        assert payload.get("host") == "127.0.0.1", payload
        assert payload.get("port") == port, payload
    finally:
        process.terminate()
        try:
            process.wait(timeout=3)
        except subprocess.TimeoutExpired:
            process.kill()
            process.wait(timeout=3)


def test_talk_nonexistent_agent(tmp_path: Path) -> None:
    """`anima talk ag_nonexistent` reports not-found semantics."""
    result = subprocess.run(
        _anima_cmd("talk", "ag_nonexistent"),
        env=_state_env(tmp_path),
        capture_output=True,
        text=True,
        timeout=5,
        check=False,
    )

    output = f"{result.stdout}\n{result.stderr}".lower()
    assert result.returncode != 0, output
    assert "agent not found" in output or "ag_nonexistent" in output, output
