"""Cold CLI provisioning and repair, with real tools and no mocked child code.

Only uv/Python are offered by the fixture's loopback archive server. The copied
PM recipe is unchanged; its small locked runtime is fetched from PyPI. The app
recipe is deliberately tiny so this cannot install the production dependency
set. Run through scripts/run_tests.sh with uv/uvx available on PATH.
"""
from __future__ import annotations

from functools import partial
import hashlib
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tarfile
import threading
import tomllib

import pytest

# Spawns children with a home it builds itself; the parent's must stay real.
pytestmark = pytest.mark.real_machine_home


class _ArchiveHandler(SimpleHTTPRequestHandler):
    def copyfile(self, source, outputfile):
        # The downloader closes its size probe without consuming the archive.
        try:
            super().copyfile(source, outputfile)
        except (BrokenPipeError, ConnectionResetError):
            pass


def _run(command, *, cwd, env, expected=0, timeout=240):
    result = subprocess.run(command, cwd=cwd, env=env, capture_output=True,
                            text=True, timeout=timeout)
    assert result.returncode == expected, (
        f"{command!r}\nexit={result.returncode}\n{result.stdout}\n{result.stderr}"
    )
    return result


def _bare(python, repo, code, *, env, expected=0):
    return _run(
        [str(python), "-I", "-S", "-B", "-c",
         f"import sys; sys.path.insert(0, {str(repo)!r});\n" + code],
        cwd=repo.parent, env=env, expected=expected,
    )


_UNSHIPPED = {"site-packages", "test", "__pycache__"}


def _foreign_link(stdlib: Path) -> Path | None:
    """First link in the shipped stdlib that the store's extractor refuses.

    PM extracts archives with tarfile's data filter, which rejects absolute or
    escaping links. Distro Pythons link stdlib files into /etc (Debian's
    sitecustomize.py), so their stdlib cannot stand in for a standalone one.
    """
    for directory, dirnames, filenames in os.walk(stdlib):
        dirnames[:] = [name for name in dirnames if name not in _UNSHIPPED]
        for name in (*dirnames, *filenames):
            path = Path(directory) / name
            if path.is_symlink() and (os.path.isabs(os.readlink(path))
                                      or not path.resolve().is_relative_to(stdlib)):
                return path
    return None


@pytest.mark.platforms("linux")
@pytest.mark.parametrize("bootstrap_name", [None, "python3.11"])
def test_cold_cli_builds_own_runtime_discovers_plugins_and_repairs_app(tmp_path, bootstrap_name):
    from pm.packages import Python, Uv
    from pm.store import current_target, tree_digest

    uv = shutil.which("uv")
    if uv is None:
        pytest.skip("real uv and uvx must be on PATH")
    uv = Path(uv).resolve()
    uvx = uv.with_name("uvx")
    assert uvx.is_file(), "the real uv distribution must include uvx"
    python = Path(sys._base_executable).resolve()
    if sys.version_info[:2] != (3, 14):
        pytest.skip("the checked-in PM runtime currently requires Python 3.14")
    stdlib = next(d for d in (Path(sys.base_prefix) / "lib").glob("python3.*") if (d / "os.py").is_file())
    foreign = _foreign_link(stdlib)
    if foreign is not None:
        pytest.skip(f"host Python is not self-contained: {foreign} links outside its stdlib")
    bootstrap_python = shutil.which(bootstrap_name) if bootstrap_name else python
    if bootstrap_python is None:
        pytest.skip(f"{bootstrap_name} must be on PATH for the legacy bootstrap test")

    source = Path(__file__).resolve().parents[2]
    repo = tmp_path / "source"
    repo.mkdir()
    for name in ("pm", "hermes_cli"):
        shutil.copytree(source / name, repo / name,
                        ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
    for name in ("utils.py", "hermes_constants.py", "hermes_yaml.py",
                 "hermes_bootstrap.py"):
        shutil.copy2(source / name, repo / name)
    # No production application lock or metadata enters this source snapshot.
    recipe = tomllib.loads((repo / "pm" / "pyproject.toml").read_text())
    yaml_requirement = next(dep for dep in recipe["project"]["dependencies"]
                            if dep.startswith("ruamel.yaml"))
    (repo / "pyproject.toml").write_text(
        '[project]\nname="cold-pm-app"\nversion="0.0.0"\n'
        'requires-python=">=3.14,<3.15"\n'
        f'dependencies=[{json.dumps(yaml_requirement)}]\n'
        '[project.optional-dependencies]\nall=[]\n'
        '[tool.uv]\npackage=false\n', encoding="utf-8",
    )
    home = tmp_path / "home"
    hermes_home = home / ".hermes"
    plugin = hermes_home / "plugins" / "cold-proof"
    plugin.mkdir(parents=True)
    config = hermes_home / "config.yaml"
    config.write_text("plugins:\n  enabled: [cold-proof]\n", encoding="utf-8")
    (plugin / "plugin.yaml").write_text(
        "name: cold-proof\nversion: 1.0.0\npip_dependencies: [idna==3.10]\n",
        encoding="utf-8",
    )
    store = hermes_home / "tools"
    scratch = tmp_path / "scratch"
    scratch.mkdir()
    env = {
        "HOME": str(home), "HERMES_HOME": str(hermes_home),
        "HERMES_RUNTIME_DIR": str(store), "PATH": os.defpath,
        "TMPDIR": str(scratch), "LANG": "C.UTF-8", "LC_ALL": "C.UTF-8",
        "UV_PYTHON_DOWNLOADS": "never", "UV_NO_CONFIG": "1",
        "UV_CACHE_DIR": str(tmp_path / "seed-cache"),
    }
    # Keep TLS functional on Nix without inheriting any application settings.
    for key in ("SSL_CERT_FILE", "SSL_CERT_DIR", "NIX_SSL_CERT_FILE"):
        if key in os.environ:
            env[key] = os.environ[key]
    _run([str(uv), "lock", "--project", str(repo), "--python", str(python)],
         cwd=tmp_path, env=env)

    archives = tmp_path / "archives"
    archives.mkdir()
    target = current_target()
    rows = {}
    for package, files in (
        (Python(), [(python, "python/bin/python3")]),
        (Uv(), [(uv, "uv-dist/uv"), (uvx, "uv-dist/uvx")]),
    ):
        archive = archives / f"{package.name}.tar.gz"
        with tarfile.open(archive, "w:gz", compresslevel=1) as tar:
            for binary, name in files:
                tar.add(binary, arcname=name)
            if package.name == "python":
                # A relocatable python-build-standalone finds its stdlib beside the binary, not at
                # the host's prefix: ship the host's stdlib the way the real archive does.
                tar.add(stdlib, arcname=f"python/lib/{stdlib.name}",
                        filter=lambda info: None if any(part in info.name.split("/") for part in ("site-packages", "test", "__pycache__")) else info)
        version = _run([str(files[0][0]), "--version"], cwd=tmp_path,
                       env=env).stdout.split()[1]
        rows[package.name] = {
            "version": version,
            "artifacts": {target: {"sha256": hashlib.sha256(archive.read_bytes()).hexdigest()}},
        }
    server = ThreadingHTTPServer(("127.0.0.1", 0),
                                 partial(_ArchiveHandler, directory=str(archives)))
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()
    for name, row in rows.items():
        row["artifacts"][target]["url"] = f"http://127.0.0.1:{server.server_port}/{name}.tar.gz"
    (repo / "pm" / "lock.json").write_text(
        json.dumps({"schema": 1, "packages": rows}), encoding="utf-8",
    )

    bootstrap = """
import importlib.util
assert importlib.util.find_spec('ruamel') is None
assert importlib.util.find_spec('yaml') is None
assert importlib.util.find_spec('packaging') is None
assert importlib.util.find_spec('idna') is None
"""
    cli = "\nsys.argv = ['hermes', 'pm', {action!r}]; import hermes_cli.main\n"
    try:
        assert not store.exists()
        assert not (hermes_home / "installs").exists()
        result = _bare(bootstrap_python, repo, bootstrap + cli.format(action="install"), env=env)
        assert "✓ venv" in result.stdout
        assert "Preparing the isolated Hermes runtime" in result.stderr
        if bootstrap_name:
            # The installed launcher still starts on the old interpreter after
            # a source swap. Completion must re-exec before importing the app.
            (repo / ".git").mkdir()
            (repo / "install-stamp.json").write_text(
                json.dumps({"updateMechanism": "self"}), encoding="utf-8",
            )
            lock = repo / "uv.lock"
            lock.write_bytes(lock.read_bytes() + b"\n# source update\n")
            entry = repo / "launch_probe.py"
            entry.write_text(
                "import hermes_bootstrap\n"
                "import idna, json, sys\n"
                "print(json.dumps({'python': sys.executable, 'version': list(sys.version_info[:2]), "
                "'idna': idna.__file__}))\n", encoding="utf-8",
            )
            launched = _run([str(bootstrap_python), "-B", str(entry)], cwd=repo, env=env)
            launch_report = json.loads(launched.stdout)
            assert Path(launch_report["python"]).is_relative_to(store)
            assert launch_report["version"] == list(sys.version_info[:2])
            assert Path(launch_report["idna"]).is_relative_to(hermes_home / "installs")
            assert launched.stderr.count("completing source-update dependencies") == 1
            # The launch finished the update's shared tail, whose maintenance migrates
            # config.yaml; `pm repair` below must then leave that migrated file alone.
            assert "_config_version" in config.read_text()
    finally:
        server.shutdown()
        server.server_close()
        thread.join(timeout=10)
    expected_config = config.read_text()

    report_code = """
import json
from pathlib import Path
from pm.environments import install_state_dir, selected_venv, runtime_facts_path
root = Path(sys.path[0])
state = install_state_dir(root)
print(json.dumps({'state': str(state), 'app': str(selected_venv(root)),
                  'facts': json.loads(runtime_facts_path(root).read_text())}))
"""
    report = json.loads(_bare(python, repo, report_code, env=env).stdout)
    state, app = Path(report["state"]), Path(report["app"])
    assert state.is_relative_to(hermes_home)
    pm_root = state / "pm-runtime"
    selection = (pm_root / "selected.json").read_bytes()
    runtime = pm_root / json.loads(selection)["generation"]
    pm_python = runtime / "bin/python"
    assert runtime != app
    facts = json.loads((store / "facts.json").read_text())["packages"]
    assert set(facts) == {"python", "uv"}
    for name, fact in facts.items():
        assert fact["version"] == rows[name]["version"]
        assert fact["artifacts"] == [rows[name]["artifacts"][target]["sha256"]]
        assert tree_digest(store / fact["entry"]) == fact["digest"]

    probe = _run([str(pm_python), "-I", "-B", "-c", f"""
import importlib.util, json, sys
from pathlib import Path
sys.path.insert(0, {str(repo)!r})
from pm.workspace import enabled_member_dirs
from ruamel.yaml import YAML
import ruamel.yaml
assert enabled_member_dirs() == [Path({str(plugin)!r})]
assert importlib.util.find_spec('idna') is None
assert importlib.util.find_spec('openai') is None
print(json.dumps({{'yaml': ruamel.yaml.__file__, 'prefix': sys.prefix}}))
"""], cwd=tmp_path, env=env)
    pm_report = json.loads(probe.stdout)
    assert Path(pm_report["yaml"]).is_relative_to(runtime)
    app_code = """
import json
from pathlib import Path
from pm.environments import activate_dependencies, selected_venv
root = Path(sys.path[0])
activate_dependencies(root)
import ruamel.yaml, idna
from ruamel.yaml import YAML
assert idna.encode('bücher.example') == b'xn--bcher-kva.example'
assert YAML(typ='safe').load('proof: true')['proof'] is True
print(json.dumps({'yaml': ruamel.yaml.__file__, 'idna': idna.__file__,
                  'app': str(selected_venv(root))}))
"""
    app_report = json.loads(_bare(python, repo, app_code, env=env).stdout)
    assert Path(app_report["yaml"]).is_relative_to(app)
    assert Path(app_report["idna"]).is_relative_to(app)
    initial_fact = report["facts"]["packages"]["venv"]
    assert initial_fact["extras"] == ["all"]
    lock_before = Path(initial_fact["resolved_lock"]).read_bytes()
    assert b'idna' in lock_before

    # Real damage: the app no longer imports, while PM retains its independent YAML.
    shutil.rmtree(Path(app_report["yaml"]).parent)
    broken = _bare(python, repo, app_code, env=env, expected=1)
    assert "ModuleNotFoundError" in broken.stderr
    assert "ruamel.yaml" in broken.stderr
    repaired = _bare(python, repo, bootstrap + cli.format(action="repair"), env=env)
    assert "Restart Hermes" in repaired.stdout
    restored = json.loads(_bare(python, repo, app_code, env=env).stdout)
    repaired_app = Path(restored["app"])
    assert repaired_app != app
    assert Path(restored["yaml"]).is_relative_to(repaired_app)
    assert Path(restored["idna"]).is_relative_to(repaired_app)
    assert (pm_root / "selected.json").read_bytes() == selection
    after = json.loads(_bare(python, repo, report_code, env=env).stdout)
    repaired_fact = after["facts"]["packages"]["venv"]
    assert repaired_fact["extras"] == initial_fact["extras"]
    assert repaired_fact["stamp"] == initial_fact["stamp"]
    assert Path(repaired_fact["resolved_lock"]).read_bytes() == lock_before
    assert config.read_text() == expected_config
    if not bootstrap_name:
        assert expected_config == "plugins:\n  enabled: [cold-proof]\n"
    assert not (state / ".repair-incomplete").exists()
    assert not (repo / "venv").exists()
    assert not (repo / ".venv").exists()
    print(f"cold PM proof: runtime={runtime}; first_app={app}; repaired_app={repaired_app}")
