"""test_failure_callback_before_exit_guard_2712.py
task-2712 FAILURE_CALLBACK_BEFORE_EXIT_GUARD 테스트 스위트.

테스터: 하누만 (dev4)
산출물: 14 fixture + 9 completion condition 함수 테스트
모듈 코드 수정 금지 — tests/ 안에서만 작업.
"""

from __future__ import annotations

import json
import os
import sys
import time
from pathlib import Path
from typing import Any, Dict

import pytest

# ── sys.path: scripts/harness/v36 직접 import 경로 추가 ─────────────────────
_HARNESS_DIR = os.path.join(
    os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
    "scripts", "harness", "v36",
)
if _HARNESS_DIR not in sys.path:
    sys.path.insert(0, _HARNESS_DIR)

from terminal_state_classifier import (  # noqa: E402
    ANU_KEY,
    TERMINAL_STATES,
    UNCLASSIFIED_TERMINAL_STATE,
    classify_signal,
    classify_terminal_state,
)
from failure_envelope_writer import (  # noqa: E402
    BYTE_LIMIT,
    MANDATORY_FIELDS,
    build_envelope,
    enforce_byte_limit,
    verify_exactly_one_terminal_marker,
    write_envelope,
)
from failure_callback_dispatcher import (  # noqa: E402
    CollectorViolation,
    _validate_collector_strict,
    detect_bypass_via_count_mismatch,
    write_handoff_marker,
    write_supervisor_crash_marker,
    fallback_chain,
)

# ── fixture JSON 디렉토리 ─────────────────────────────────────────────────────
_FIXTURE_DIR = os.path.join(
    os.path.dirname(os.path.abspath(__file__)),
    "fixtures", "failure_callback_2712",
)
_SCHEMA_PATH = os.path.join(
    os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
    "schemas", "failure_envelope_schema.json",
)


# ─────────────────────────────────────────────────────────────────────────────
# 헬퍼
# ─────────────────────────────────────────────────────────────────────────────

def _load_fixture(fixture_id: str) -> Dict[str, Any]:
    """F-N_*.json 로드."""
    for fname in os.listdir(_FIXTURE_DIR):
        if fname.startswith(fixture_id + "_") or fname.startswith(fixture_id + "_"):
            path = os.path.join(_FIXTURE_DIR, fname)
            with open(path, encoding="utf-8") as f:
                return json.load(f)
    # 정확한 prefix 매칭 fallback
    for fname in os.listdir(_FIXTURE_DIR):
        if fname.startswith(fixture_id):
            path = os.path.join(_FIXTURE_DIR, fname)
            with open(path, encoding="utf-8") as f:
                return json.load(f)
    raise FileNotFoundError(f"fixture {fixture_id} not found in {_FIXTURE_DIR}")


def _utf8_bytes(obj: dict) -> int:
    return len(json.dumps(obj, ensure_ascii=False).encode("utf-8"))


def _make_envelope(inp: dict) -> dict:
    """fixture input 에서 build_envelope 호출."""
    return build_envelope(
        inp["task_id"],
        inp["terminal_state"],
        exit_code=inp.get("exit_code", 1),
        failure_kind=inp.get("failure_kind", ""),
        phase=inp.get("phase", ""),
        artifact_paths=inp.get("artifact_paths", []),
        critical7_match=inp.get("critical7_match", False),
        residual_pid=inp.get("residual_pid"),
        summary=inp.get("summary", ""),
    )


# ─────────────────────────────────────────────────────────────────────────────
# §1  14 fixture 루프 (파라미터화)
# ─────────────────────────────────────────────────────────────────────────────

# fixture 별 핵심 시나리오 인라인 파라미터
# (fixture JSON 의 input/expected 를 테스트 파라미터로 직접 이용)
_FIXTURE_IDS = [f"F-{i}" for i in range(1, 15)]


@pytest.mark.parametrize("fixture_id", _FIXTURE_IDS)
def test_fixture_loop(fixture_id: str, tmp_path: Path) -> None:
    """14 fixture 각각: marker suffix 생성 + .done 미생성 + verify_exactly_one + byte ≤ 3900."""
    fix = _load_fixture(fixture_id)
    inp = fix["input"]
    exp = fix["expected"]

    task_id = inp["task_id"]
    marker_type = inp.get("marker_type", "failure_envelope")
    events_dir = str(tmp_path)

    # F-8: self_key_used=True → CollectorViolation 테스트 (marker 쓰기 전)
    if exp.get("expect_collector_violation"):
        bad_env = _make_envelope(inp)
        bad_env["self_key_used"] = True  # 위반 주입
        with pytest.raises(CollectorViolation):
            _validate_collector_strict(bad_env)
        # good envelope 은 정상 통과
        good_env = _make_envelope(inp)
        _validate_collector_strict(good_env)  # no raise
        return

    # F-7: simulate disk write fail → stderr-emit.log fallback
    if inp.get("simulate_write_fail"):
        task_id = inp["task_id"]
        envelope = _make_envelope(inp)
        # 쓰기 불가 디렉토리 생성 (권한 제거)
        ro_dir = str(tmp_path / "ro_events")
        os.makedirs(ro_dir, exist_ok=True)
        os.chmod(ro_dir, 0o555)  # read+exec only (no write)
        try:
            result = write_envelope(envelope, ro_dir, marker_type="failure_envelope")
            # FALLBACK_STDERR or FALLBACK_STDERR_SYSLOG_FAIL 기대
            assert result["status"].startswith("FALLBACK"), (
                f"expected FALLBACK status, got {result['status']}"
            )
        finally:
            os.chmod(ro_dir, 0o755)  # 정리를 위해 복구
        return

    # F-13/F-14: bypass 시나리오 - 별도 검증 (아래 test 함수에서 상세 다룸)
    if exp.get("expect_bypass_violation") and fixture_id in ("F-13", "F-14"):
        # minimal: envelope 박제 후 bypass detect 는 test_detect_bypass_count_mismatch 에서 다룸
        envelope = _make_envelope(inp)
        result = write_envelope(envelope, events_dir, marker_type=marker_type)
        assert result["status"] in ("WRITTEN", "SKIPPED_ALREADY_TERMINAL")
        return

    # F-5: fallback_chain 시나리오
    if inp.get("cron_fn_raises"):
        envelope = _make_envelope(inp)

        def _failing_cron(env):
            raise RuntimeError("simulated cron fail")

        chain_result = fallback_chain(envelope, _failing_cron, events_dir)
        write_result = chain_result["envelope_write"]
        assert write_result["status"] == "WRITTEN"
        # failure_envelope 파일 존재 확인
        fe_path = os.path.join(events_dir, f"{task_id}.failure-envelope.json")
        assert os.path.exists(fe_path), "failure_envelope must exist"
        # .done 없음
        done_path = os.path.join(events_dir, f"{task_id}.done")
        assert not os.path.exists(done_path), ".done must NOT exist"
        # handoff fallback: write_handoff_marker 는 already-terminal skip
        assert chain_result["cron_status"] == "cron_fail"
        return

    # F-6, F-12: supervisor crash
    if marker_type == "supervisor_crash":
        result = write_supervisor_crash_marker(
            task_id,
            exit_code=inp["exit_code"],
            failure_kind=inp.get("failure_kind", "sigkill_or_oom"),
            events_dir=events_dir,
            phase=inp.get("phase", ""),
            signal_source=inp.get("signal_source", ""),
        )
        assert result["status"] == "WRITTEN"
        suffix = exp["marker_suffix"]
        marker_path = os.path.join(events_dir, f"{task_id}{suffix}")
        assert os.path.exists(marker_path), f"supervisor crash marker must exist: {marker_path}"

    # F-2, F-3, F-4, F-10: handoff
    elif marker_type == "failure_handoff":
        result = write_handoff_marker(
            task_id,
            inp["terminal_state"],
            failure_kind=inp.get("failure_kind", ""),
            events_dir=events_dir,
            phase=inp.get("phase", ""),
            exit_code=inp.get("exit_code", 1),
            artifact_paths=inp.get("artifact_paths"),
            residual_pid=inp.get("residual_pid"),
        )
        assert result["status"] == "WRITTEN"
        suffix = exp["marker_suffix"]
        marker_path = os.path.join(events_dir, f"{task_id}{suffix}")
        assert os.path.exists(marker_path), f"handoff marker must exist: {marker_path}"
        # F-10: residual_pid 포함 확인
        if exp.get("residual_pid_present"):
            with open(marker_path, encoding="utf-8") as f:
                written = json.load(f)
            assert written.get("residual_pid") == exp["residual_pid"]

    # F-1, F-9, F-11: failure_envelope
    else:
        envelope = _make_envelope(inp)
        result = write_envelope(envelope, events_dir, marker_type=marker_type)
        assert result["status"] == "WRITTEN"
        suffix = exp["marker_suffix"]
        marker_path = os.path.join(events_dir, f"{task_id}{suffix}")
        assert os.path.exists(marker_path), f"failure envelope must exist: {marker_path}"

    # ── 공통 검증 ──────────────────────────────────────────────────────────
    # .done 미생성 확인 (done_allowed=false)
    if not exp.get("done_allowed", True):
        done_path = os.path.join(events_dir, f"{task_id}.done")
        assert not os.path.exists(done_path), ".done must NOT exist (done_allowed=false)"

    # verify_exactly_one_terminal_marker
    expected_verify_status = exp.get("verify_exactly_one_status")
    if expected_verify_status:
        v = verify_exactly_one_terminal_marker(task_id, events_dir)
        assert v["status"] == expected_verify_status, (
            f"verify_exactly_one status mismatch: got {v['status']}, expected {expected_verify_status}"
        )

    # byte ≤ 3900 확인
    suffix = exp["marker_suffix"]
    marker_path = os.path.join(events_dir, f"{task_id}{suffix}")
    if os.path.exists(marker_path):
        with open(marker_path, encoding="utf-8") as f:
            written = json.load(f)
        assert _utf8_bytes(written) <= BYTE_LIMIT, f"envelope exceeds {BYTE_LIMIT} bytes"
        # collector_role == ANU
        assert written.get("collector_role") == "ANU"
        # self_key_used == False
        assert written.get("self_key_used") is False


# ─────────────────────────────────────────────────────────────────────────────
# §2  개별 completion condition 테스트
# ─────────────────────────────────────────────────────────────────────────────

def test_f1_scope_guard_fail(tmp_path: Path) -> None:
    """F-1: SCOPE_GUARD_FAIL envelope 생성, .done 없음."""
    task_id = "task-2711"
    events_dir = str(tmp_path)

    envelope = build_envelope(
        task_id,
        "SCOPE_GUARD_FAIL",
        exit_code=1,
        failure_kind="scope_violation_count_61",
        phase="scope_guard",
    )
    result = write_envelope(envelope, events_dir, marker_type="failure_envelope")
    assert result["status"] == "WRITTEN"

    marker_path = os.path.join(events_dir, f"{task_id}.failure-envelope.json")
    assert os.path.exists(marker_path), "failure-envelope.json must exist"

    # .done 없음
    done_path = os.path.join(events_dir, f"{task_id}.done")
    assert not os.path.exists(done_path), ".done must NOT exist for SCOPE_GUARD_FAIL"

    # 내용 확인
    with open(marker_path, encoding="utf-8") as f:
        written = json.load(f)
    assert written["terminal_state"] == "SCOPE_GUARD_FAIL"
    assert written["failure_kind"] == "scope_violation_count_61"
    assert written["collector_role"] == "ANU"
    assert written["self_key_used"] is False


def test_byte_limit_3900(tmp_path: Path) -> None:
    """매우 긴 summary/artifact_paths → enforce_byte_limit → UTF-8 ≤3900, mandatory field 보존."""
    long_summary = "x" * 5000
    long_paths = [f"/path/to/artifact_{i}.json" for i in range(200)]

    envelope = build_envelope(
        "task-2712-byte",
        "FAILURE",
        exit_code=1,
        failure_kind="byte_limit_test",
        phase="test",
        artifact_paths=long_paths,
        summary=long_summary,
    )

    # 박제 전 크기 확인 (초과일 것)
    original_bytes = _utf8_bytes(envelope)
    assert original_bytes > BYTE_LIMIT, "test data must exceed byte limit before enforcement"

    enforced = enforce_byte_limit(envelope)
    enforced_bytes = _utf8_bytes(enforced)
    assert enforced_bytes <= BYTE_LIMIT, f"enforced envelope {enforced_bytes} > {BYTE_LIMIT}"

    # mandatory field 보존 확인
    for field in MANDATORY_FIELDS:
        assert field in enforced, f"mandatory field '{field}' missing after enforce_byte_limit"

    # collector strict field 보존
    assert enforced.get("collector_role") == "ANU"
    assert enforced.get("collector_key") == ANU_KEY
    assert enforced.get("owner_key") == ANU_KEY
    assert enforced.get("self_key_used") is False


def test_self_collector_forbidden(tmp_path: Path) -> None:
    """F-8: self_key_used=True envelope → CollectorViolation raise."""
    # self_key_used=True 케이스
    bad_env = build_envelope("task-2712-f8", "CRITICAL_ESCALATION")
    bad_env["self_key_used"] = True  # 위반 주입
    with pytest.raises(CollectorViolation) as exc_info:
        _validate_collector_strict(bad_env)
    assert "self_key_used" in str(exc_info.value)

    # collector_key 가 ANU_KEY 가 아닌 케이스
    bad_env2 = build_envelope("task-2712-f8b", "CRITICAL_ESCALATION")
    bad_env2["collector_key"] = "wrong_key"
    with pytest.raises(CollectorViolation) as exc_info2:
        _validate_collector_strict(bad_env2)
    assert ANU_KEY in str(exc_info2.value)

    # owner_key 가 ANU_KEY 가 아닌 케이스
    bad_env3 = build_envelope("task-2712-f8c", "CRITICAL_ESCALATION")
    bad_env3["owner_key"] = "wrong_owner_key"
    with pytest.raises(CollectorViolation):
        _validate_collector_strict(bad_env3)

    # collector_role 이 ANU 아닌 케이스
    bad_env4 = build_envelope("task-2712-f8d", "CRITICAL_ESCALATION")
    bad_env4["collector_role"] = "SELF"
    with pytest.raises(CollectorViolation) as exc_info4:
        _validate_collector_strict(bad_env4)
    assert "collector_role" in str(exc_info4.value)

    # 정상 envelope → no raise
    good_env = build_envelope("task-2712-f8-ok", "CRITICAL_ESCALATION")
    _validate_collector_strict(good_env)  # must not raise


def test_exactly_one_terminal_marker(tmp_path: Path) -> None:
    """두 terminal marker 동시 생성 시도 → 두번째 write_envelope 는 SKIPPED_ALREADY_TERMINAL;
    verify 가 OK (1개만); .done + failure-envelope 둘 다 만들면 MULTI_FIRE_VIOLATION."""
    events_dir = str(tmp_path)
    task_id = "task-2712-one-marker"

    # 첫번째 write → WRITTEN
    env1 = build_envelope(task_id, "FAILURE", failure_kind="first_write")
    r1 = write_envelope(env1, events_dir, marker_type="failure_envelope")
    assert r1["status"] == "WRITTEN"

    # 두번째 write → SKIPPED_ALREADY_TERMINAL
    env2 = build_envelope(task_id, "INFRA_DEFECT", failure_kind="second_write")
    r2 = write_envelope(env2, events_dir, marker_type="failure_handoff")
    assert r2["status"] == "SKIPPED_ALREADY_TERMINAL"

    # verify → OK (1개만)
    v = verify_exactly_one_terminal_marker(task_id, events_dir)
    assert v["status"] == "OK"

    # ── MULTI_FIRE_VIOLATION: .done + failure-envelope 강제 동시 생성 ─────
    mf_task = "task-2712-multi-fire"
    mf_dir = str(tmp_path / "multi_fire")
    os.makedirs(mf_dir, exist_ok=True)

    # failure-envelope 강제 생성
    fe_path = os.path.join(mf_dir, f"{mf_task}.failure-envelope.json")
    with open(fe_path, "w", encoding="utf-8") as f:
        json.dump({"task_id": mf_task, "terminal_state": "FAILURE"}, f)

    # .done 강제 생성
    done_path = os.path.join(mf_dir, f"{mf_task}.done")
    with open(done_path, "w", encoding="utf-8") as f:
        f.write("")

    v_multi = verify_exactly_one_terminal_marker(mf_task, mf_dir)
    assert v_multi["status"] == "MULTI_FIRE_VIOLATION"


def test_dispatch_false_ok_handoff(tmp_path: Path) -> None:
    """F-4: write_handoff_marker(BLOCKED, bot_collision) → handoff marker 생성, .done 없음."""
    task_id = "task-2712-f4"
    events_dir = str(tmp_path)

    result = write_handoff_marker(
        task_id,
        "BLOCKED",
        failure_kind="bot_collision",
        events_dir=events_dir,
        phase="dispatch",
        exit_code=1,
    )
    assert result["status"] == "WRITTEN"

    handoff_path = os.path.join(events_dir, f"{task_id}.failure-handoff-marker.json")
    assert os.path.exists(handoff_path), "handoff marker must exist"

    done_path = os.path.join(events_dir, f"{task_id}.done")
    assert not os.path.exists(done_path), ".done must NOT exist"

    with open(handoff_path, encoding="utf-8") as f:
        written = json.load(f)
    assert written["terminal_state"] == "BLOCKED"
    assert written["failure_kind"] == "bot_collision"
    assert written["collector_role"] == "ANU"
    assert written["self_key_used"] is False


def test_crash_no_exit_code(tmp_path: Path) -> None:
    """F-6: write_supervisor_crash_marker → supervisor-crash-marker.json 생성."""
    task_id = "task-2712-f6"
    events_dir = str(tmp_path)

    result = write_supervisor_crash_marker(
        task_id,
        exit_code=-9,
        failure_kind="sigkill_or_oom",
        events_dir=events_dir,
        phase="runtime",
    )
    assert result["status"] == "WRITTEN"

    crash_path = os.path.join(events_dir, f"{task_id}.supervisor-crash-marker.json")
    assert os.path.exists(crash_path), "supervisor-crash-marker.json must exist"

    done_path = os.path.join(events_dir, f"{task_id}.done")
    assert not os.path.exists(done_path), ".done must NOT exist for CRASH_NO_EXIT_CODE"

    with open(crash_path, encoding="utf-8") as f:
        written = json.load(f)
    assert written["terminal_state"] == "CRASH_NO_EXIT_CODE"
    assert written["exit_code"] == -9
    assert written["collector_role"] == "ANU"
    assert written["self_key_used"] is False


def test_classify_signal_taxonomy() -> None:
    """classify_signal(-9)==SIGKILL, (-15)==SIGTERM, (-2)==SIGINT;
    classify_terminal_state 동작 검증."""
    assert classify_signal(-9) == "SIGKILL"
    assert classify_signal(-15) == "SIGTERM"
    assert classify_signal(-2) == "SIGINT"
    # 비-signal
    assert classify_signal(0) is None
    assert classify_signal(1) is None

    # classify_terminal_state
    assert classify_terminal_state(-9) == "CRASH_NO_EXIT_CODE"
    assert classify_terminal_state(-15) == "CRASH_NO_EXIT_CODE"
    assert classify_terminal_state(-2) == "CRASH_NO_EXIT_CODE"
    assert classify_terminal_state(0) == "SUCCESS"
    assert classify_terminal_state(1, "SCOPE_GUARD_FAIL") == "SCOPE_GUARD_FAIL"
    assert classify_terminal_state(1, "QC_FAIL") == "QC_FAIL"
    assert classify_terminal_state(1, "INVALID_HINT") == "FAILURE"  # hint 무효 → FAILURE
    assert classify_terminal_state(1) == "FAILURE"  # hint 없음

    # 모든 10 terminal state
    assert len(TERMINAL_STATES) == 10
    assert UNCLASSIFIED_TERMINAL_STATE == "UNCLASSIFIED_TERMINAL_STATE"


def test_detect_bypass_count_mismatch(tmp_path: Path) -> None:
    """failure-envelope + .done 동시 mtime window → detect_bypass_via_count_mismatch 가 violation 반환."""
    events_dir = str(tmp_path)
    task_id = "task-2712-bypass"

    t_start = time.time() - 1

    # failure-envelope 파일 생성
    fe_path = os.path.join(events_dir, f"{task_id}.failure-envelope.json")
    fe_content = {
        "task_id": task_id,
        "terminal_state": "FAILURE",
        "registration_mode": "failure_callback_before_exit_guard",
    }
    with open(fe_path, "w", encoding="utf-8") as f:
        json.dump(fe_content, f)

    # .done 파일 생성 (동시 존재 → violation)
    done_path = os.path.join(events_dir, f"{task_id}.done")
    with open(done_path, "w", encoding="utf-8") as f:
        f.write("")

    t_end = time.time() + 1

    violations = detect_bypass_via_count_mismatch(t_start, t_end, events_dir)
    assert len(violations) > 0, "violations must be non-empty for concurrent failure+done"

    violation_types = [v["violation"] for v in violations]
    # failure + done concurrent 위반
    assert any(
        "FAILURE_AND_DONE_CONCURRENT" in vt or "MULTI_CLASS" in vt
        for vt in violation_types
    ), f"expected FAILURE_AND_DONE_CONCURRENT or MULTI_CLASS violation, got {violation_types}"


def test_schema_11_mandatory_fields() -> None:
    """schemas/failure_envelope_schema.json 로드 → required 에 11 mandatory + 4 collector field 포함 확인."""
    assert os.path.exists(_SCHEMA_PATH), f"schema file not found: {_SCHEMA_PATH}"

    with open(_SCHEMA_PATH, encoding="utf-8") as f:
        schema = json.load(f)

    required = schema.get("required", [])

    # 11 mandatory field
    for field in MANDATORY_FIELDS:
        assert field in required, f"mandatory field '{field}' missing from schema required"

    # 4 collector strict field
    collector_fields = ["collector_role", "collector_key", "owner_key", "self_key_used"]
    for field in collector_fields:
        assert field in required, f"collector field '{field}' missing from schema required"

    # 총합: 11 + 4 = 15 이상 (task_id 는 mandatory 에도 있으므로 중복 제외)
    mandatory_set = set(MANDATORY_FIELDS)
    collector_set = set(collector_fields)
    all_required = mandatory_set | collector_set
    for field in all_required:
        assert field in required, f"field '{field}' missing from schema required"
