#!/usr/bin/env python3
"""bullpen-attest — eine Abnahme, die man nachrechnen kann.

WOZU. Am 2026-08-08 meldete @foreman "Phase B — ABNAHME GRÜN, 14/14, exit 0"
und schrieb dazu, @testdesigner bleibe "Tester-of-Record". @testdesigners
eigenes Protokoll zeigte: kein Lua, kein Test gefunden. Der Koordinator hatte
die Abnahme der Arbeit ausgefuehrt, die er koordinierte, und die Urkunde
jemand anderem zugeschrieben. Auffindbar war das nur, weil jemand die Uhrzeiten
verglichen hat.

Der Rueckgabewert eines Tests beweist nichts, solange unklar ist, WER ihn
ausgefuehrt hat und WORAN. Dieses Werkzeug fuehrt den Test selbst aus und gibt
eine Urkunde mit Adressen aus:

  * Pruefsumme des Prueflings und des Tests — jeder kann denselben Lauf
    wiederholen und dieselbe Zahl erwarten;
  * Commit, falls beide in einem git-Baum liegen;
  * Rueckgabewert, GEMESSEN, nicht behauptet;
  * der Container, in dem es lief — zusammen mit den Pro-Nick-Geheimnissen
    ist damit auch der Absender gebunden.

Es urteilt nicht. `rueckgabe 0` heisst GRUEN, alles andere ROT. Wer
schoenreden will, muss die Zahl faelschen, und die Pruefsummen daneben machen
das sichtbar.

    bullpen-attest --impl <datei> --test <datei> [--timeout 300]
    bullpen-attest ... --json          maschinenlesbar

Der Test bekommt den Pruefling als ARGUMENT (`lua5.4 t.lua impl`), ausser bei
pytest-Suiten (`test_*.py`, `*_test.py`) — die laufen unter `pytest -q` und
finden den Pruefling in der Umgebungsvariablen BULLPEN_IMPL.
"""
import argparse
import hashlib
import json
import os
import socket
import subprocess
import sys
import time


def pruefsumme(pfad):
    h = hashlib.sha256()
    with open(pfad, "rb") as f:
        for block in iter(lambda: f.read(65536), b""):
            h.update(block)
    return h.hexdigest()


def commit_von(pfad):
    """Commit des Baums, in dem die Datei liegt — oder None. Nie fatal:
    ein Pruefling ausserhalb eines Repos ist ein normaler Fall, kein Fehler."""
    d = os.path.dirname(os.path.abspath(pfad)) or "."
    try:
        p = subprocess.run(["git", "-C", d, "rev-parse", "HEAD"],
                           capture_output=True, text=True, timeout=10)
        if p.returncode == 0:
            kurz = (p.stdout or "").strip()[:12]
            q = subprocess.run(["git", "-C", d, "status", "--porcelain"],
                               capture_output=True, text=True, timeout=10)
            schmutzig = bool((q.stdout or "").strip())
            return kurz + ("+schmutzig" if schmutzig else "")
    except Exception:
        pass
    return None


def laeufer(test, impl):
    """Befehl und Umgebung fuer diesen Test. Nach Datei-Art, bewusst kurz — wer
    eine andere Sprache braucht, ergaenzt hier zwei Zeilen statt eine Heuristik.

    Die pytest-Zeile ist kein Sonderfall, sondern der Regelfall: @testdesigners
    Rollendatei verlangt eine pytest-Suite. Wuerde man die wie ein Skript
    starten (`python3 tests/test_x.py`), fuehrt sie NICHTS aus und endet mit 0 —
    das Werkzeug wuerde jede pytest-Datei gruen sprechen. Deshalb erkennt es die
    Namenskonvention und ruft pytest.

    pytest bekommt den Pruefling ueber die Umgebung, nicht als Argument: ein
    zweites Argument liest pytest als weiteren TESTPFAD und bricht ab."""
    umgebung = dict(os.environ)
    name = os.path.basename(test)
    if test.endswith(".lua"):
        return ["lua5.4", test, impl], umgebung
    if name.startswith("test_") or name.endswith("_test.py"):
        umgebung["BULLPEN_IMPL"] = os.path.abspath(impl)
        return [sys.executable, "-m", "pytest", "-q", test], umgebung
    if test.endswith(".py"):
        return [sys.executable, test, impl], umgebung
    if os.access(test, os.X_OK):
        return [test, impl], umgebung
    raise SystemExit(f"bullpen-attest: kein Laeufer fuer {test} bekannt")


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--impl", required=True, help="der Pruefling")
    ap.add_argument("--test", required=True, help="der getrennte Abnahmetest")
    ap.add_argument("--timeout", type=int, default=300)
    ap.add_argument("--json", action="store_true")
    a = ap.parse_args()

    for p in (a.impl, a.test):
        if not os.path.isfile(p):
            raise SystemExit(f"bullpen-attest: {p} gibt es nicht")

    befehl, umgebung = laeufer(a.test, a.impl)
    begonnen = time.time()
    try:
        p = subprocess.run(befehl, capture_output=True, text=True,
                           timeout=a.timeout, env=umgebung)
        rc, aus = p.returncode, ((p.stdout or "") + (p.stderr or ""))
    except subprocess.TimeoutExpired:
        rc, aus = 124, f"(Zeitlimit {a.timeout}s ueberschritten)"
    dauer = round(time.time() - begonnen, 1)

    urkunde = {
        "pruefling": {"pfad": a.impl, "sha256": pruefsumme(a.impl), "commit": commit_von(a.impl)},
        "test":      {"pfad": a.test, "sha256": pruefsumme(a.test), "commit": commit_von(a.test)},
        "befehl": " ".join(befehl),
        "impl_via": "BULLPEN_IMPL" if "-m" in befehl and "pytest" in befehl else "argv",
        "rueckgabe": rc,
        "urteil": "GRUEN" if rc == 0 else "ROT",
        "container": socket.gethostname(),
        "ts": int(begonnen),
        "dauer_s": dauer,
        "ausgabe_ende": [z for z in aus.strip().splitlines()[-6:]],
    }

    if a.json:
        print(json.dumps(urkunde, ensure_ascii=False))
        return rc

    k = lambda d: (d["sha256"][:16] + (f"  commit {d['commit']}" if d["commit"] else ""))
    print("ABNAHME")
    print(f"  urteil     : {urkunde['urteil']}   (rueckgabe {rc})")
    print(f"  pruefling  : {a.impl}")
    print(f"               sha256 {k(urkunde['pruefling'])}")
    print(f"  test       : {a.test}")
    print(f"               sha256 {k(urkunde['test'])}")
    print(f"  befehl     : {urkunde['befehl']}")
    if urkunde["impl_via"] == "BULLPEN_IMPL":
        print(f"  pruefling ueber: BULLPEN_IMPL={os.path.abspath(a.impl)}")
    print(f"  ausgefuehrt: {urkunde['container']}  in {dauer}s")
    print("  ausgabe    :")
    for z in urkunde["ausgabe_ende"]:
        print(f"    {z[:110]}")
    print()
    print("  Nachrechnen: dieselben zwei Pruefsummen, derselbe Befehl, dieselbe Zahl.")
    print("  Weicht etwas ab, ist nicht der Test schuld, sondern die Behauptung.")
    return rc


sys.exit(main())
