"""Prepared desktop inputs bind a source and reject stale files before building."""
from __future__ import annotations

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

import pytest


def _project(tmp_path: Path) -> tuple[Path, str]:
    from pm.store import current_target
    source = tmp_path / "source with spaces"
    source.mkdir()
    (source / "pyproject.toml").write_text('[project]\nname="fixture"\nversion="1.2.3"\n', encoding="utf-8")
    (source / "package.json").write_text('{"workspaces": ["apps/desktop"]}', encoding="utf-8")
    (source / "package-lock.json").write_text('{"packages": {}}', encoding="utf-8")
    (source / ".gitignore").write_text(".build/\n", encoding="utf-8")
    (source / "pm").mkdir()
    (source / "pm/lock.json").write_text(json.dumps({"schema": 1, "packages": {
        name: {"version": "1.0.0", "artifacts": {current_target(): {"url": "https://example.test/tool", "sha256": "a" * 64}}}
        for name in ("python", "node", "npm")
    }}), encoding="utf-8")
    subprocess.run(["git", "init", str(source)], check=True, capture_output=True)
    subprocess.run(["git", "add", "."], cwd=source, check=True)
    subprocess.run(["git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test", "commit", "-m", "fixture"], cwd=source, check=True, capture_output=True)
    commit = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=source, text=True).strip()
    return source, commit


def test_stable_build_accepts_the_admitted_commit_before_the_final_tag_exists(tmp_path, monkeypatch):
    from scripts.bundles.desktop_inputs import identity_environment
    from scripts.bundles.desktop_prepare import BuildRequest

    source, commit = _project(tmp_path)
    subprocess.run([
        "git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test",
        "-c", "tag.gpgSign=false", "tag", "-a", "rc.1-v1.2.4", "-m", "claim",
    ], cwd=source, check=True, env={**__import__("os").environ,
                                  "GIT_COMMITTER_DATE": "2026-08-29T01:02:03Z"})
    monkeypatch.setenv("RELEASE_CLAIM_TAG", "rc.1-v1.2.4")
    claim_object = subprocess.check_output(
        ["git", "rev-parse", "rc.1-v1.2.4"], cwd=source, text=True,
    ).strip()
    monkeypatch.setenv("RELEASE_CLAIM_OBJECT", claim_object)
    request = BuildRequest.create(
        source, tag="v1.2.4", commit=None, release_commit=commit, variant="bundled",
        work=tmp_path / "work", cache=tmp_path / "cache", bundle_env={},
    )

    assert request.commit == commit
    assert request.version == "1.2.4"
    # The payload identity stays plain; the attempt ref lives only in the claim env.
    assert request.tag == "v1.2.4"
    assert request.release_epoch == 1787965323
    env = identity_environment(request, "bundled", {"HERMES_RELEASE_EPOCH": "1"})
    assert env["HERMES_RELEASE_EPOCH"] == "1787965323"

    monkeypatch.setenv("RELEASE_CLAIM_OBJECT", "f" * 40)
    with pytest.raises(ValueError, match="exact claim tag object"):
        BuildRequest.create(
            source, tag="v1.2.4", commit=None, release_commit=commit, variant="bundled",
            work=tmp_path / "wrong-work", cache=tmp_path / "wrong-cache", bundle_env={},
        )


def test_native_version_capture_uses_admitted_build_environment(tmp_path, monkeypatch):
    from scripts.bundles.desktop import capture

    monkeypatch.delenv("HERMES_RELEASE_EPOCH", raising=False)
    env = {**os.environ, "HERMES_RELEASE_EPOCH": "1787965323"}
    script = "import os; print(os.environ['HERMES_RELEASE_EPOCH'])"
    assert capture([sys.executable, "-c", script], tmp_path, env) == env["HERMES_RELEASE_EPOCH"]


def test_stable_build_rejects_a_claim_tag_for_another_version(tmp_path, monkeypatch):
    from scripts.bundles.desktop_prepare import BuildRequest

    source, commit = _project(tmp_path)
    subprocess.run([
        "git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test",
        "-c", "tag.gpgSign=false", "tag", "-a", "rc.2-v1.2.5", "-m", "claim",
    ], cwd=source, check=True, env={**__import__("os").environ,
                                  "GIT_COMMITTER_DATE": "2026-08-29T01:02:03Z"})
    monkeypatch.setenv("RELEASE_CLAIM_TAG", "rc.2-v1.2.5")
    monkeypatch.setenv("RELEASE_CLAIM_OBJECT", subprocess.check_output(
        ["git", "rev-parse", "rc.2-v1.2.5"], cwd=source, text=True).strip())
    with pytest.raises(ValueError, match="exact claim tag"):
        BuildRequest.create(
            source, tag="v1.2.4", commit=None, release_commit=commit, variant="bundled",
            work=tmp_path / "work", cache=tmp_path / "cache", bundle_env={},
        )


def test_prepared_input_roundtrip_rejects_mutation_and_foreign_source(tmp_path):
    from scripts.bundles.desktop_prepare import BuildRequest, PreparedDesktop

    source, commit = _project(tmp_path)
    work = tmp_path / "work"
    cache = tmp_path / "cache"
    request = BuildRequest.create(source, tag=None, commit=commit, variant="light", work=work, cache=cache, bundle_env={})
    from pm.lock import Facts
    from pm.store import tree_digest
    from pm.registry import get_package
    store = cache / "tools"
    binaries = {}
    for name in ("python", "node", "npm"):
        package = get_package(name)
        entry = store / package.store_entry("1.0.0", request.target)
        binary = package.binary(entry, request.target)
        assert binary is not None
        binary.parent.mkdir(parents=True)
        binary.write_bytes(b"published tool fixture")
        binaries[name] = binary
        Facts(store / "facts.json").record(name, "1.0.0", entry.name, package.env(entry, request.target), store,
                                           target=request.target, artifacts=["a" * 64], digest=tree_digest(entry))
    work.mkdir()
    executable, node = binaries["python"], binaries["node"]
    icons = work / "icon-environment" / "bin" / "python"
    icons.parent.mkdir(parents=True)
    icons.write_bytes(b"prepared icon interpreter")
    icon_library = work / "icon-environment" / "library"
    icon_library.write_bytes(b"prepared library")
    native = work / "native"
    native.mkdir()
    (native / "binding.node").write_bytes(b"prepared native input")
    packager = work / "packager.json"
    packager.write_text("{}", encoding="utf-8")
    prepared = PreparedDesktop.record(request, python=executable, node=node, icon_python=icons,
                                      native=native, packager=packager, payload=None, native_toolchain="fixture-toolchain")
    path = work / "prepared.json"
    prepared.write(path)
    restored = PreparedDesktop.load(path)
    restored.validate()
    assert restored.request.commit == commit
    assert restored.request.version == "1.2.3"
    assert "environment" not in json.loads(path.read_text())
    icon_library.unlink()
    with pytest.raises(ValueError, match="changed|stale|missing"):
        restored.validate()
    icon_library.write_bytes(b"prepared library")
    (native / "binding.node").write_bytes(b"changed")
    with pytest.raises(ValueError, match="changed|stale"):
        restored.validate()
    (native / "binding.node").write_bytes(b"prepared native input")
    (source / "pyproject.toml").write_text('[project]\nname="fixture"\nversion="9.9.9"\n', encoding="utf-8")
    with pytest.raises(ValueError, match="source|checkout"):
        restored.validate()


def test_prepared_paths_cannot_escape_their_owned_roots(tmp_path):
    from scripts.bundles.desktop_prepare import BuildRequest, PreparedDesktop

    source, commit = _project(tmp_path)
    work, cache = tmp_path / "work", tmp_path / "cache"
    request = BuildRequest.create(source, tag=None, commit=commit, variant="light", work=work, cache=cache, bundle_env={})
    external = tmp_path / "external"
    external.write_bytes(b"not preparation-owned")
    with pytest.raises(ValueError, match="outside|owned"):
        PreparedDesktop.record(request, python=external, node=external, icon_python=external,
                               native=external, packager=external, payload=None, native_toolchain="fixture-toolchain")


def test_prepared_cli_rejects_missing_result_without_provisioning(tmp_path):
    source = Path(__file__).resolve().parents[2]
    absent = tmp_path / "absent" / "prepared.json"
    result = subprocess.run([sys.executable, str(source / "scripts/bundles/desktop.py"), "--prepared", str(absent)],
                            cwd=tmp_path, capture_output=True, text=True, timeout=30)
    assert result.returncode != 0
    assert "preparation" in result.stderr.lower()
    assert "unrecognized arguments" not in result.stderr
    assert not absent.parent.exists()


def test_source_admission_rejects_dirty_checkout_and_invalid_store_before_writes(tmp_path):
    from scripts.bundles.desktop_prepare import BuildRequest

    source, commit = _project(tmp_path)
    work = tmp_path / "work"
    cache = tmp_path / "cache"
    for tag, selected in [(None, commit), ("v1.2.0+canary.20260911T120000Z", None)]:
        with pytest.raises(ValueError, match="Store.*stable"):
            BuildRequest.create(source, tag=tag, commit=selected, variant="store", work=work, cache=cache, bundle_env={})
    (source / "package.json").write_text("{}", encoding="utf-8")
    (source / "pyproject.toml").write_text('staged fixture contents', encoding="utf-8")
    subprocess.run(["git", "add", "pyproject.toml"], cwd=source, check=True)
    (source / "generated output").mkdir()
    (source / "generated output" / "receipt.json").write_text('untracked fixture contents', encoding="utf-8")
    with pytest.raises(ValueError, match="source|checkout") as rejected:
        BuildRequest.create(source, tag=None, commit=commit, variant="light", work=work, cache=cache, bundle_env={})
    status = subprocess.check_output(
        ["git", "status", "--porcelain", "--untracked-files=all"], cwd=source, text=True,
    ).rstrip("\r\n")
    assert str(rejected.value).endswith("\n" + status)
    assert 'generated output/receipt.json' in str(rejected.value)
    assert 'fixture contents' not in str(rejected.value)
    assert not work.exists()
    assert not cache.exists()


def test_preparation_never_reuses_an_unowned_work_directory(tmp_path):
    from scripts.bundles.desktop_prepare import BuildRequest, prepare

    source, commit = _project(tmp_path)
    work = tmp_path / "someone-elses-files"
    work.mkdir()
    precious = work / "request.json"
    precious.write_text("do not replace", encoding="utf-8")
    request = BuildRequest.create(source, tag=None, commit=commit, variant="light",
                                  work=work, cache=tmp_path / "cache", bundle_env={})
    with pytest.raises(ValueError, match="owned"):
        prepare(request)
    assert precious.read_text() == "do not replace"


def test_checkout_lock_excludes_a_second_build_process(tmp_path):
    from scripts.bundles.desktop_inputs import build_lock

    source, _ = _project(tmp_path)
    project = Path(__file__).resolve().parents[2]
    probe = (
        "import sys; from pathlib import Path; sys.path.insert(0,sys.argv[1]); "
        "from scripts.bundles.desktop_inputs import build_lock; "
        "lock=build_lock(Path(sys.argv[2])); lock.__enter__(); print('acquired'); lock.__exit__(None,None,None)"
    )
    command = [sys.executable, "-I", "-c", probe, str(project), str(source)]
    with build_lock(source):
        blocked = subprocess.run(command, capture_output=True, text=True, timeout=30)
        assert blocked.returncode != 0 and "another desktop" in blocked.stderr
    released = subprocess.run(command, capture_output=True, text=True, timeout=30)
    assert released.returncode == 0, released.stderr
    assert released.stdout.strip() == "acquired"


def test_flavored_icon_staging_hands_the_admitted_checkout_back(tmp_path):
    from scripts.bundles.desktop import flavored_assets
    from scripts.bundles.desktop_prepare import require_source

    source, commit = _project(tmp_path)
    assets = source / "apps/desktop/assets"
    (assets / "appx").mkdir(parents=True)
    (assets / "icon.png").write_bytes(b"admitted icon")
    (assets / "appx/Square150x150Logo.png").write_bytes(b"admitted tile")
    subprocess.run(["git", "add", "."], cwd=source, check=True)
    subprocess.run(["git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test",
                    "commit", "-m", "icons"], cwd=source, check=True, capture_output=True)
    commit = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=source, text=True).strip()

    rendered = tmp_path / "products/icons"
    (rendered / "appx").mkdir(parents=True)
    (rendered / "icon.png").write_bytes(b"canary icon")
    (rendered / "icon-mac.png").write_bytes(b"canary dock icon")
    (rendered / "appx/Square150x150Logo.png").write_bytes(b"canary tile")

    # Packaging reads artwork from the workspace path, so the render is live
    # there — and the custody check rightly refuses the checkout in that window.
    with flavored_assets(rendered, assets):
        assert (assets / "icon.png").read_bytes() == b"canary icon"
        assert (assets / "icon-mac.png").read_bytes() == b"canary dock icon"
        with pytest.raises(ValueError, match="clean source checkout"):
            require_source(source, commit)

    # A finished or failed package leaves the admitted files back in place, with
    # render-only additions removed; a dirty tree breaks the next build.
    assert (assets / "icon.png").read_bytes() == b"admitted icon"
    assert (assets / "appx/Square150x150Logo.png").read_bytes() == b"admitted tile"
    assert not (assets / "icon-mac.png").exists()
    require_source(source, commit)

    with pytest.raises(RuntimeError, match="packaging failed"):
        with flavored_assets(rendered, assets):
            raise RuntimeError("packaging failed")
    require_source(source, commit)
