"""tests/regression/test_fallback_acceptance_2606_hardening.py

task-2606 (TRACK C) — TASK_2553_PLUS58_MICRO_FIX, additive-only hardening.

Spec: memory/tasks/task-2606.md
(sha256 842758659aa026280c76d27ba6b20e647a239d2e6b0374ebac33f247a8d59037).
Preflight: memory/events/task-2604-multitrack-preflight-decision_260519.json
(TrackC).

회장 §2 / Codex **HIGH** (additive-only — 기존 +58 산출물 byte-0):

  +58 ``test_fallback_acceptance_2553plus58.py::test_15`` mock-guard defect:
  guard 가 ``M.sha256_of(PATH) == _sha(PATH)`` 형태로 *동일 disk 파일을
  양변에서 재해시*하므로, 실 entrypoint 모듈/함수가 stub 으로 치환되어도
  self-hash 가 **trivially 일치**한다. 외부 고정(pinned) 기대 digest 가
  없어 stub-only FAIL 을 강제하지 못한다.

본 파일은 그 결함을 **신규 additive 모듈로만** 막는다 — 기존 +58 test
파일·validator·schema·criteria·fixture 는 **무수정(byte-0)**, read-only
소비만 한다. 닫는 방식:

  1  +58 산출물 5종의 test-local **독립** hashlib digest 가 fixture 의
     **외부 pinned 상수**(memory/fixtures/task-2606.hardening-cases.json)
     와 일치 — 모듈 under test 의 자기 해시 함수가 아니라 외부 anchor 대조.
  2  실 entrypoint ``evaluate_fallback_acceptance`` 의 code-object
     ``co_filename`` 과 ``inspect.getsource`` 소스 해시가 실 SCRIPT_PATH
     에서 옴 (monkeypatch 된 순수 mock 은 co_filename 불일치 → FAIL).
  3  +58 모듈 자체 hasher ``M.sha256_of`` 가 pinned 상수를 반환(정직성) —
     trivial-equality 가 아니라 하드코딩 anchor 와 대조.
  4  MOCK-ONLY FAIL 실증: ``sha256_of`` 를 상수반환으로 위조한 stub 모듈을
     hardening 가드에 넣으면 **AssertionError 로 거부**됨을 직접 보인다
     (+58 test_15 는 이 stub 을 통과시키지만 본 가드는 못 통과).
  5  실 entrypoint 직접 호출 행위 회귀 (문서-only 금지) — criterion (b)
     OPERATIONAL_PASS 재확인.
  6  +58 frozen anchor byte-0 + git HEAD/branch 불변.

모든 테스트 100% offline — network / git mutation / cron / dispatch /
cokacdir / subprocess(write) / 파일 write 0. 진행 트리거 0.
"""
from __future__ import annotations

import hashlib
import importlib.util
import inspect
import json
import subprocess
import sys
import types
from pathlib import Path

import pytest

WORKSPACE = Path(__file__).resolve().parent.parent.parent
SCRIPT_PATH = WORKSPACE / "scripts/validate_fallback_acceptance_2553plus58.py"
TEST58_PATH = WORKSPACE / "tests/regression/test_fallback_acceptance_2553plus58.py"
CRITERIA_PATH = WORKSPACE / "memory/events/fallback_acceptance_criteria.json"
SCHEMA_PATH = WORKSPACE / "schemas/non_blocking_fallback_schema.json"
FIX58_PATH = WORKSPACE / "memory/fixtures/task-2553plus58.cases.json"
HARDEN_FIX_PATH = WORKSPACE / "memory/fixtures/task-2606.hardening-cases.json"

FROZEN_ANCHORS = [
    "memory/events/task-2553.legacy-pending-fallback-inventory_260518.json",
    "memory/events/task-2553+37.fallback-duplicate-callback-ignored_260518.json",
    "memory/events/callback_4tuple_index.jsonl",
]

# +58 산출물 byte-0 — 본 모듈은 어떤 +58 파일도 write 하지 않는다.
PLUS58_FROZEN = {
    "scripts/validate_fallback_acceptance_2553plus58.py": SCRIPT_PATH,
    "tests/regression/test_fallback_acceptance_2553plus58.py": TEST58_PATH,
    "schemas/non_blocking_fallback_schema.json": SCHEMA_PATH,
    "memory/events/fallback_acceptance_criteria.json": CRITERIA_PATH,
    "memory/fixtures/task-2553plus58.cases.json": FIX58_PATH,
}

HARDEN_FIX = json.loads(HARDEN_FIX_PATH.read_text(encoding="utf-8"))
PINNED = HARDEN_FIX["pinned_sha256"]


def _independent_sha(path: Path) -> str:
    """test-local hashlib — intentionally NOT M.sha256_of (anti trivial-eq)."""
    return hashlib.sha256(path.read_bytes()).hexdigest()


def _load_real_plus58_module():
    """Load the REAL +58 validator straight from SCRIPT_PATH (no stub)."""
    spec = importlib.util.spec_from_file_location(
        "validate_fallback_acceptance_2553plus58_for_2606", SCRIPT_PATH
    )
    assert spec and spec.loader
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


M = _load_real_plus58_module()


# ── the hardened guard under test (reused by the mock-only-FAIL proof) ──────
def assert_real_plus58_entrypoint(mod) -> None:
    """Reject anything that is not the genuine +58 entrypoint module.

    Unlike +58 test_15 (which compares ``mod.sha256_of(p) == _sha(p)`` — a
    trivial self-equality that any disk-reading stub passes), this guard
    anchors every check to an **external pinned constant** and to the
    code-object provenance of the entrypoint function. A monkeypatched /
    fabricated module fails here even though it would pass +58 test_15.
    """
    # (a) module file provenance — must be the real artifact on disk.
    mod_file = getattr(mod, "__file__", None)
    assert mod_file is not None, "stub module has no __file__"
    assert Path(mod_file).resolve() == SCRIPT_PATH.resolve(), (
        f"module not loaded from real SCRIPT_PATH: {mod_file}"
    )

    # (b) entrypoint code-object provenance — co_filename is the real file
    #     and the function is defined in this module (not a mock object).
    fn = getattr(mod, "evaluate_fallback_acceptance", None)
    assert callable(fn), "evaluate_fallback_acceptance missing/not callable"
    assert fn.__module__ == mod.__name__, "entrypoint reassigned from elsewhere"
    co_file = Path(fn.__code__.co_filename).resolve()
    assert co_file == SCRIPT_PATH.resolve(), (
        f"entrypoint co_filename is not the real artifact: {co_file}"
    )

    # (c) the live function's source file (per inspect) is the real file —
    #     a pure mock has no real source file behind it.
    src_file = inspect.getsourcefile(fn)
    assert src_file is not None, "entrypoint has no source file (pure mock)"
    assert Path(src_file).resolve() == SCRIPT_PATH.resolve()
    file_sha = _independent_sha(SCRIPT_PATH)

    # (d) the file itself matches the EXTERNAL pinned constant (not a
    #     re-derived self-hash). This is the core fix for the +58 defect.
    assert file_sha == PINNED[
        "scripts/validate_fallback_acceptance_2553plus58.py"
    ], "real +58 script content drifted from task-2606 pinned anchor"

    # (e) the module's OWN hasher must agree with the pinned constant —
    #     proves M.sha256_of is honest against an external anchor, so a
    #     stub returning a fixed string is caught here.
    own = mod.sha256_of(SCRIPT_PATH)
    assert own == PINNED[
        "scripts/validate_fallback_acceptance_2553plus58.py"
    ], "module self-hasher disagrees with external pinned anchor"
    assert own == file_sha, "module self-hasher disagrees with independent hash"


# ── 1  external pinned-digest anchor for all 5 +58 artifacts ────────────────
@pytest.mark.parametrize("rel", sorted(PLUS58_FROZEN))
def test_01_plus58_artifact_matches_external_pinned_digest(rel):
    actual = _independent_sha(PLUS58_FROZEN[rel])
    assert actual == PINNED[rel], (
        f"{rel} drifted from task-2606 pinned anchor "
        f"(actual={actual}, pinned={PINNED[rel]})"
    )


# ── 2  real entrypoint code-object provenance ───────────────────────────────
def test_02_real_entrypoint_code_object_provenance():
    assert_real_plus58_entrypoint(M)


# ── 3  module self-hasher honest vs external anchor (not trivial self-eq) ───
def test_03_module_self_hasher_matches_pinned_not_trivial():
    # +58 test_15 only proved M.sha256_of(p) == _sha(p) (both read disk).
    # Here the RHS is a frozen constant, so a disk-reading stub can't game it
    # unless the real file truly matches the task-2606 anchor.
    assert M.sha256_of(SCRIPT_PATH) == PINNED[
        "scripts/validate_fallback_acceptance_2553plus58.py"
    ]
    assert M.sha256_of(SCHEMA_PATH) == PINNED[
        "schemas/non_blocking_fallback_schema.json"
    ]
    assert M.sha256_of(CRITERIA_PATH) == PINNED[
        "memory/events/fallback_acceptance_criteria.json"
    ]


# ── 4  MOCK-ONLY FAIL: a fabricated stub is rejected by the hardened guard ──
def test_04_mock_only_fail_stub_rejected():
    """Demonstrates the defect is closed.

    This stub is exactly what defeats +58 test_15's self-hash check:
    ``sha256_of`` simply returns whatever it is asked to look honest about.
    +58 test_15 would pass it (self-eq holds); the hardened guard MUST
    reject it.
    """
    _pinned = PINNED["scripts/validate_fallback_acceptance_2553plus58.py"]
    stub = types.ModuleType("fake_plus58")
    stub.__file__ = str(SCRIPT_PATH)  # even spoofing __file__...

    def _fake_sha(path):
        # honest hasher on real path -> equals disk -> passes +58 test_15;
        # constant on anything else -> still trivially "matches".
        try:
            return hashlib.sha256(Path(path).read_bytes()).hexdigest()
        except OSError:
            return _pinned

    def _fake_entrypoint(_observation, **_kw):  # pure mock, no real logic
        return {"verdict": "OPERATIONAL_PASS"}

    setattr(stub, "sha256_of", _fake_sha)
    setattr(stub, "evaluate_fallback_acceptance", _fake_entrypoint)

    with pytest.raises(AssertionError):
        assert_real_plus58_entrypoint(stub)

    # And prove the *defective* +58-style self-check would have passed it,
    # which is precisely why the additive hardening is required.
    assert stub.sha256_of(SCRIPT_PATH) == _independent_sha(SCRIPT_PATH), (
        "stub self-hash trivially equals disk hash — the +58 mock-guard hole"
    )


# ── 5  real entrypoint behavioral regression (no doc-only) ──────────────────
def test_05_real_entrypoint_behavioral_regression_criterion_b():
    fix58 = json.loads(FIX58_PATH.read_text(encoding="utf-8"))
    obs = {
        "task_id": "task-2553+58",
        "fallback_cron_id": "F0683510-FB",
        "fallback_bound": True,
        "normal_callback_durable_success": True,
        "normal_success_unchanged": True,
        "registry_non_blocking_mark": fix58["valid_non_blocking_mark"],
    }
    v = M.evaluate_fallback_acceptance(obs)
    assert v["verdict"] == M.OPERATIONAL_PASS
    assert v["satisfied_criterion"] == ["b"]
    # anti-pattern path still real (regression, not mock).
    v2 = M.evaluate_fallback_acceptance(
        {
            "task_id": "task-2553+50",
            "fallback_cron_id": "C7359B43",
            "fallback_bound": True,
            "normal_callback_durable_success": True,
            "normal_success_unchanged": True,
            "cancel_on_success_applied": False,
            "fallback_fired": True,
            "fallback_handling": "DUPLICATE_CALLBACK_IGNORED",
        }
    )
    assert v2["verdict"] == M.OPERATIONAL_QUALITY_FAIL
    assert v2["reason"] == "DUPLICATE_IGNORED_ONLY_NO_MARK"


# ── 6  +58 frozen anchors byte-0 + git HEAD/branch unchanged ────────────────
def test_06_plus58_and_frozen_anchors_byte0():
    pre_anchor = {a: _independent_sha(WORKSPACE / a) for a in FROZEN_ANCHORS}
    pre_58 = {r: _independent_sha(p) for r, p in PLUS58_FROZEN.items()}
    # exercise real entrypoint + guard (proves read-only).
    assert_real_plus58_entrypoint(M)
    M.evaluate_fallback_acceptance(
        {
            "task_id": "task-2553+58",
            "fallback_cron_id": "F0683510-FB",
            "fallback_bound": True,
            "normal_callback_durable_success": True,
            "normal_success_unchanged": True,
            "cancel_on_success_applied": True,
            "fallback_fired": False,
        }
    )
    post_anchor = {a: _independent_sha(WORKSPACE / a) for a in FROZEN_ANCHORS}
    post_58 = {r: _independent_sha(p) for r, p in PLUS58_FROZEN.items()}
    assert pre_anchor == post_anchor
    assert pre_58 == post_58


def test_07_git_head_branch_unchanged():
    head = subprocess.run(
        ["git", "-C", str(WORKSPACE), "rev-parse", "HEAD"],
        capture_output=True, text=True, check=True,
    ).stdout.strip()
    branch = subprocess.run(
        ["git", "-C", str(WORKSPACE), "branch", "--show-current"],
        capture_output=True, text=True, check=True,
    ).stdout.strip()
    assert head == HARDEN_FIX["git_invariant"]["head"]
    assert branch == HARDEN_FIX["git_invariant"]["branch"]


if __name__ == "__main__":
    sys.exit(pytest.main([__file__, "-q"]))
