"""Desktop preparation keeps dependency acquisition out of live installs."""
from __future__ import annotations

import importlib
import os
from pathlib import Path
import json
import subprocess
import sys
from types import SimpleNamespace

import pytest


def test_bootstrap_environment_isolates_owned_paths_without_mutating_caller(tmp_path):
    toolchain = importlib.import_module("scripts.bundles.desktop_toolchain")
    source, work, cache = (tmp_path / name for name in ("source", "work", "cache"))
    original_home = tmp_path / "live-user"
    inherited = {
        "HOME": str(original_home), "USERPROFILE": str(original_home),
        "LOCALAPPDATA": str(original_home / "AppData/Local"),
        "APPDATA": str(original_home / "AppData/Roaming"),
        "HERMES_HOME": str(original_home / "profile"),
        "HERMES_RUNTIME_DIR": str(original_home / "tools"),
        "HERMES_INSTALL_ROOT": str(original_home / "installed"),
        "HERMES_PAYLOAD_ROOT": str(original_home / "payload"),
        "HERMES_PAYLOAD_TAG": "v1.2.3", "HERMES_BUILD_COMMIT": "a" * 40,
        "HERMES_SITE": str(original_home / "site"),
        "HERMES_PYTHON_SRC_ROOT": str(original_home / "repo"),
        "HERMES_PYTHON": str(original_home / "python"),
        "HERMES_NODE": str(original_home / "node"),
        "HERMES_PROFILE": "live", "HERMES_REAL_HOME": str(original_home),
        "HERMES_BUNDLED_SKILLS": str(original_home / "skills"),
        "HERMES_OPTIONAL_MCPS": str(original_home / "mcps"),
        "VIRTUAL_ENV": str(original_home / "venv"),
        "PYTHONHOME": str(original_home / "python-home"),
        "PYTHONPATH": str(original_home / "site"),
        "UV_CACHE_DIR": str(original_home / "uv-cache"),
        "npm_config_cache": str(original_home / "npm-cache"),
        "npm_execpath": str(original_home / "foreign-npm.js"),
        "NPM_CONFIG_USERCONFIG": str(original_home / "credentials.npmrc"),
        "CARGO_HOME": str(original_home / "custom-cargo"),
        "RUSTUP_HOME": str(original_home / "custom-rustup"),
        "PATH": os.defpath, "SIGNING_TOKEN": "inherited-not-serialized",
    }
    before = inherited.copy()
    process_before = dict(os.environ)
    environment = toolchain.bootstrap_environment(source, work, cache, inherited)
    assert inherited == before
    assert dict(os.environ) == process_before
    assert environment["CARGO_HOME"] == inherited["CARGO_HOME"]
    assert environment["RUSTUP_HOME"] == inherited["RUSTUP_HOME"]
    for key in ("HOME", "USERPROFILE", "LOCALAPPDATA", "APPDATA", "HERMES_HOME",
                "XDG_CONFIG_HOME", "XDG_CACHE_HOME"):
        assert Path(environment[key]).is_relative_to(work)
    assert Path(environment["HERMES_RUNTIME_DIR"]) == cache / "tools"
    assert Path(environment["UV_CACHE_DIR"]) == cache / "python/runtime"
    assert Path(environment["npm_config_cache"]) == cache / "npm"
    assert "npm_execpath" not in environment
    assert "NPM_CONFIG_USERCONFIG" not in environment
    assert environment["npm_config_userconfig"] == os.devnull
    assert "npm_config_globalconfig" not in environment
    assert environment["HERMES_PYTHON_SRC_ROOT"] == str(source)
    for key in ("HERMES_INSTALL_ROOT", "HERMES_PAYLOAD_ROOT", "HERMES_PAYLOAD_TAG",
                "HERMES_BUILD_COMMIT", "HERMES_SITE", "HERMES_PROFILE", "HERMES_REAL_HOME",
                "HERMES_PYTHON", "HERMES_NODE", "HERMES_BUNDLED_SKILLS", "HERMES_OPTIONAL_MCPS",
                "VIRTUAL_ENV", "PYTHONHOME", "PYTHONPATH"):
        assert key not in environment
    assert environment["SIGNING_TOKEN"] == inherited["SIGNING_TOKEN"]
    assert environment["PATH"] == inherited["PATH"]
    assert not work.exists()  # Forming the child environment is pure.

    defaults = toolchain.bootstrap_environment(source, work, cache, {"HOME": str(original_home),
                                                                   "USERPROFILE": str(original_home)})
    assert defaults["CARGO_HOME"] == str(original_home / ".cargo")
    assert defaults["RUSTUP_HOME"] == str(original_home / ".rustup")

    mixed = toolchain.bootstrap_environment(source, work, cache, {
        "Home": str(original_home), "UserProfile": str(original_home), "LocalAppData": "live-local",
        "Hermes_Home": "live-profile", "Hermes_Runtime_Dir": "live-store", "Cargo_Home": "custom-cargo",
        "Rustup_Home": "custom-rustup", "NPM_CONFIG_CACHE": "live-npm", "Path": os.defpath,
    })
    assert mixed["CARGO_HOME"] == "custom-cargo" and mixed["RUSTUP_HOME"] == "custom-rustup"
    assert len([key for key in mixed if key.upper() == "HOME"]) == 1
    assert len([key for key in mixed if key.upper() == "HERMES_RUNTIME_DIR"]) == 1


@pytest.mark.parametrize("exit_code", [0, 7])
@pytest.mark.platforms("linux", "macos", "windows")
def test_run_preparation_bootstraps_before_worker_in_isolated_child(tmp_path, monkeypatch, exit_code):
    from scripts.bundles import desktop_toolchain

    source, work, cache = (tmp_path / name for name in ("source space", "work", "cache"))
    (source / "pm").mkdir(parents=True)
    (source / "pm/__init__.py").write_text("", encoding="utf-8")
    # A bootstrap double runs in a REAL child: imports must already be isolated,
    # not temporarily redirected in the parent's process around the PM call.
    (source / "pm/runtime.py").write_text(
        "import os, sys\n"
        "from pathlib import Path\n"
        "assert Path(os.environ['HOME']).name == 'home'\n"
        "assert 'HERMES_INSTALL_ROOT' not in os.environ\n"
        "assert sys.flags.isolated and sys.flags.no_site\n"
        "def runtime_command(script, args, *, cache):\n"
        "    assert cache == Path(os.environ['UV_CACHE_DIR'])\n"
        "    return [sys.executable, '-I', '-B', str(script), *args]\n",
        encoding="utf-8",
    )
    worker = source / "scripts/bundles/desktop_prepare.py"
    worker.parent.mkdir(parents=True)
    worker.write_text(
        "import json, os, sys\n"
        "from pathlib import Path\n"
        "assert sys.argv[1] == '--request' and sys.argv[3:] == ['--worker']\n"
        "import ssl\n"
        "assert ssl.SSLContext.__module__.startswith('truststore')\n"
        "request = json.loads(Path(sys.argv[2]).read_text())\n"
        "assert os.environ['HERMES_RUNTIME_DIR'] == request['tools']\n"
        "Path(request['receipt']).write_text(str(Path.cwd()))\n"
        "sys.exit(request['exit_code'])\n", encoding="utf-8",
    )
    request_file = tmp_path / "request space.json"
    receipt = tmp_path / "worker-ran"
    request_file.write_text(json.dumps({"receipt": str(receipt), "tools": str(cache / "tools"),
                                        "exit_code": exit_code}), encoding="utf-8")
    monkeypatch.setenv("HERMES_INSTALL_ROOT", str(tmp_path / "live-install"))
    before = dict(os.environ)
    assert desktop_toolchain.run_preparation(source, work, cache, request_file) == exit_code
    assert dict(os.environ) == before
    assert receipt.read_text(encoding="utf-8-sig") == str(source)


def test_bootstrap_real_pm_resolves_only_build_owned_state(tmp_path):
    from scripts.bundles.desktop_toolchain import bootstrap_environment

    source = Path(__file__).resolve().parents[2]
    work, cache = tmp_path / "work", tmp_path / "cache"
    env = bootstrap_environment(source, work, cache, os.environ)
    probe = (
        "import json, sys; from pathlib import Path; sys.path.insert(0, sys.argv[1]); "
        "from pm import paths; from pm.packages import uv_cache_dir; "
        "from pm.environments import install_state_dir; "
        "print(json.dumps([str(paths.store_root()), str(paths.partials_root()), "
        "str(uv_cache_dir()), str(install_state_dir(Path(sys.argv[1])))]))"
    )
    result = subprocess.run([sys.executable, "-I", "-S", "-B", "-c", probe, str(source)],
                            env=env, capture_output=True, text=True, check=True)
    store, partials, uv_default, state = map(Path, json.loads(result.stdout))
    assert store == cache / "tools"
    assert all(path.is_relative_to(work) for path in (partials, uv_default, state))


@pytest.mark.parametrize("target", ["darwin-arm64", "darwin-x64", "linux-x64", "win32-arm64"])
def test_packaging_preserves_keychain_home_without_retargeting_build_state(tmp_path, target):
    from scripts.bundles.desktop_inputs import packaging_environment
    from scripts.bundles.desktop_toolchain import bootstrap_environment

    source, work, cache = (tmp_path / name for name in ("source", "work", "cache"))
    login_home = str(tmp_path / "login")
    inherited = {"HOME": login_home, "CSC_KEYCHAIN": "explicit.keychain"}
    isolated = bootstrap_environment(source, work, cache, inherited)
    before = isolated.copy()
    for caller in (inherited, {**inherited, "HOME": str(work / "launcher-home"),
                               "HERMES_REAL_HOME": login_home}, {}):
        packaged = packaging_environment(isolated, caller, target)
        expected_home = (caller.get("HERMES_REAL_HOME") or caller.get("HOME") or str(Path.home())
                         if target.startswith("darwin-") else isolated["HOME"])
        assert packaged == {**isolated, "HOME": expected_home}
    assert isolated == before


@pytest.mark.platforms("linux", "macos", "windows")
def test_prepare_tools_uses_pm_native_pins_and_separate_cache(tmp_path, monkeypatch):
    import pm
    from scripts.bundles import desktop_toolchain
    from pm import native_build

    source, work, cache = (tmp_path / name for name in ("source", "work", "cache"))
    env = desktop_toolchain.bootstrap_environment(source, work, cache, os.environ)
    monkeypatch.setenv("HERMES_RUNTIME_DIR", env["HERMES_RUNTIME_DIR"])
    acquired, native_calls = [], []
    bins = {name: cache / "tools" / name / "bin" / name for name in ("python", "node")}
    for binary in bins.values():
        binary.parent.mkdir(parents=True)
        binary.touch()

    def ensure(name, *, explicit):
        assert explicit
        acquired.append(name)

    def native(**kwargs):
        native_calls.append(kwargs)
        return {**kwargs["env"], "OPENSSL_DIR": str(cache / "native/openssl")}

    monkeypatch.setattr(pm, "ensure", ensure)
    monkeypatch.setattr(pm, "installed_package", lambda name: SimpleNamespace(binary=bins[name]))
    monkeypatch.setattr(pm, "env_for", lambda *names, base_env: {**base_env, "PATH": "pm-tools"})
    monkeypatch.setattr(native_build, "prepare_windows_environment", native)
    monkeypatch.setattr(desktop_toolchain, "native_cache_path", lambda cache, env: cache / "python/runtime/native-identity",
                        raising=False)
    before = dict(os.environ)
    python, node, prepared = desktop_toolchain.prepare_tools(source, work, cache, env)
    assert acquired == ["uv", "npm"]
    assert python == bins["python"] and node == bins["node"]
    assert prepared["HERMES_PYTHON"] == str(python)
    assert prepared["HERMES_NODE"] == str(node)
    assert prepared["PATH"] == "pm-tools"
    assert Path(prepared["UV_CACHE_DIR"]) == cache / "python/runtime/native-identity"
    assert dict(os.environ) == before
    if pm.current_target() == "win32-arm64":
        assert len(native_calls) == 1
        assert native_calls[0]["state"].is_relative_to(cache / "native")
        assert prepared["OPENSSL_DIR"] == str(cache / "native/openssl")
    else:
        assert native_calls == []


@pytest.mark.platforms("linux", "macos", "windows")
def test_native_cache_identity_tracks_compilers_sdk_and_openssl(tmp_path, monkeypatch):
    from scripts.bundles import desktop_toolchain

    openssl = tmp_path / "openssl"
    (openssl / "include/openssl").mkdir(parents=True)
    (openssl / "include/openssl/opensslv.h").write_bytes(b"version one")
    (openssl / "lib").mkdir()
    library = openssl / "lib/libcrypto.lib"
    library.write_bytes(b"first build")
    (openssl / "lib/libssl.lib").write_bytes(b"ssl build")
    env = {"PATH": os.defpath, "OPENSSL_DIR": str(openssl),
           "WindowsSDKVersion": "10.0.1", "VCToolsVersion": "14.1"}
    version = ["compiler one"]

    def probe(command, **kwargs):
        assert kwargs["env"] == env
        return SimpleNamespace(returncode=0, stdout=version[0], stderr="")

    # Let stdlib cache its real-host probe before substituting compiler commands.
    desktop_toolchain.platform.platform()
    monkeypatch.setattr(desktop_toolchain.subprocess, "run", probe)
    cache = tmp_path / "cache"
    first = desktop_toolchain.native_cache_path(cache, env)
    assert first.is_relative_to(cache / "python/runtime")
    assert desktop_toolchain.native_cache_path(cache, env) == first
    moved_cache = tmp_path / "relocated-cache"
    moved_openssl = moved_cache / "native/openssl"
    import shutil
    shutil.copytree(openssl, cache / "native/openssl")
    shutil.copytree(openssl, moved_openssl)
    env["OPENSSL_DIR"] = str(cache / "native/openssl")
    portable = desktop_toolchain.native_cache_path(cache, env)
    env["OPENSSL_DIR"] = str(moved_openssl)
    assert desktop_toolchain.native_cache_path(moved_cache, env).name == portable.name
    env["OPENSSL_DIR"] = str(openssl)
    version[0] = "compiler two"
    second = desktop_toolchain.native_cache_path(cache, env)
    assert second != first
    library.write_bytes(b"changed build")
    third = desktop_toolchain.native_cache_path(cache, env)
    assert third != second
    env["WindowsSDKVersion"] = "10.0.2"
    assert desktop_toolchain.native_cache_path(cache, env) != third
    monkeypatch.setattr(desktop_toolchain.subprocess, "run", lambda *a, **k: (_ for _ in ()).throw(FileNotFoundError()))
    assert desktop_toolchain.native_cache_path(cache, env) != desktop_toolchain.native_cache_path(cache, env)


@pytest.mark.parametrize("complete", [False, True])
def test_native_cache_leaves_room_for_sdist_compiler_outputs(tmp_path, monkeypatch, complete):
    from pathlib import PureWindowsPath
    from scripts.bundles import desktop_toolchain

    # Replay the Windows CI layout as path data, without faking the host OS.
    # uv builds in its cached sdist; /Fo is relative to that working directory.
    cache = tmp_path / "cache"
    desktop_toolchain.platform.platform()

    def probe(*args, **kwargs):
        if not complete:
            raise FileNotFoundError()
        return SimpleNamespace(returncode=0, stdout="compiler identity", stderr="")

    openssl = cache / "native/openssl"
    for name in ("include/openssl/opensslv.h", "lib/libcrypto.lib"):
        path = openssl / name
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_bytes(b"native input")
    monkeypatch.setattr(desktop_toolchain.subprocess, "run", probe)
    selected = desktop_toolchain.native_cache_path(cache, {
        "WindowsSDKVersion": "10.0.1", "VCToolsVersion": "14.1", "OPENSSL_DIR": str(openssl),
    })
    assert selected.is_relative_to(cache / "python/runtime")
    runner_cache = PureWindowsPath("D:/a/hermes-agent/hermes-agent/.cache/desktop-inputs")
    sdist = PureWindowsPath("sdists-v9/pypi/pilk/0.2.4/5b4cbVXt0GPuGQrp/src")
    for architecture in ("win-amd64", "win-arm64"):
        output = (runner_cache.joinpath(*selected.relative_to(cache).parts) / sdist
                  / f"build/temp.{architecture}-cpython-314/Release/src/SKP_SILK_SRC"
                  / "SKP_Silk_NLSF_VQ_rate_distortion_FIX.obj")
        assert len(str(output)) < 260, str(output)
