"""
test_gate_shadow_runner_2765.py — v2.14 S1/S2 §31 gate shadow report-only wrapper 회귀.

검증: fail-open / no-block / no-secret-log / verdict 기록 / log write 실패 fail-open /
      dispatch behavior unchanged(호출 계약 = 항상 None·raise 0).
회장 승인: ANU_V2_14_PHASE1_GATE_SHADOW_S1_S2_IMPLEMENTATION_APPROVED_ACTIVE_FALSE.
"""
from __future__ import annotations

import importlib.util
import json
import sys
from pathlib import Path

# tests/dispatch shadow 회피 (task-2640 동일 패턴). 실 dispatch 패키지 + 신규 모듈 강제 로드.
_ROOT = Path(__file__).resolve().parents[2]
_TESTS_DIR = str(_ROOT / "tests")
sys.path[:] = [p for p in sys.path if p != _TESTS_DIR]
if str(_ROOT) not in sys.path:
    sys.path.insert(0, str(_ROOT))
for _m in [k for k in list(sys.modules) if k == "dispatch" or k.startswith("dispatch.")]:
    _cached = sys.modules.get(_m)
    _f = getattr(_cached, "__file__", "") or ""
    if "/tests/dispatch" in _f or _f == "":
        del sys.modules[_m]


def _load_real(modname: str, relpath: str):
    existing = sys.modules.get(modname)
    if existing is not None and getattr(existing, "__file__", "") and str(existing.__file__).endswith(relpath):
        return existing
    spec = importlib.util.spec_from_file_location(modname, _ROOT / relpath)
    assert spec is not None and spec.loader is not None, f"spec not found: {relpath}"
    mod = importlib.util.module_from_spec(spec)
    sys.modules[modname] = mod
    spec.loader.exec_module(mod)
    return mod


_load_real("dispatch", "dispatch/__init__.py")
_v213 = _load_real("dispatch.v2_13_gate", "dispatch/v2_13_gate.py")
gsr = _load_real("dispatch.gate_shadow_runner", "dispatch/gate_shadow_runner.py")


def test_returns_none_no_raise(tmp_path):
    # report-only: 항상 None, 예외 0
    assert gsr.shadow_evaluate(
        task_id="t1", level="normal", task_type="coding", log_dir=str(tmp_path)
    ) is None


def test_fail_open_when_classify_raises(tmp_path, monkeypatch):
    # classify_gate 가 raise 해도 호출자에게 전파 0 → None
    def boom(*a, **k):
        raise RuntimeError("intentional")

    monkeypatch.setattr(_v213, "classify_gate", boom)
    assert gsr.shadow_evaluate(task_id="t2", level="normal", log_dir=str(tmp_path)) is None


def test_verdict_logged(tmp_path):
    gsr.shadow_evaluate(task_id="t3", level="normal", task_type="coding", log_dir=str(tmp_path))
    files = list(Path(tmp_path).glob("gate_shadow_*.jsonl"))
    assert files, "shadow log 파일이 생성돼야 함"
    rec = json.loads(files[0].read_text(encoding="utf-8").strip().splitlines()[-1])
    assert "verdict" in rec
    assert rec["mode"] == "report_only_shadow"
    assert rec["active"] is False
    # dispatch 시점 표준 contract 부재 → INVALID_CONTRACT_PROVENANCE 관측(차단 아님)
    assert rec["verdict"] == "INVALID_CONTRACT_PROVENANCE"


def test_no_sensitive_fields_in_log(tmp_path):
    # runtime_state 로 민감값을 줘도 로그엔 최소 메타만, 그 값/키는 기록 0
    gsr.shadow_evaluate(
        task_id="t4", level="normal", task_type="coding",
        runtime_state={"prompt": "RAWPROMPT", "credential": "ghp_X", "env": {"K": "V"}},
        log_dir=str(tmp_path),
    )
    rec = json.loads(Path(list(Path(tmp_path).glob("*.jsonl"))[0]).read_text(encoding="utf-8").strip().splitlines()[-1])
    allowed = {"ts", "source", "task_id", "goal_id", "level", "task_type",
               "verdict", "reason", "touched_axes", "rollback_cost", "contract_source", "mode", "active"}
    assert set(rec.keys()) <= allowed, f"화이트리스트 외 키 유입: {set(rec.keys()) - allowed}"
    blob = json.dumps(rec, ensure_ascii=False)
    assert "RAWPROMPT" not in blob
    assert "ghp_X" not in blob
    assert "credential" not in blob
    assert '"env"' not in blob


def test_log_write_failure_fail_open():
    # 쓰기 불가 경로여도 raise 0 → None
    assert gsr.shadow_evaluate(
        task_id="t5", level="normal", log_dir="/proc/forbidden_nonexistent_xyz_2765"
    ) is None


def test_minimal_meta_only_no_task_desc():
    # 호출 계약상 task_desc/prompt 를 받지 않는다(시그니처에 부재) — 구조적 no-secret 보장
    import inspect
    params = set(inspect.signature(gsr.shadow_evaluate).parameters)
    assert "task_desc" not in params
    assert "prompt" not in params
