# tests/test_prevent_registry.py
"""registry.py — discover domains/*/detectors/*/detector.json and select which run for a (file, trigger).
Selection rule: per-file detector runs iff (file matches a scope_glob) AND (trigger ∈ triggers); a repo
detector runs iff (any changed file matches a trigger_glob) AND (trigger ∈ triggers)."""
import importlib.util, json, os

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

def _load(name, path):
    spec = importlib.util.spec_from_file_location(name, path)
    mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod); return mod

registry = _load("sg_registry", os.path.join(ROOT, "prevent", "registry.py"))

def test_load_finds_the_three_v1_detectors():
    dets, skipped = registry.load(ROOT)
    ids = {d["id"] for d in dets}
    assert {"oracle", "deps", "headers"} <= ids, f"missing detectors; got {ids}"
    assert skipped == [], f"no malformed manifest expected in a clean tree; got {skipped}"

def test_glob_match_handles_double_star_and_top_level():
    assert registry.glob_match("apps/web/src/x.ts", "**/*.ts")
    assert registry.glob_match("x.ts", "**/*.ts")          # top-level file must match
    assert not registry.glob_match("x.md", "**/*.ts")
    assert registry.glob_match("pnpm-lock.yaml", "**/pnpm-lock.yaml")

def test_per_file_selects_oracle_on_ts_precommit_only():
    dets, _ = registry.load(ROOT)
    on_ts = {d["id"] for d in registry.applicable_per_file(dets, "a/b.ts", "pre-commit")}
    assert "oracle" in on_ts                                 # .ts at pre-commit → oracle
    assert "deps" not in on_ts                               # deps is repo-scope, not per-file
    none_md = registry.applicable_per_file(dets, "README.md", "pre-commit")
    assert none_md == []                                     # .md → no per-file detector
    # the cross-file oracle declares pre-commit ONLY — never selected at pre-edit (unsound there)
    on_edit = {d["id"] for d in registry.applicable_per_file(dets, "a/b.ts", "pre-edit")}
    assert "oracle" not in on_edit

def test_repo_selects_deps_on_lockfile_change():
    dets, _ = registry.load(ROOT)
    on_lock = {d["id"] for d in registry.applicable_repo(dets, ["pnpm-lock.yaml"], "pre-commit")}
    assert "deps" in on_lock
    on_ts = {d["id"] for d in registry.applicable_repo(dets, ["a/b.ts"], "pre-commit")}
    assert "deps" not in on_ts                               # no lockfile staged → deps not triggered

def test_load_surfaces_malformed_manifest_as_skipped(tmp_path):
    # a manifest that fails to parse must come back in `skipped` (reduced coverage), never silently dropped
    d = tmp_path / "domains" / "x" / "detectors" / "broken"
    d.mkdir(parents=True)
    (d / "detector.json").write_text("{ this is not json", encoding="utf-8")
    dets, skipped = registry.load(str(tmp_path))
    assert dets == []
    assert len(skipped) == 1 and skipped[0][0].endswith("detector.json"), f"malformed not surfaced; got {skipped}"
