"""test_mapper.py — deterministic gate for the attack-surface mapper.
Loads mapper.py by PATH (project convention, mirrors test_s11_band3.py — no package import).
The fixture plants a glob-MISSED entry point so recall MUST be < 1.0 (a tautological test would always pass)."""
import importlib.util, os, sys, tempfile, subprocess, json

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


mapper = _load("sg_mapper", os.path.join(ROOT, "orchestrator", "mapper.py"))


def _tree():
    d = tempfile.mkdtemp()
    os.makedirs(os.path.join(d, "src/server/api"), exist_ok=True)
    os.makedirs(os.path.join(d, "src/lib"), exist_ok=True)
    os.makedirs(os.path.join(d, "node_modules/pkg"), exist_ok=True)
    open(os.path.join(d, "src/server/api/orders.ts"), "w").write("export const x = defineApi({ handler() {} })")
    open(os.path.join(d, "src/lib/hidden.ts"), "w").write("export const y = defineApi({ handler() {} })")  # glob-missed? signal still primary
    open(os.path.join(d, "src/lib/jobs.ts"), "w").write("export default { async queue(batch, env) {} }")  # queue-consumer: no CONVENTIONS row
    open(os.path.join(d, "node_modules/pkg/route.ts"), "w").write("export const z = defineApi({})")  # pruned
    return d


def test_actual_excludes_pruned_trees():
    d = _tree()
    paths = {os.path.relpath(p, d) for p, _kind in mapper.actual_surface(d)}
    assert "node_modules/pkg/route.ts" not in paths  # pruned, never double-counts
    assert "src/server/api/orders.ts" in paths
    assert "src/lib/jobs.ts" in paths               # denominator catches the queue-consumer


def test_enumerate_recall_below_one_when_table_misses():
    d = _tree()
    enumerated = {os.path.relpath(p, d) for p in (e.path for e in mapper.enumerate_surface(d))}
    actual = {os.path.relpath(p, d) for p, _ in mapper.actual_surface(d)}
    # queue-consumer (jobs.ts) is in actual but has NO CONVENTIONS row -> not enumerated -> recall < 1.0
    assert "src/lib/jobs.ts" in actual
    assert "src/lib/jobs.ts" not in enumerated
    assert actual - enumerated  # non-empty miss set => non-tautological


def test_prioritize_orders_critical_first_never_drops():
    es = [mapper.Entry("/r/feed.ts", "http-file-route", "x"),
          mapper.Entry("/r/payout.ts", "http-defn-call", "x"),   # CRITICAL token 'payout'
          mapper.Entry("/r/profile.ts", "http-file-route", "x")]
    ordered = mapper.prioritize(es)
    assert ordered[0].path == "/r/payout.ts"   # critical first
    assert len(ordered) == len(es)             # never excludes — only orders


def test_coverage_map_three_disjoint_buckets():
    enumerated = [mapper.Entry("/r/a.ts", "http-defn-call", "x"), mapper.Entry("/r/b.ts", "http-file-route", "x")]
    actual = [("/r/a.ts", "http-defn-call"), ("/r/b.ts", "http-file-route"), ("/r/q.ts", "queue-consumer")]
    scanned = {"/r/a.ts": "reports/a.md"}  # only a.ts dispatched (budget)
    cmap = mapper.coverage_map(enumerated, actual, scanned)
    assert cmap["enumerated_scanned"] == [{"path": "/r/a.ts", "report": "reports/a.md"}]
    assert cmap["enumerated_budget_dropped"] == ["/r/b.ts"]                       # enumerated, not dispatched
    assert cmap["not_enumerated"] == [{"path": "/r/q.ts", "kind": "queue-consumer"}]  # in actual, no table row
    seen = [x["path"] if isinstance(x, dict) else x for b in cmap.values() for x in b]
    assert len(seen) == len(set(seen))     # disjoint: no path in two buckets


def test_file_routed_recall_surfaces_signal_miss_and_prunes_nested_git():
    """The within-kind filesystem oracle MUST: (a) score 1.0 when the signal catches every route file, (b) drop
    below 1.0 + name the offender when a route file uses an idiom the signal misses, (c) NOT count a route inside a
    nested git worktree (a copy). A test that can't drop below 1.0 would be as vacuous as the cross-kind number."""
    d = tempfile.mkdtemp()
    os.makedirs(os.path.join(d, "src/pages/api"), exist_ok=True)
    os.makedirs(os.path.join(d, ".wt/functions/api"), exist_ok=True)  # a nested worktree copy
    open(os.path.join(d, "src/pages/api/orders.ts"), "w").write("export const GET = () => {}")   # signal HIT
    open(os.path.join(d, "src/pages/api/subscribe.ts"), "w").write("export default async (r) => {}")  # signal MISS
    open(os.path.join(d, ".wt/.git"), "w").write("gitdir: /elsewhere")   # marks .wt as a nested worktree
    open(os.path.join(d, ".wt/functions/api/copy.ts"), "w").write("export const onRequest = () => {}")  # in a COPY
    fr = mapper.file_routed_recall(d)
    hfr = fr["http-file-route"]
    assert hfr["fs_routes"] == 2                         # both files are route-LOCATED
    assert hfr["within_kind_recall"] == 1.0             # LOCATION enumerates BOTH (incl the export-default one)
    assert hfr["retired_signal_recall"] == 0.5         # the retired content regex would have caught only 1/2
    assert hfr["missed_sample"] == []                   # nothing missed by location -> no offender
    assert fr["edge-function"]["fs_routes"] == 0       # the nested-worktree copy is pruned, not counted


def test_edge_function_enumerated_by_location_not_content():
    """Part A: file-routed kinds enumerate by LOCATION (filesystem oracle), not the content signal.
    A CF Pages function with the `export default` idiom (which the retired `onRequest` regex MISSED) IS
    enumerated because it lives under functions/. A file with `onRequest` OUTSIDE functions/ is NOT (the
    content signal is retired as an enumerator -> its false-positives vanish)."""
    d = tempfile.mkdtemp()
    os.makedirs(os.path.join(d, "functions/api"), exist_ok=True)
    os.makedirs(os.path.join(d, "src"), exist_ok=True)
    open(os.path.join(d, "functions/api/x.ts"), "w").write("export default async (r) => new Response('ok')")
    open(os.path.join(d, "src/mw.ts"), "w").write("export const onRequest = () => {}")  # off-location helper
    enumerated = {os.path.relpath(p, d) for p in (e.path for e in mapper.enumerate_surface(d))}
    assert "functions/api/x.ts" in enumerated      # location enumerates it despite export-default idiom
    assert "src/mw.ts" not in enumerated           # onRequest off-location no longer enumerated (FP gone)


def test_git_driven_walk_excludes_gitignored_build_output_keeps_untracked_source():
    """PRIMARY path: in a git work tree the walk goes through `git ls-files`, so `.gitignore` is the single source of
    truth — a gitignored build artifact (the multideal `tmp/`+`.dist-stack/` defect: 28 compiled `.mjs` polluted the
    count) MUST be excluded, while a TRACKED route AND a brand-new UNTRACKED-but-not-ignored route MUST both be
    scanned (a security gate has to see uncommitted work). A non-git tree falls back to os.walk (covered above)."""
    d = tempfile.mkdtemp()
    os.makedirs(os.path.join(d, "src/pages/api"), exist_ok=True)
    os.makedirs(os.path.join(d, "build-out/chunks"), exist_ok=True)
    open(os.path.join(d, ".gitignore"), "w").write("build-out/\n")
    open(os.path.join(d, "src/pages/api/orders.ts"), "w").write("export const GET = () => {}")        # tracked source
    open(os.path.join(d, "src/pages/api/new-route.ts"), "w").write("export const POST = () => {}")    # untracked, not ignored
    open(os.path.join(d, "build-out/chunks/edge.mjs"), "w").write("export const onRequest = () => {}")  # gitignored build output
    subprocess.run(["git", "-C", d, "init", "-q"], check=True)
    subprocess.run(["git", "-C", d, "add", "src/pages/api/orders.ts"], check=True)
    paths = {os.path.relpath(p, d) for p, _k in mapper.actual_surface(d)}
    assert "src/pages/api/orders.ts" in paths        # tracked source kept
    assert "src/pages/api/new-route.ts" in paths     # untracked-but-not-ignored source kept (uncommitted work scanned)
    assert "build-out/chunks/edge.mjs" not in paths  # gitignored build output excluded by git itself


def test_bench_mapper_scores_recall_and_canonical_confirmation():
    if not os.path.isdir("/home/user/Projects/multideal"):
        import pytest; pytest.skip("real tree not checked out")
    r = subprocess.run([sys.executable, os.path.join(ROOT, "bench.py"), "--mapper", "/home/user/Projects/multideal"],
                       capture_output=True, text=True)
    assert r.returncode == 0, r.stderr
    out = json.loads(r.stdout)
    assert 0.0 <= out["kind_coverage_by_volume"] <= 1.0   # cross-kind: 1.0-per-rowed-kind by construction
    assert "by_kind" in out and "not_enumerated_kinds" in out
    assert "within_kind_recall" in out                    # the signal-completeness measure (filesystem oracle)
    assert out["within_kind_recall"]["http-file-route"]["within_kind_recall"] == 1.0  # signal complete for bulk kind
    assert "canonical_confirmation" in out  # secondary; gated value is acceptable
