"""Exercise a freshly installed Termux bundle with networking disabled."""
from __future__ import annotations

import json
import os
from pathlib import Path
import re
import select
import struct
import subprocess
import tempfile
import time


def run(argv: list[str], env: dict[str, str], cwd: Path) -> subprocess.CompletedProcess:
    result = subprocess.run(argv, env=env, cwd=cwd, capture_output=True, text=True, encoding="utf-8", timeout=90)
    print("+", " ".join(argv), flush=True)
    print(result.stdout, result.stderr, flush=True)
    result.check_returncode()
    return result


def stop_child_tree(child: subprocess.Popen) -> None:
    import psutil
    from agent.deadline import kill_process_tree

    if child.poll() is not None:
        return
    descendants = psutil.Process(child.pid).children(recursive=True)
    kill_process_tree(child.pid)
    child.wait(timeout=10)
    deadline = time.monotonic() + 10
    while descendants and time.monotonic() < deadline:
        descendants = [p for p in descendants if p.is_running() and p.status() != psutil.STATUS_ZOMBIE]
        if descendants:
            time.sleep(0.05)
    if descendants:
        raise RuntimeError(f"TUI children still alive: {[p.pid for p in descendants]}")


def tui_smoke(launcher: Path, env: dict[str, str], cwd: Path) -> None:
    import fcntl
    import pty
    import termios

    master, slave = pty.openpty()
    fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack("HHHH", 40, 120, 0, 0))
    child = subprocess.Popen(
        [str(launcher), "--tui"], stdin=slave, stdout=slave, stderr=slave,
        cwd=cwd, env={**env, "TERM": "xterm-256color"}, start_new_session=True,
    )
    os.close(slave)
    captured = bytearray()
    ready = False
    try:
        deadline = time.monotonic() + 90
        while time.monotonic() < deadline:
            if select.select([master], [], [], 0.5)[0]:
                try:
                    chunk = os.read(master, 65536)
                except OSError:
                    break
                if not chunk:
                    break
                captured.extend(chunk)
                plain = re.sub(rb"\x1b\[[0-?]*[ -/]*[@-~]", b"", bytes(captured))
                if b"Setup Required" in plain and b"/model" in plain:
                    ready = True
                    break
            if child.poll() is not None:
                break
        if not ready:
            raise RuntimeError("TUI never reached its real setup screen:\n" + captured.decode(errors="replace"))
        print("TUI_SETUP_SCREEN_OK", flush=True)
    finally:
        try:
            stop_child_tree(child)
        finally:
            os.close(master)


def validate_update_refusal(project_root: Path, result: subprocess.CompletedProcess) -> None:
    from hermes_cli.update_contract import COMMIT_BUILD_UPDATE_MESSAGE, is_commit_build

    # Commit artifacts have no update channel, even when installed through dpkg.
    commit_build = is_commit_build(project_root)
    expected = COMMIT_BUILD_UPDATE_MESSAGE if commit_build else "pkg upgrade hermes-agent"
    if result.returncode != 2 or expected not in result.stdout + result.stderr:
        raise RuntimeError(f"wrong updater refusal ({result.returncode}): {result.stdout}\n{result.stderr}")
    print("COMMIT_BUILD_UPDATE_REFUSAL_OK" if commit_build else "APT_UPDATE_REFUSAL_OK", flush=True)


def main() -> None:
    prefix = Path(os.environ["PREFIX"])
    root = prefix / "lib/hermes-agent"
    with tempfile.TemporaryDirectory(prefix="hermes-install-proof-", dir=prefix / "tmp") as tmp:
        home = Path(tmp)
        env = {
            "PREFIX": str(prefix), "HOME": str(home), "HERMES_HOME": str(home / "state"),
            "PATH": str(prefix / "bin"), "TERM": "xterm-256color", "LANG": "C.UTF-8",
            "LD_LIBRARY_PATH": ":".join((
                str(root / "tools/python" / prefix.relative_to("/") / "lib"),
                str(root / "tools/node" / prefix.relative_to("/") / "lib"),
                str(root / "tools/ffmpeg" / prefix.relative_to("/") / "lib"),
                str(root / "runtime-libs/lib"), str(prefix / "lib"),
            )),
            "HERMES_RUNTIME_DIR": str(root / "tools"),
            "PYTHONPATH": str(root / "app"),
            "PYTHONPYCACHEPREFIX": str(home / "pycache"),
        }
        launcher = prefix / "bin/hermes"
        run([str(launcher), "--version"], env, home)
        run([str(launcher), "chat", "--help"], env, home)
        run([str(prefix / "bin/hermes-acp"), "--check"], env, home)
        python = root / "venv/bin/python"
        run([
            str(python), "-c",
            "import ctypes, ssl, sqlite3, bz2, lzma, zlib, hashlib, readline; "
            "import cli, run_agent, tui_gateway.server; "
            "from hermes_cli.config import detect_install_method; "
            "assert detect_install_method() == 'apt'; "
            "print('CLI_AND_STDLIB_IMPORTS_OK')",
        ], env, home)
        natives = json.loads((root / "native-wheels.json").read_text(encoding="utf-8-sig"))
        run([
            str(python), str(root / "app/scripts/termux/build_wheels.py"),
            "--import-modules", *natives,
        ], env, home)
        node = root / "tools/node" / prefix.relative_to("/") / "bin/node"
        run([str(node), "--version"], env, home)
        run([str(node), str(root / "tools/npm/lib/node_modules/npm/bin/npm-cli.js"), "--version"], env, home)
        run([str(root / "tools/ripgrep/rg"), "--version"], env, home)
        ffmpeg = root / "tools/ffmpeg" / prefix.relative_to("/") / "bin/ffmpeg"
        output = home / "silence.wav"
        run([str(ffmpeg), "-hide_banner", "-f", "lavfi", "-i", "anullsrc=r=16000:cl=mono", "-t", "0.1", str(output)], env, home)
        import wave
        with wave.open(str(output), "rb") as audio:
            assert audio.getnframes() > 0 and audio.getframerate() == 16000
        print("FFMPEG_MEDIA_CONVERSION_OK", flush=True)
        run([
            str(python), "-c",
            "import pm; issues = pm.check(); "
            "assert not issues, issues; print('PM_RUNTIME_TOOLS_OK')",
        ], env, home)
        result = subprocess.run([str(launcher), "update"], env=env, cwd=home, capture_output=True, text=True, encoding="utf-8", timeout=60)
        validate_update_refusal(root / "app", result)
        tui_smoke(launcher, env, home)
        print("INSTALLED_BUNDLE_VALIDATION_OK", flush=True)


if __name__ == "__main__":
    main()
