"""Prepared inputs become runnable products without acquiring dependencies."""
from __future__ import annotations

import json
import os
from pathlib import Path
import shutil
import subprocess
import sys

import pytest
from tests.pm._fixtures import stage_host_python

ROOT = Path(__file__).resolve().parents[2]


def inputs_fixture(tmp_path):
    source = tmp_path / "prepared source"
    source.mkdir()
    (source / "pyproject.toml").write_text(
        '[project]\nname="assembly-fixture"\nversion="1.2.3"\n'
        '[project.scripts]\nprobe="entry:main"\n', encoding="utf-8")
    (source / "entry.py").write_text(
        "import json, os, sys\nfrom pathlib import Path\nimport dependency\n"
        "from importlib.metadata import version\n"
        "def main():\n print(json.dumps([sys.argv[1:], dependency.VALUE, "
        "version('assembly-fixture'), (Path(__file__).parent/'skills/data').read_text()]))\n"
        " return 7\n", encoding="utf-8")
    (source / "skills").mkdir()
    (source / "skills/data").write_text("resource", encoding="utf-8")
    out = tmp_path / "payload"
    python = stage_host_python(out / "tools/python/bin/python3")
    site = out / "venv/lib/python3.14/site-packages"
    site.mkdir(parents=True)
    (site / "dependency.py").write_text("VALUE = 'prepared'\n", encoding="utf-8")
    (site / "entry.py").write_text("raise RuntimeError('dependency shadowed source')\n", encoding="utf-8")
    pm = out / "pm-runtime"
    (pm / "lib/site-packages").mkdir(parents=True)
    (pm / "pm-runtime.json").write_text(json.dumps({
        "python": "../tools/python/bin/python3", "sitePackages": "lib/site-packages"}), encoding="utf-8")
    tui, web = tmp_path / "tui", tmp_path / "web"
    (tui / "dist").mkdir(parents=True)
    web.mkdir()
    (tui / "dist/entry.js").write_text("export const built = true;", encoding="utf-8")
    (tui / "package.json").write_text('{"type":"module"}', encoding="utf-8")
    (web / "index.html").write_text("built web", encoding="utf-8")
    return out, {
        "project": str(source / "pyproject.toml"), "code": str(source),
        "repo": "app", "target": "linux-x64", "placement": "contained",
        "python": str(python), "site_packages": str(site),
        "environment": str(out / "venv"), "tools": str(out / "tools"),
        "pm_runtime": str(pm), "resources": {"skills": str(source / "skills")},
        "frontends": {"tui": str(tui), "web": str(web)}, "ref": "fixture-revision",
    }


def build_cli(data, out, tmp_path):
    document = tmp_path / "inputs.json"
    document.write_text(json.dumps(data), encoding="utf-8")
    home = tmp_path / "home"
    home.mkdir(exist_ok=True)
    return subprocess.run(
        [sys.executable, "-B", "-m", "scripts.build.agent", "--inputs", str(document), "--out", str(out)],
        cwd=tmp_path, env={"PATH": os.environ["PATH"], "HOME": str(home),
                          "HERMES_HOME": str(home / ".hermes"), "PYTHONPATH": str(ROOT)},
        capture_output=True, text=True, timeout=30,
    )


@pytest.mark.platforms("posix")
def test_contained_cli_assembly_runs_after_move_and_preserves_prepared_state(tmp_path):
    out, data = inputs_fixture(tmp_path)
    (out / "provider-state").write_text("retain", encoding="utf-8")
    before = {p.relative_to(data["code"]): p.read_bytes() for p in Path(data["code"]).rglob("*") if p.is_file()}
    result = build_cli(data, out, tmp_path)
    assert result.returncode == 0, result.stderr
    assert (out / "provider-state").read_text() == "retain"
    manifest = json.loads((out / "manifest.json").read_text())
    assert manifest["ref"] == data["ref"]
    assert manifest["runtime"] == {
        "repoDir": "app", "toolsDir": "tools", "storePython": "tools/python/bin/python3",
        "sitePackages": "venv/lib/python3.14/site-packages", "commands": {"probe": "bin/probe"}}
    assert (out / "app/hermes_cli/tui_dist/entry.js").is_file()
    assert json.loads((out / "app/hermes_cli/tui_dist/package.json").read_text())["type"] == "module"
    assert (out / "app/hermes_cli/web_dist/index.html").is_file()
    assert not (tmp_path / "home/.hermes").exists()
    moved = tmp_path / "relocated payload"
    out.rename(moved)
    (tmp_path / "entry.py").write_text("raise RuntimeError('cwd shadowed source')", encoding="utf-8")
    run = subprocess.run([str(moved / manifest["runtime"]["commands"]["probe"]), "two words", "$(no)", ""],
                         cwd=tmp_path, env={"PATH": os.environ["PATH"], "HOME": str(tmp_path / "home"),
                                            "PYTHONPATH": "/foreign", "PYTHONHOME": "/foreign"},
                         text=True, capture_output=True, timeout=30)
    assert run.returncode == 7, run.stderr
    assert json.loads(run.stdout) == [["two words", "$(no)", ""], "prepared", "1.2.3", "resource"]
    assert before == {p.relative_to(data["code"]): p.read_bytes() for p in Path(data["code"]).rglob("*") if p.is_file()}
    assert not list(moved.rglob("__pycache__"))
    assert (moved / "pm-runtime/pm-runtime.json").is_file()


@pytest.mark.platforms("posix")
def test_referenced_installed_code_uses_derived_command_map_without_copying(tmp_path):
    _, data = inputs_fixture(tmp_path)
    out = tmp_path / "nix output"
    source = Path(data["code"])
    (source / "entry.py").write_text(
        "import json, os\nfrom pathlib import Path\n"
        "def main():\n print(json.dumps([Path(os.environ['HERMES_BUNDLED_SKILLS'],'data').read_text(),"
        "os.environ['HERMES_NODE']]))\n", encoding="utf-8")
    bindir = tmp_path / "installed env/bin"
    bindir.mkdir(parents=True)
    command = bindir / "probe"
    command.write_text(f"#!{sys.executable}\nimport sys\nsys.path.insert(0, {str(source)!r})\nfrom entry import main\nmain()\n", encoding="utf-8")
    command.chmod(0o755)
    data.update(placement="references", repo="share/hermes-agent", command_dir=str(bindir),
                env={"HERMES_NODE": "/prepared/node"})
    before = {p: p.read_bytes() for p in source.rglob("*") if p.is_file()}
    result = build_cli(data, out, tmp_path)
    assert result.returncode == 0, result.stderr
    mapping = json.loads((out / "command-map.json").read_text())
    assert mapping["commands"] == {"probe": {
        "source": str(command), "destination": "bin/probe", "entry": "entry:main"}}
    assert Path(mapping["env"]["HERMES_TUI_DIR"]).resolve() == Path(data["frontends"]["tui"])
    assert Path(mapping["env"]["HERMES_WEB_DIST"]).resolve() == Path(data["frontends"]["web"])
    assert mapping["env"]["HERMES_INSTALL_ROOT"] == str(out / "share/hermes-agent")
    assert not list(out.rglob("entry.py"))
    assert (out / "share/hermes-agent/skills").is_symlink()
    assert (out / "ui-tui").is_symlink()
    assert (out / "share/hermes-agent/web_dist").is_symlink()
    assert not (out / "bin/probe").exists(), "Nix owns native makeWrapper"
    run = subprocess.run([mapping["commands"]["probe"]["source"]], cwd=tmp_path,
                         env={"PATH": os.environ["PATH"], "HOME": str(tmp_path / "home"),
                              "PYTHONDONTWRITEBYTECODE": "1", **mapping["env"]},
                         text=True, capture_output=True, check=True)
    assert json.loads(run.stdout) == ["resource", "/prepared/node"]
    assert before == {p: p.read_bytes() for p in source.rglob("*") if p.is_file()}


@pytest.mark.platforms("posix")
def test_fixed_root_keeps_privilege_shim_and_resolves_venv_command_symlink(tmp_path):
    out, data = inputs_fixture(tmp_path)
    # Docker source already occupies /opt/hermes; its bin/hermes belongs to s6.
    shutil.copytree(data["code"], out, dirs_exist_ok=True)
    data.update(placement="fixed", repo=".", code=str(out), project=str(out / "pyproject.toml"), bin_dir="libexec", python=sys.executable)
    bindir = out / "bin"
    bindir.mkdir()
    (bindir / "probe").write_text("privilege shim", encoding="utf-8")
    stale = out / "hermes_cli/web_dist/stale"
    stale.parent.mkdir(parents=True)
    stale.write_bytes(b"old surface")
    result = build_cli(data, out, tmp_path)
    assert result.returncode == 0, result.stderr
    assert (bindir / "probe").read_text() == "privilege shim"
    assert not stale.exists()
    link = out / "venv/bin/probe"
    link.parent.mkdir()
    link.symlink_to("../../libexec/probe")
    run = subprocess.run([str(link), "ok"], cwd=tmp_path,
                         env={"PATH": os.environ["PATH"], "HOME": str(tmp_path / "home")},
                         text=True, capture_output=True)
    assert run.returncode == 7, run.stderr
    assert json.loads(run.stdout)[0] == ["ok"]


@pytest.mark.platforms("posix")
def test_source_metadata_keeps_declared_requirements_extras_and_entrypoints(tmp_path):
    from importlib.metadata import distributions
    from packaging.requirements import Requirement

    out, data = inputs_fixture(tmp_path)
    project = Path(data["project"])
    project.write_text(
        '[project]\nname="assembly-fixture"\nversion="1.2.3"\nrequires-python=">=3.11"\n'
        'dependencies=["dependency>=1,<2; python_version >= \'3.11\'"]\n'
        '[project.optional-dependencies]\nextra=["optional>=2; sys_platform == \'linux\'"]\n'
        '[project.scripts]\nprobe="entry:main"\n', encoding="utf-8")
    result = build_cli(data, out, tmp_path)
    assert result.returncode == 0, result.stderr
    metadata = next(distributions(path=[str(out / "app")]))
    assert metadata.metadata["Requires-Python"] == ">=3.11"
    reqs = {Requirement(r).name: Requirement(r) for r in metadata.requires}
    assert str(reqs["dependency"].specifier) == "<2,>=1"
    optional = reqs["optional"]
    assert optional.marker.evaluate({"extra": "extra", "sys_platform": "linux"})
    assert not optional.marker.evaluate({"extra": "", "sys_platform": "linux"})
    assert not optional.marker.evaluate({"extra": "extra", "sys_platform": "win32"})
    assert [(e.name, e.value) for e in metadata.entry_points] == [("probe", "entry:main")]


@pytest.mark.platforms("posix")
@pytest.mark.parametrize("fault", ["missing-pm", "missing-resource", "missing-tui", "missing-web", "unknown-field", "bad-repo", "bad-target"])
def test_failed_inputs_cannot_leave_a_completion_claim(tmp_path, fault):
    out, data = inputs_fixture(tmp_path)
    (out / "manifest.json").write_text('{"runtime": {"commands": {}}}', encoding="utf-8")
    if fault == "missing-pm":
        (Path(data["pm_runtime"]) / "pm-runtime.json").unlink()
    elif fault == "missing-resource":
        data["resources"]["skills"] = str(tmp_path / "missing skills")
    elif fault == "missing-tui":
        (Path(data["frontends"]["tui"]) / "dist/entry.js").unlink()
    elif fault == "missing-web":
        (Path(data["frontends"]["web"]) / "index.html").unlink()
    elif fault == "unknown-field":
        data["typo"] = True
    elif fault == "bad-repo":
        data["repo"] = "../escape"
    else:
        data["target"] = "linux-typo"
    result = build_cli(data, out, tmp_path)
    assert result.returncode != 0
    assert not (out / "manifest.json").exists()
    assert not (out / "bin/probe").exists()


@pytest.mark.platforms("posix")
def test_incremental_copy_drops_removed_source_without_deleting_provider_files(tmp_path):
    out, data = inputs_fixture(tmp_path)
    source = Path(data["code"])
    (source / "obsolete.py").write_text("old", encoding="utf-8")
    assert build_cli(data, out, tmp_path).returncode == 0
    sentinel = out / "provider-file"
    sentinel.write_text("retain", encoding="utf-8")
    (source / "obsolete.py").unlink()
    (out / "app/hermes_cli/web_dist/stale").write_text("old", encoding="utf-8")
    assert build_cli(data, out, tmp_path).returncode == 0
    assert not (out / "app/obsolete.py").exists()
    assert not (out / "app/hermes_cli/web_dist/stale").exists()
    assert (out / "app/hermes_cli/tui_dist/entry.js").read_bytes() == (Path(data["frontends"]["tui"]) / "dist/entry.js").read_bytes()
    assert sentinel.read_text() == "retain"


@pytest.mark.platforms("posix")
def test_payload_smoke_uses_relocated_manifest_commands(tmp_path):
    out, data = inputs_fixture(tmp_path)
    source = Path(data["code"])
    (source / "pyproject.toml").write_text(
        '[project]\nname="smoke-fixture"\nversion="1"\n'
        '[project.scripts]\nhermes="entry:main"\n', encoding="utf-8")
    (source / "entry.py").write_text(
        "import json, os, sys\nfrom pathlib import Path\n"
        "def main():\n"
        f" assert not Path({str(out)!r}).exists(), 'payload was not moved'\n"
        " assert 'HERMES_PYTHON' not in os.environ\n"
        " assert not Path.cwd().is_relative_to(Path(__file__).parent)\n"
        " if sys.argv[1:] == ['tools', 'list']:\n"
        "  import dependency\n"
        "  print(json.dumps([sys.argv[1:], dependency.VALUE]))\n"
        " else: print('fast path')\n", encoding="utf-8")
    data["bin_dir"] = "libexec"
    harness = Path(data["site_packages"]) / "browser_harness"
    harness.mkdir()
    (harness / "__init__.py").write_text("", encoding="utf-8")
    (harness / "run.py").write_text("print('harness ok')\n", encoding="utf-8")
    assert build_cli(data, out, tmp_path).returncode == 0
    shutil.rmtree(source)
    env = dict(os.environ, PYTHONPATH="/foreign", PYTHONHOME="/foreign", HERMES_PYTHON="/foreign")
    command = ["bash", str(ROOT / "scripts/smoke-payload.sh"), str(out)]
    result = subprocess.run(command, cwd=tmp_path, env=env, capture_output=True, text=True, timeout=30)
    assert result.returncode == 0, result.stdout + result.stderr
    assert '"prepared"' in result.stdout
    assert out.is_dir(), "smoke must restore the artifact for packaging"
    assert "harness ok" in result.stdout
    shutil.rmtree(harness)
    no_harness = subprocess.run(command, cwd=tmp_path, env=env, capture_output=True, text=True, timeout=30)
    assert no_harness.returncode != 0
    assert "browser-harness failed" in no_harness.stdout + no_harness.stderr
    dependency = Path(data['site_packages']) / 'dependency.py'
    dependency.unlink()
    missing_dep = subprocess.run(command, cwd=tmp_path, env=env, capture_output=True, text=True, timeout=30)
    assert missing_dep.returncode != 0
    assert 'SMOKE OK' not in missing_dep.stdout
    # A broken published command cannot be rescued by importing raw Python.
    (out / "libexec/hermes").unlink()
    failed = subprocess.run(command, cwd=tmp_path, env=env, capture_output=True, text=True, timeout=30)
    assert failed.returncode != 0
    assert "SMOKE OK" not in failed.stdout
    assert out.is_dir(), "failed smoke must also restore the artifact"
    # Even an executable reporting success is not a payload command if it escapes.
    manifest = json.loads((out / 'manifest.json').read_text())
    external = shutil.which('true')
    assert external
    (out / 'libexec/hermes').symlink_to(external)
    for path in (external, 'libexec/hermes'):
        manifest['runtime']['commands']['hermes'] = path
        (out / 'manifest.json').write_text(json.dumps(manifest), encoding='utf-8')
        escaped = subprocess.run(command, cwd=tmp_path, env=env, capture_output=True, text=True, timeout=30)
        assert escaped.returncode != 0, escaped.stdout
        assert 'SMOKE OK' not in escaped.stdout


@pytest.mark.platforms("posix")
def test_stage_passes_explicit_products_to_the_single_assembler(tmp_path, monkeypatch):
    from scripts.bundles import stage
    from scripts.build.agent import assemble
    from scripts.build.inputs import AgentInputs

    out, data = inputs_fixture(tmp_path)
    calls = []
    def native(args):
        calls.append(args.frontends)
        assemble(AgentInputs.from_dict({**data, "frontends": args.frontends}), out)
        return 0
    monkeypatch.setattr(stage, "stage_native", native)
    assert stage.main(["--out", str(out), "--ref", "fixture", "--tui", data["frontends"]["tui"],
                       "--web", data["frontends"]["web"]]) == 0
    assert calls == [{k: Path(v) for k, v in data["frontends"].items()}]
    assert json.loads((out / "manifest.json").read_text())["runtime"]["commands"] == {"probe": "bin/probe"}


@pytest.mark.platforms("posix")
def test_prepared_environment_python_imports_app_from_unrelated_script(tmp_path):
    out, data = inputs_fixture(tmp_path)
    environment = out / ".venv"
    subprocess.run([sys.executable, "-m", "venv", "--without-pip", str(environment)], check=True)
    python = environment / "bin/python"
    site = subprocess.check_output([str(python), "-I", "-c", "import sysconfig; print(sysconfig.get_paths()['purelib'])"], text=True).strip()
    Path(site, "dependency.py").write_text("VALUE='prepared'", encoding="utf-8")
    data.update(placement="fixed", environment=str(environment), python=str(python), site_packages=site)
    result = build_cli(data, out, tmp_path)
    assert result.returncode == 0, result.stderr
    script = tmp_path / "external-probe.py"
    script.write_text("import entry\nfrom importlib.metadata import version\nprint(version('assembly-fixture'))\n", encoding="utf-8")
    run = subprocess.run([str(python), "-I", str(script)], cwd=tmp_path,
                         env={"PATH": os.environ["PATH"], "HOME": str(tmp_path / "home"), "PYTHONDONTWRITEBYTECODE": "1"},
                         capture_output=True, text=True)
    assert run.returncode == 0, run.stderr
    assert run.stdout.strip() == "1.2.3"


def test_site_layout_uses_explicit_target_version_not_builder_python(tmp_path):
    from scripts.build.inputs import dependency_site
    assert dependency_site(tmp_path, "3.99.1+vendor", "linux-x64") == tmp_path / "lib/python3.99/site-packages"
    assert dependency_site(tmp_path, "3.99.1", "win32-arm64") == tmp_path / "Lib/site-packages"


def test_automatic_frontends_use_requested_snapshot_not_current_checkout(tmp_path, monkeypatch):
    from scripts.bundles import stage
    repo = tmp_path / "checkout"
    repo.mkdir()
    subprocess.run(["git", "init", str(repo)], check=True, capture_output=True)
    project = repo / "pyproject.toml"
    project.write_text('[project]\nversion="1"\n', encoding="utf-8")
    subprocess.run(["git", "add", "."], cwd=repo, check=True)
    subprocess.run(["git", "-c", "user.name=Fixture", "-c", "user.email=f@example.test", "commit", "-m", "selected"], cwd=repo, check=True, capture_output=True)
    revision = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=repo, text=True).strip()
    project.write_text('[project]\nversion="dirty-current"\n', encoding="utf-8")
    monkeypatch.setattr(stage, "ROOT", repo)
    monkeypatch.setattr("scripts.build.icon_environment.prepare_icon_environment",
                        lambda source, out, cache: sys.executable)
    monkeypatch.setattr(stage.shutil, "which", lambda name: "/prepared/node")
    real_run = subprocess.run
    built_sources = []
    def run(command, **kwargs):
        if command[0] != "/prepared/node":
            return real_run(command, **kwargs)
        source = Path(command[command.index("--source") + 1])
        built_sources.append(source)
        assert source != repo
        assert 'version="1"' in (source / "pyproject.toml").read_text()
    monkeypatch.setattr(stage.subprocess, "run", run)
    monkeypatch.setattr(stage, "stage_native", lambda args: 0)
    assert stage.main(["--out", str(tmp_path / "out"), "--ref", revision]) == 0
    assert len(built_sources) == 4
