"""tests/regression/test_callback_runtime_enforcement_2626.py

task-2626 — CALLBACK_RUNTIME_ENFORCEMENT_WIRING regression (회장 §6, 10건).
mock-based · 실 cron 0 · 실 발사 0 · subprocess 0.
"""
from __future__ import annotations

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

import pytest

_ROOT = Path(__file__).resolve().parents[2]
if str(_ROOT) not in sys.path:
    sys.path.insert(0, str(_ROOT))


def _load_real(modname: str, relpath: str):
    existing = sys.modules.get(modname)
    if existing is not None and getattr(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
    mod = importlib.util.module_from_spec(spec)
    sys.modules[modname] = mod
    spec.loader.exec_module(mod)
    return mod


_load_real("dispatch.callback_owner_enforcer", "dispatch/callback_owner_enforcer.py")
_load_real("dispatch.normal_fallback_callback_helper", "dispatch/normal_fallback_callback_helper.py")

from anu_v3.dispatch_callback_contract import (  # noqa: E402
    DISPATCH_CONTRACT_VIOLATION,
    classify_dispatch_contract,
)
from dispatch.callback_owner_enforcer import (  # noqa: E402
    SELF_COLLECTOR_FORBIDDEN,
    COLLECTOR_ROLE_ANU,
    PATH_DISPATCH_PY,
    enforce_callback_owner,
)
from dispatch.normal_fallback_callback_helper import (  # noqa: E402
    CALLBACK_KIND_FALLBACK,
    CALLBACK_KIND_NORMAL,
    LAUNCH_FAIL_CLOSED,
    LAUNCH_PASS,
    STATUS_ANU_OWNED_READY,
    STATUS_CALLBACK_PROMPT_TOO_LARGE,
    STATUS_SELF_KEY_FAIL_CLOSED,
    launch_callback,
)
from utils.completion_callback_fallback_cancel import (  # noqa: E402
    ANU_KEY as CANCEL_ANU_KEY,
    ANU_CHAT_ID,
    CancelClassification,
    RemoverResult,
    cancel_fallback_on_success,
)

ANU_KEY = "c119085addb0f8b7"
EXEC_SELF_KEY = "1e41a2324a3ccdd0"   # dev6 페룬 self key — 발사 금지 대상
CHAT = "6937032012"
ENVELOPE = "task_id=task-2626\nowner_key=c119085addb0f8b7\ncollector_role=ANU"


# ── test_01 ──────────────────────────────────────────────────────────────────

def test_01_anu_key_normal_callback_pass():
    """ANU key + normal kind → LAUNCH_PASS, argv 에 ANU_KEY 포함, contract_fields 확인."""
    dec = launch_callback(
        kind=CALLBACK_KIND_NORMAL,
        task_id="task-2626",
        executor_key=EXEC_SELF_KEY,
        owner_key=ANU_KEY,
        chat_id=CHAT,
        prompt=ENVELOPE,
        at="10m",
        canonical_root="/home/jay/workspace",
    )
    assert dec.verdict == LAUNCH_PASS
    assert dec.status == STATUS_ANU_OWNED_READY
    assert dec.argv is not None and ANU_KEY in dec.argv and EXEC_SELF_KEY not in dec.argv
    assert dec.contract_fields["callback_role"] == "COLLECTOR_ANU"


# ── test_02 ──────────────────────────────────────────────────────────────────

def test_02_executor_self_key_normal_fail_closed():
    """self-key normal → verdict LAUNCH_FAIL_CLOSED, status STATUS_SELF_KEY_FAIL_CLOSED, argv is None."""
    dec = launch_callback(
        kind=CALLBACK_KIND_NORMAL,
        task_id="task-2626",
        executor_key=EXEC_SELF_KEY,
        owner_key=EXEC_SELF_KEY,
        chat_id=CHAT,
        prompt=ENVELOPE,
        at="10m",
        canonical_root="/home/jay/workspace",
    )
    assert dec.verdict == LAUNCH_FAIL_CLOSED
    assert dec.status == STATUS_SELF_KEY_FAIL_CLOSED
    assert dec.argv is None


# ── test_03 ──────────────────────────────────────────────────────────────────

def test_03_anu_key_fallback_pass():
    """ANU key + fallback kind → LAUNCH_PASS, argv 에 ANU_KEY 포함,
    fallback_safety_net_registered is True, fallback_safety_net_role_single_purpose 확인."""
    dec = launch_callback(
        kind=CALLBACK_KIND_FALLBACK,
        task_id="task-2626",
        executor_key=EXEC_SELF_KEY,
        owner_key=ANU_KEY,
        chat_id=CHAT,
        prompt=ENVELOPE,
        at="10m",
        canonical_root="/home/jay/workspace",
    )
    assert dec.verdict == LAUNCH_PASS
    assert dec.argv is not None and ANU_KEY in dec.argv
    assert dec.contract_fields["fallback_safety_net_registered"] is True
    assert (
        dec.contract_fields["fallback_safety_net_role_single_purpose"]
        == "RECOVERY_ONLY_NO_FINAL_REPORT_TRIGGER"
    )


# ── test_04 ──────────────────────────────────────────────────────────────────

def test_04_executor_self_key_fallback_fail_closed():
    """fallback + self-key → LAUNCH_FAIL_CLOSED, STATUS_SELF_KEY_FAIL_CLOSED, argv None."""
    dec = launch_callback(
        kind=CALLBACK_KIND_FALLBACK,
        task_id="task-2626",
        executor_key=EXEC_SELF_KEY,
        owner_key=EXEC_SELF_KEY,
        chat_id=CHAT,
        prompt=ENVELOPE,
        at="10m",
        canonical_root="/home/jay/workspace",
    )
    assert dec.verdict == LAUNCH_FAIL_CLOSED
    assert dec.status == STATUS_SELF_KEY_FAIL_CLOSED
    assert dec.argv is None


# ── test_05 ──────────────────────────────────────────────────────────────────

def test_05_normal_success_fallback_cancel_on_success(tmp_path):
    """durable evidence + dispatch-fired marker → fake remover 로 cancel → CANCELLED."""
    target = "fb-cron-2626"
    result_json = tmp_path / "task-2626.result.json"
    result_json.write_text(json.dumps({"status": "ok"}), encoding="utf-8")
    report = tmp_path / "task-2626.md"
    report.write_text("# report\nok", encoding="utf-8")
    collector_marker = tmp_path / "collector.marker"
    collector_marker.write_text("done", encoding="utf-8")
    marker = tmp_path / "dispatch-fired.json"
    marker.write_text(
        json.dumps({
            "task_id": "task-2626",
            "callback_policy_a": {
                "fallback_callback_cron_id": target,
                "chat_id": ANU_CHAT_ID,
                "anu_key": CANCEL_ANU_KEY,
                "fallback_role": "fallback",
            },
        }),
        encoding="utf-8",
    )
    fb_marker = tmp_path / "fallback_cancelled.json"

    def fake_remover(cron_id, *, dry_run=True):
        assert cron_id == target
        return RemoverResult(status="removed", detail="fake")

    dec = cancel_fallback_on_success(
        task_id="task-2626",
        target_cron_id=target,
        dispatch_fired_marker_path=marker,
        result_json_path=result_json,
        report_path=report,
        collector_result_marker_path=collector_marker,
        fallback_cancelled_marker_path=fb_marker,
        normal_collector_success=True,
        remover=fake_remover,
        dry_run=True,
    )
    assert dec.classification == CancelClassification.CANCELLED
    assert dec.fallback_cancelled is True
    assert fb_marker.exists()


# ── test_06 ──────────────────────────────────────────────────────────────────

def test_06_fallback_fires_after_success_noop_no_ledger_append(tmp_path):
    """cancel lock 선점 시 fallback 뒤늦은 발화 → ALREADY_FIRED, remover 미호출, 마커 미생성."""
    target = "fb-cron-2626"
    result_json = tmp_path / "r.json"
    result_json.write_text(json.dumps({"status": "ok"}), encoding="utf-8")
    report = tmp_path / "r.md"
    report.write_text("ok", encoding="utf-8")
    collector_marker = tmp_path / "c.marker"
    collector_marker.write_text("done", encoding="utf-8")
    marker = tmp_path / "d.json"
    marker.write_text(
        json.dumps({
            "task_id": "task-2626",
            "callback_policy_a": {
                "fallback_callback_cron_id": target,
                "chat_id": ANU_CHAT_ID,
                "anu_key": CANCEL_ANU_KEY,
                "fallback_role": "fallback",
            },
        }),
        encoding="utf-8",
    )
    lock = tmp_path / "cancel.lock"
    lock.write_text("held-by-normal-success", encoding="utf-8")  # 선점됨
    fb_marker = tmp_path / "fb.json"
    removed = []

    def fake_remover(cron_id, *, dry_run=True):
        removed.append(cron_id)
        return RemoverResult(status="removed")

    dec = cancel_fallback_on_success(
        task_id="task-2626",
        target_cron_id=target,
        dispatch_fired_marker_path=marker,
        result_json_path=result_json,
        report_path=report,
        collector_result_marker_path=collector_marker,
        fallback_cancelled_marker_path=fb_marker,
        cancel_lock_path=lock,
        normal_collector_success=True,
        remover=fake_remover,
        dry_run=True,
    )
    assert dec.classification == CancelClassification.ALREADY_FIRED
    assert dec.cron_remove_invoked is False
    assert removed == []          # remover 미호출 = 이중처리 0
    assert not fb_marker.exists()  # ledger append 없음


# ── test_07 ──────────────────────────────────────────────────────────────────

def test_07_result_present_normal_and_fallback_missing_violation():
    """result 존재 + normal·fallback 둘 다 부재 → DISPATCH_CONTRACT_VIOLATION + recovery_required."""
    rec = classify_dispatch_contract(
        task_id="task-2626",
        normal_callback_present=False,
        fallback_present=False,
        result_present=True,
    )
    assert rec.classification == DISPATCH_CONTRACT_VIOLATION
    assert rec.recovery_required is True


# ── test_08 ──────────────────────────────────────────────────────────────────

def test_08_callback_prompt_too_large():
    """3900 bytes 초과 prompt → STATUS_CALLBACK_PROMPT_TOO_LARGE, LAUNCH_FAIL_CLOSED, argv None."""
    dec = launch_callback(
        kind=CALLBACK_KIND_NORMAL,
        task_id="task-2626",
        executor_key=EXEC_SELF_KEY,
        owner_key=ANU_KEY,
        chat_id=CHAT,
        prompt="task_id=" + "x" * 4000,
        at="10m",
        canonical_root="/home/jay/workspace",
        require_envelope=False,
    )
    assert dec.status == STATUS_CALLBACK_PROMPT_TOO_LARGE
    assert dec.verdict == LAUNCH_FAIL_CLOSED and dec.argv is None
    assert dec.contract_fields["callback_prompt_utf8_bytes"] > 3900


# ── test_09 ──────────────────────────────────────────────────────────────────

def test_09_canonical_root_missing_corrected_or_fail_closed():
    """canonical_root 빈값 → 교정(canonical_root_corrected=True, verdict PASS).
    canonical_root='/tmp/evil' → LAUNCH_FAIL_CLOSED, canonical root invalid."""
    # 빈값 → 자동 교정
    dec_corrected = launch_callback(
        kind=CALLBACK_KIND_NORMAL,
        task_id="task-2626",
        executor_key=EXEC_SELF_KEY,
        owner_key=ANU_KEY,
        chat_id=CHAT,
        prompt=ENVELOPE,
        at="10m",
        canonical_root="",
    )
    assert dec_corrected.canonical_root == "/home/jay/workspace"
    assert dec_corrected.canonical_root_corrected is True
    assert dec_corrected.verdict == LAUNCH_PASS

    # 잘못된 경로 → fail-closed
    dec_evil = launch_callback(
        kind=CALLBACK_KIND_NORMAL,
        task_id="task-2626",
        executor_key=EXEC_SELF_KEY,
        owner_key=ANU_KEY,
        chat_id=CHAT,
        prompt=ENVELOPE,
        at="10m",
        canonical_root="/tmp/evil",
    )
    assert dec_evil.verdict == LAUNCH_FAIL_CLOSED
    # STATUS_CANONICAL_ROOT_INVALID 계열 확인
    assert "CANONICAL_ROOT" in dec_evil.status or "invalid" in dec_evil.status.lower()
    assert dec_evil.argv is None


# ── test_10 ──────────────────────────────────────────────────────────────────

def test_10_self_collector_attempt_forbidden():
    """enforce_callback_owner self-collector → SELF_COLLECTOR_FORBIDDEN.
    launch_callback self-key → LAUNCH_FAIL_CLOSED, argv None."""
    enf = enforce_callback_owner(
        task_id="task-2626",
        executor_key=EXEC_SELF_KEY,
        collector_key=EXEC_SELF_KEY,
        collector_owner_key=EXEC_SELF_KEY,
        collector_role=COLLECTOR_ROLE_ANU,
        normal_collector_cron_id="NC",
        fallback_callback_cron_id="FB",
        dispatch_cron_id="D",
        chat_id=CHAT,
        prompt_claims_anu_collector=True,
        entry_path=PATH_DISPATCH_PY,
    )
    assert SELF_COLLECTOR_FORBIDDEN in enf.classifications

    dec = launch_callback(
        kind=CALLBACK_KIND_NORMAL,
        task_id="task-2626",
        executor_key=EXEC_SELF_KEY,
        owner_key=EXEC_SELF_KEY,
        chat_id=CHAT,
        prompt=ENVELOPE,
        at="10m",
        canonical_root="/home/jay/workspace",
    )
    assert dec.verdict == LAUNCH_FAIL_CLOSED and dec.argv is None
