#!/usr/bin/env python3
"""compose-lock.py contract check (flake check `installer-compose-lock`).

Usage: check-compose-lock.py <compose-lock.py>

Exercises the offline lock-composer the installer runs on the target with
no network: given nomarchy's own flake.lock and the baked locked/original
node metadata, it must emit a well-formed downstream lock where

  root ─▶ nomarchy (locked to the baked rev+narHash)
              └─▶ every upstream node, verbatim, one level down

and where `follows` paths (list-valued inputs, absolute from the old root)
are rebased under the new `nomarchy` node. Also pins the fail-closed
guards: a locked node without a narHash, and an upstream lock that already
carries a `nomarchy` node, must both abort.
"""

import json
import subprocess
import sys
import tempfile
from pathlib import Path

tool = sys.argv[1]


def run(src_lock, locked, original):
    """Run compose-lock on a fixture; return (returncode, stderr, out_json|None)."""
    with tempfile.TemporaryDirectory() as d:
        src = Path(d) / "upstream.lock"
        out = Path(d) / "out.lock"
        src.write_text(json.dumps(src_lock))
        p = subprocess.run(
            [sys.executable, tool, str(src), str(out),
             json.dumps(locked), json.dumps(original)],
            capture_output=True, text=True,
        )
        got = json.loads(out.read_text()) if p.returncode == 0 and out.exists() else None
        return p.returncode, p.stderr, got


def check(cond, msg):
    if not cond:
        sys.exit(f"compose-lock contract: {msg}")


# A representative upstream lock: a plain input, and one that `follows`
# it (list-valued input — the case the rebasing logic exists for).
upstream = {
    "nodes": {
        "root": {"inputs": {"nixpkgs": "nixpkgs", "home-manager": "home-manager"}},
        "nixpkgs": {"locked": {"type": "github", "narHash": "sha256-NP"}},
        "home-manager": {
            "inputs": {"nixpkgs": ["nixpkgs"]},
            "locked": {"type": "github", "narHash": "sha256-HM"},
        },
    },
    "root": "root",
    "version": 7,
}
locked = {"type": "path", "path": "/nix/store/x", "narHash": "sha256-SELF", "lastModified": 1}
original = {"type": "git", "url": "https://example.invalid/nomarchy.git", "ref": "v1"}

rc, err, got = run(upstream, locked, original)
check(rc == 0, f"valid inputs were rejected (rc={rc}): {err}")
check(got is not None, "no output lock written on the happy path")

check(got["version"] == 7 and got["root"] == "root", "output is not a v7 lock rooted at 'root'")
check(got["nodes"]["root"] == {"inputs": {"nomarchy": "nomarchy"}},
      f"root node must point only at nomarchy, got {got['nodes']['root']}")

nomarchy = got["nodes"].get("nomarchy")
check(nomarchy is not None, "no 'nomarchy' node in output")
check(nomarchy["locked"] == locked, "nomarchy node lost its baked locked metadata")
check(nomarchy["original"] == original, "nomarchy node lost its baked original metadata")
check(nomarchy["inputs"] == upstream["nodes"]["root"]["inputs"],
      "nomarchy node did not inherit the upstream root's input set")

check(got["nodes"]["nixpkgs"] == upstream["nodes"]["nixpkgs"],
      "a plain upstream node was mutated")
check(got["nodes"]["home-manager"]["inputs"]["nixpkgs"] == ["nomarchy", "nixpkgs"],
      "a follows path was not rebased under the nomarchy node "
      f"(got {got['nodes']['home-manager']['inputs']['nixpkgs']})")

# Fail-closed: no narHash in the baked locked metadata.
rc, err, _ = run(upstream, {"type": "path", "path": "/x"}, original)
check(rc != 0, "a locked node without a narHash was accepted")
check("narHash" in err, f"missing-narHash abort did not mention narHash: {err}")

# Fail-closed: an upstream lock that already names a nomarchy input.
poisoned = json.loads(json.dumps(upstream))
poisoned["nodes"]["nomarchy"] = {"locked": {"narHash": "sha256-X"}}
rc, err, _ = run(poisoned, locked, original)
check(rc != 0, "an upstream lock already carrying a 'nomarchy' node was accepted")

print("compose-lock contract: happy-path shape, follows rebasing, and both "
      "fail-closed guards hold")
