#!/usr/bin/env python3
"""List, filter, and optionally run entries from ``testing/deliberate_matrix.json``.

Uses only the stdlib. Substitutes ``{python}`` in harness argv with ``sys.executable``.

Examples (repo root):
  python3 scripts/run_deliberate_matrix.py --list
  python3 scripts/run_deliberate_matrix.py --layer deterministic_tools --automated
  python3 scripts/run_deliberate_matrix.py --suite pre_release --dry-run --run
  python3 scripts/run_deliberate_matrix.py --id http.full_tool_probe.default --run
"""
from __future__ import annotations

import argparse
import json
import os
import subprocess
import sys
from typing import Any

_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
_REPO_ROOT = os.path.dirname(_SCRIPT_DIR)
_MATRIX_PATH = os.path.join(_REPO_ROOT, "testing", "deliberate_matrix.json")


def _load_matrix() -> dict[str, Any]:
    if not os.path.isfile(_MATRIX_PATH):
        raise SystemExit(f"Matrix file not found: {_MATRIX_PATH}")
    with open(_MATRIX_PATH, encoding="utf-8") as f:
        return json.load(f)


def _resolve_argv(argv: list[str]) -> list[str]:
    py = sys.executable
    return [py if x == "{python}" else x for x in argv]


def _merge_env(base: dict[str, str] | None, extra: dict[str, str] | None) -> dict[str, str]:
    m: dict[str, str] = {}
    if base:
        m.update(base)
    if extra:
        m.update(extra)
    return m


def main() -> int:
    p = argparse.ArgumentParser(description="Deliberate test matrix helper.")
    p.add_argument("--list", action="store_true", help="Print id, layer, automated, risk, title.")
    p.add_argument("--json", action="store_true", help="Print filtered scenarios as JSON array.")
    p.add_argument("--layer", type=str, default=None, help="Filter scenarios.layer == value.")
    p.add_argument("--automated", action="store_true", help="Only scenarios with automated true.")
    p.add_argument("--risk", type=str, default=None, help="Filter scenarios.risk == value.")
    p.add_argument("--tag", action="append", default=[], metavar="KEY=VALUE", help="Tag filter (all must match).")
    p.add_argument("--id", action="append", dest="ids", default=[], metavar="ID", help="Scenario id (repeatable).")
    p.add_argument("--suite", type=str, default=None, help="Expand suite name to scenario ids from matrix.")
    p.add_argument("--list-suites", action="store_true", help="Print suite names and optional suite_notes, then exit.")
    p.add_argument("--dry-run", action="store_true", help="With --run, print commands instead of executing.")
    p.add_argument("--run", action="store_true", help="Run automated scenarios (subprocess per scenario).")
    args = p.parse_args()

    data = _load_matrix()
    if args.list_suites:
        suites = data.get("suites") or {}
        notes = data.get("suite_notes") or {}
        for name in sorted(suites):
            extra = f" — {notes[name]}" if name in notes else ""
            print(f"{name} ({len(suites[name])} scenarios){extra}")
        return 0

    harnesses: dict[str, Any] = data.get("harnesses") or {}
    tag_req: dict[str, str] = {}
    for raw in args.tag:
        if "=" not in raw:
            raise SystemExit(f"--tag expects KEY=VALUE, got {raw!r}")
        k, v = raw.split("=", 1)
        tag_req[k.strip()] = v.strip()

    id_set: set[str] | None = None
    if args.suite:
        suites = data.get("suites") or {}
        if args.suite not in suites:
            raise SystemExit(f"Unknown suite {args.suite!r}. Known: {', '.join(sorted(suites))}")
        id_set = set(suites[args.suite])
    if args.ids:
        id_set = set(args.ids) if id_set is None else id_set & set(args.ids)

    scenarios: list[dict[str, Any]] = []
    for s in data.get("scenarios", []):
        if id_set is not None and s.get("id") not in id_set:
            continue
        if args.layer and s.get("layer") != args.layer:
            continue
        if args.automated and not s.get("automated"):
            continue
        if args.risk and s.get("risk") != args.risk:
            continue
        tags = s.get("tags") or {}
        if not isinstance(tags, dict):
            tags = {}
        ok = True
        for k, v in tag_req.items():
            if str(tags.get(k)) != v:
                ok = False
                break
        if ok:
            scenarios.append(s)

    if args.json:
        print(json.dumps(scenarios, indent=2))
        return 0

    if args.list or not args.run:
        for s in scenarios:
            print(
                f"{s.get('id')}\t{s.get('layer')}\t{s.get('automated')}\t{s.get('risk')}\t{s.get('title')}"
            )
        if not args.list and not scenarios:
            print("(no matching scenarios)", file=sys.stderr)
            return 1
        if not args.run:
            return 0

    if not scenarios:
        print("(no scenarios to run)", file=sys.stderr)
        return 2

    # --run
    failed = 0
    for s in scenarios:
        if not s.get("automated"):
            print(f"skip non-automated: {s.get('id')}", file=sys.stderr)
            continue
        hid = s.get("harness")
        if not hid or hid not in harnesses:
            print(f"skip missing harness {hid!r}: {s.get('id')}", file=sys.stderr)
            failed += 1
            continue
        h = harnesses[hid]
        argv = list(h.get("argv") or [])
        extra = s.get("argv_extra")
        if isinstance(extra, list):
            argv = argv + extra
        argv = _resolve_argv(argv)
        env = os.environ.copy()
        env.update(_merge_env(h.get("env"), s.get("env")))
        cwd = h.get("cwd") or _REPO_ROOT
        if isinstance(cwd, str) and cwd == "{repo_root}":
            cwd = _REPO_ROOT
        print(f"\n=== {s.get('id')}: {' '.join(argv)} ===", flush=True)
        if args.dry_run:
            continue
        r = subprocess.run(argv, cwd=cwd, env=env)
        if r.returncode != 0:
            print(f"FAIL exit {r.returncode}: {s.get('id')}", file=sys.stderr)
            failed += 1
    return 1 if failed else 0


if __name__ == "__main__":
    raise SystemExit(main())
