"""
test_oracle_xfile.py — the #44 cross-file CATCH proof for the founding oracle class (S9 / C02 self-deal).

Closes the band2-delivery-shipped.md gap: post-#42 run_oracle_set runs the oracle PER in-scope file, but no
cell proved the oracle FIRES on a money-move sink BUILT INSIDE an imported helper, reached via cross-file
resolution — "reach restored, not catch measured". This is that measurement, deterministic (no LLM):

  RED  : gate scans cells/caller_vuln.ts (NO sink itself) → collect_deps resolves ./service_vuln →
         run_oracle_set runs the oracle on the resolved helper → the canonical pair `owner|referee` IS flagged.
  GREEN: gate scans cells/caller_safe.ts → ./service_safe → `owner|referee` NOT flagged (buyer self-deal mediated).
  LOAD-BEARING: the oracle on caller_vuln.ts ALONE (no resolution) is SILENT — proving cross-file resolution,
         not the caller, is what surfaces the catch (without it the C02 is a silent false-clean).

Reconstructed from multideal service.ts @ c2d8b4195 (RAW git). Cell lives WITH the detector (band-3 convention),
not under corpus/. Skips cleanly where `bun` is absent (the oracle runtime).
"""
import importlib.util, json, os, shutil, subprocess
import pytest

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CELLS = os.path.join(ROOT, "domains", "security", "detectors", "oracle", "cells")
ORACLE = os.path.join(ROOT, "domains", "security", "detectors", "oracle", "oracle2.ts")
RUNTIME = "bun"

pytestmark = pytest.mark.skipif(shutil.which(RUNTIME) is None, reason="oracle runtime `bun` not installed")


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


gate = _load("sg_gate", os.path.join(ROOT, "orchestrator", "gate.py"))
resolver = _load("sg_resolver", os.path.join(ROOT, "orchestrator", "resolver.py"))


def _canonical():
    with open(os.path.join(CELLS, "canonical.json")) as f:
        return json.load(f)


def _scan(target):
    """The gate's cross-file oracle path: resolve first-party deps, run the oracle on target + each dep."""
    deps, dropped = resolver.collect_deps(target, [], 1, 60)
    dep_files = [f for f, _s, _i in deps]
    results = dict(gate.run_oracle_set(ORACLE, RUNTIME, [target] + dep_files))
    return dep_files, dropped, results


def _raw_oracle(path):
    """FULL oracle stdout (EXTRACTED + FLAGS). gate.run_oracle deliberately strips to the FLAGS section
    (triage-ready) — but the canonical pair `owner|referee` is SEMANTICALLY AMBIGUOUS across sections:
    in FLAGS it means an unmediated GAP (RED); in an EXTRACTED `[gated:...]` line it means MEDIATED (GREEN).
    The discriminator must read the section it means, so the right-reason checks below scan the raw output."""
    return subprocess.run([RUNTIME, ORACLE, path], capture_output=True, text=True, timeout=120).stdout


def _gated_with(raw, sym):
    return [ln for ln in raw.lower().splitlines() if "[gated:" in ln and sym in ln]


def test_red_caller_resolves_helper_and_oracle_catches_canonical():
    sym = _canonical()["canonical_symbol"].lower()
    target = os.path.join(CELLS, "caller_vuln.ts")
    helper = os.path.join(CELLS, "service_vuln.ts")
    dep_files, dropped, results = _scan(target)
    # collect_deps reached the imported helper that BUILDS the sink
    assert helper in dep_files, f"caller must resolve ./service_vuln; got {dep_files} dropped={dropped}"
    # CATCH: the oracle fires on the resolved helper for the canonical self-deal pair (FLAGS section = gap)
    assert sym in results[helper].lower(), f"oracle must flag {sym!r} on the resolved helper; got {results[helper]!r}"
    # the caller itself carries no sink → oracle silent on it (the sink is cross-file only)
    assert sym not in results[target].lower(), f"caller must be silent on {sym!r}; got {results[target]!r}"
    # right-reason: in the VULN helper the pair is an UNMEDIATED gap — it must NOT appear in a [gated:] line.
    # (Guards the discriminator against a future oracle change that emits the pair both ways.)
    assert not _gated_with(_raw_oracle(helper), sym), \
        f"vuln helper must NOT gate {sym!r} (it IS the unmediated self-deal)"


def test_green_caller_resolves_fixed_helper_and_oracle_silent_on_canonical():
    sym = _canonical()["canonical_symbol"].lower()
    target = os.path.join(CELLS, "caller_safe.ts")
    helper = os.path.join(CELLS, "service_safe.ts")
    dep_files, _dropped, results = _scan(target)
    assert helper in dep_files
    # GREEN: the fixed helper mediates the buyer self-deal → canonical pair NOT flagged anywhere in scope (FLAGS)
    assert all(sym not in out.lower() for out in results.values()), \
        f"fixed helper must not flag {sym!r}; got {results}"
    # right-reason: GREEN is green because the pair is MEDIATED, not because the referee principal vanished.
    # The fixed helper must still EXTRACT owner|referee and mark it [gated:...]. A fix that DELETED the referee
    # principal would also pass the FLAGS check above — for the wrong reason; this assertion rejects that.
    assert _gated_with(_raw_oracle(helper), sym), \
        f"fixed helper must EXTRACT {sym!r} as [gated:] (mediated, not vanished)"


def test_resolution_is_load_bearing_caller_alone_is_silent():
    """Without cross-file resolution the C02 is a silent false-clean: the caller alone has no sink to flag."""
    sym = _canonical()["canonical_symbol"].lower()
    solo = gate.run_oracle(ORACLE, RUNTIME, os.path.join(CELLS, "caller_vuln.ts"))
    assert sym not in solo.lower(), f"caller alone must be oracle-silent on {sym!r}; got {solo!r}"
