#!/usr/bin/env python3
"""One command for everything in this kit.

    formwork/fw                 what you can do
    formwork/fw install         set this project up
    formwork/fw setup           answer a few questions, write what they imply
    formwork/fw check           run every check
    formwork/fw demo            watch every check refuse a broken input
    formwork/fw roles           rebuild the role files after editing one
    formwork/fw record          write down what the kit looks like now
    formwork/fw test            run every test
    formwork/fw sync <target>   bring the kit copy in another project up to this commit

WHY THIS EXISTS
---------------
Before this, you had to remember `formwork/install --runtime claude-code` and
`formwork/check/run` and `formwork/build`. Three paths and a flag, for five
things you do every day.

A Makefile at your project root would have been the obvious answer. It is also
a file name your project may already be using, and overwriting somebody's build
to save them typing is exactly what this kit exists to prevent.

So: one file, inside the kit's own folder, where nothing can collide.

Exit status is whatever the program underneath returned, so this can be used in
a script the same way as the programs it calls.
"""
import os
import re
import subprocess
import sys

HERE = os.path.dirname(os.path.abspath(__file__))

COMMANDS = [
    ("install", "set this project up. Add --runtime <name> to pick your agent"),
    ("setup",   "answer a few questions, and write the files nobody knows to write"),
    ("check",   "run every check on this project"),
    ("demo",    "watch every check refuse a broken input"),
    ("roles",   "rebuild the role files after editing one"),
    ("record",  "write down what the kit looks like now, after you changed it"),
    ("test",    "run every test in the kit"),
    ("sync",    "bring the kit copy in <target> up to this commit. --dry-run first"),
]

TESTS = [
    os.path.join("guard", "test_boundary.py"),
    os.path.join("guard", "test_protection.py"),
    os.path.join("guard", "test_quality_gate.py"),
    os.path.join("check", "test_gate.py"),
    "test_install.py",
    "test_setup.py",
    "test_build.py",
    "test_sync.py",
]


PROJECT = os.path.dirname(HERE)


def run(path, args):
    full = os.path.join(HERE, path)
    if not os.path.isfile(full):
        print("ERROR: %s is missing from this kit" % path, file=sys.stderr)
        return 2
    try:
        # From the project, always. Run from a subfolder without this and
        # `install` wrote a second configuration into that subfolder and
        # called it a success.
        return subprocess.run([full] + args, cwd=PROJECT).returncode
    except OSError as e:
        print("ERROR: could not run %s: %s" % (path, e), file=sys.stderr)
        return 2


# Every suite prints its own total, and they have to be added up. This command
# used to end on "every test passed", so anybody checking the number the pages
# claim had to do the addition by hand or take it on trust.
TOTAL = re.compile(r"(\d+) of \1 behaved as specified|(\d+) passed, 0 failed")


def run_tests():
    failed = []
    total = 0
    for t in TESTS:
        full = os.path.join(HERE, t)
        if not os.path.isfile(full):
            continue
        print("\n%s" % t, flush=True)
        p = subprocess.run([sys.executable, full], cwd=PROJECT,
                           capture_output=True, text=True)
        sys.stdout.write(p.stdout)
        sys.stderr.write(p.stderr)
        if p.returncode != 0:
            failed.append(t)
        for m in TOTAL.finditer(p.stdout):
            total += int(m.group(1) or m.group(2))
    print("")
    if failed:
        print("FAILED: %s" % ", ".join(failed), file=sys.stderr)
        return 1
    print("%d tests, all passed" % total)
    return 0


def usage():
    print("formwork/fw <command>")
    print("")
    for name, what in COMMANDS:
        print("  %-9s %s" % (name, what))
    print("")
    print("Nothing here is hidden. Each one runs a program in this folder that")
    print("you can also run directly.")
    return 0


def main(argv):
    if len(argv) < 2 or argv[1] in ("help", "-h", "--help"):
        return usage()
    cmd, rest = argv[1], argv[2:]
    if cmd == "install":
        return run("install", rest)
    if cmd == "setup":
        return run("setup", rest)
    if cmd == "check":
        return run(os.path.join("check", "run"), rest)
    if cmd == "demo":
        return run(os.path.join("check", "run"), ["--demo-fail"] + rest)
    if cmd == "roles":
        return run("build", rest)
    if cmd == "test":
        return run_tests()
    if cmd == "sync":
        # The program runs from the kit, so a relative target is made absolute here.
        return run("sync", [a if a.startswith("-") else os.path.abspath(a)
                            for a in rest])
    if cmd == "record":
        return run(os.path.join("check", "checks", "kit-integrity"),
                   ["--record", os.path.dirname(HERE)] + rest)
    print("ERROR: no such command: %s" % cmd, file=sys.stderr)
    print("", file=sys.stderr)
    usage()
    return 2


if __name__ == "__main__":
    sys.exit(main(sys.argv))
