# -*- coding: utf-8 -*-
"""tests/regression/test_prompt_lv3_cliff_2950.py — task-2950 회귀.

Lv.3+ 4096자 절벽 **본수정** 보호:
  1. 전 팀 × 전 레벨 프롬프트 문자수 < 3900 (운영 상한) · dev8 Lv.3+ ≤ 3700 (목표)
  2. 이관 verbatim: 게이트 지시 / Codex 명령 / Sanitize / 3문서 본문이
     QC-RULES.md 안에 **코드 canonical 과 글자 단위로 동일**하게 존재
  3. 프롬프트에는 본문이 없고 sha256 핀 + §참조만 (재잠식 회귀 방지)
  4. 인지 검증 압축본이 핵심 동사를 verbatim 유지 (doctrine 유실 방지)
  5. trip-wire 5종 인라인 상주
  6. dispatch 가드: 상한 3900 · env 상향 clamp · 비활성화 불가(fail-closed)
  7. sanitize 중복 append 재발 방지 (본문 이관 후에도 dispatch 가 다시 붙이지 않음)
  8. trip-wire 증거 헬퍼: 생성/병합/판정/대조 + 자기증명 신뢰금지 계약

실제 cokacdir 호출 / 네트워크 / 봇 발사 없음.
"""
from __future__ import annotations

import hashlib
import importlib.util as _ilu
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))

# ── dispatch 패키지를 실제 worktree dispatch 로 고정 (2946 테스트와 동일 패턴) ──
_real_init = _ROOT / "dispatch" / "__init__.py"
_cached = sys.modules.get("dispatch")
if _cached is None or (getattr(_cached, "__file__", "") or "") != str(_real_init):
    for _k in [k for k in list(sys.modules) if k == "dispatch" or k.startswith("dispatch.")]:
        del sys.modules[_k]
    _spec = _ilu.spec_from_file_location(
        "dispatch", _real_init, submodule_search_locations=[str(_ROOT / "dispatch")]
    )
    assert _spec is not None and _spec.loader is not None
    _pkg = _ilu.module_from_spec(_spec)
    sys.modules["dispatch"] = _pkg
    _spec.loader.exec_module(_pkg)

import dispatch  # noqa: E402
import prompts.team_prompts as tp  # noqa: E402
from prompts.gate_instructions import format_for_prompt as _gate  # noqa: E402
from utils import trip_wire_evidence as twe  # noqa: E402

_TASK_ID = "task-9999"
_DESC = "테스트 작업 설명" * 3
_LEVELS = ("normal", "critical", "security")
_DEV_TEAMS = [f"dev{i}-team" for i in range(1, 9)]

# 회장 지시 실측 목표: dev8 critical/security ≤ 3700 (3900 가드 대비 여유 200)
_DEV8_LV3_TARGET = 3700


@pytest.fixture(autouse=True)
def _clean_env(monkeypatch):
    monkeypatch.delenv(dispatch.DISPATCH_PROMPT_MAX_CHARS_ENV, raising=False)
    # _get_anu_key() 는 모듈 변수 ANU_KEY 를 읽는다 — 실제 키와 같은 길이의 더미로
    # 패치해 계측 오차를 0 으로 두고 비밀은 노출하지 않는다.
    monkeypatch.setattr(tp, "ANU_KEY", "0123456789abcdef")


def _prompt(team_id: str, level: str = "normal") -> str:
    return tp.build_prompt(team_id, _TASK_ID, _DESC, level)


# ---------------------------------------------------------------------------
# 1. 전 팀 × 전 레벨 문자수
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("team_id", sorted(tp.TEAM_INFO))
@pytest.mark.parametrize("level", _LEVELS)
def test_every_team_every_level_under_guard(team_id, level):
    length = len(_prompt(team_id, level))
    assert length < dispatch.DISPATCH_PROMPT_MAX_CHARS, f"{team_id}/{level}: {length}자"


@pytest.mark.parametrize("level", ("critical", "security"))
def test_dev8_lv3_meets_measured_target(level):
    """★ 본 작업의 실측 목표. dev8 이 가장 큰 팀이라 여기서 절벽이 먼저 터진다."""
    length = len(_prompt("dev8-team", level))
    assert length <= _DEV8_LV3_TARGET, f"dev8/{level}: {length}자 > {_DEV8_LV3_TARGET}"


@pytest.mark.parametrize("team_id", _DEV_TEAMS)
def test_lv3_increment_is_bounded(team_id):
    """Lv.3+ 증분이 다시 비대해지는 것을 막는다(종전 1,241자 → 400자 이하)."""
    increment = len(_prompt(team_id, "critical")) - len(_prompt(team_id, "normal"))
    assert 0 < increment <= 400, f"{team_id}: Lv.3+ 증분 {increment}자"


# ---------------------------------------------------------------------------
# 2. 이관 verbatim (본문 유실 방지)
# ---------------------------------------------------------------------------
def _qc_rules_text() -> str:
    return Path(tp.QC_RULES_PATH).read_text(encoding="utf-8")


@pytest.mark.parametrize("level_int", (2, 3, 4))
def test_gate_instructions_transferred_verbatim(level_int):
    assert _gate(level_int) in _qc_rules_text(), f"Lv.{level_int} 게이트 지시 본문 유실"


def test_codex_and_sanitize_and_three_docs_transferred_verbatim():
    text = _qc_rules_text()
    assert tp.CODEX_GATE_INSTRUCTION_BODY.rstrip("\n") in text
    assert tp.SANITIZE_GATE_BLOCK.strip("\n") in text
    assert tp.THREE_DOCS_DOCTRINE_BODY.strip("\n") in text


def test_canonical_sections_block_is_present_verbatim():
    """코드 canonical 출력 전체가 파일 안에 통째로 존재해야 한다."""
    assert tp.build_qc_rules_transferred_sections() in _qc_rules_text()


def test_section_anchors_exist():
    text = _qc_rules_text()
    for anchor in (
        tp.QC_RULES_GATE_SECTION,
        tp.QC_RULES_LV3_SECTION,
        tp.QC_RULES_SANITIZE_SECTION,
        tp.QC_RULES_THREE_DOCS_SECTION,
    ):
        assert anchor in text, f"{anchor} 절 부재"


def test_sync_script_reports_in_sync():
    """QC-RULES.md 가 코드와 드리프트하면 즉시 실패한다."""
    spec = _ilu.spec_from_file_location(
        "sync_qc_rules_sections", _ROOT / "scripts" / "sync_qc_rules_sections.py"
    )
    assert spec is not None and spec.loader is not None
    mod = _ilu.module_from_spec(spec)
    spec.loader.exec_module(mod)
    current = _qc_rules_text()
    assert mod.apply(current) == current, "sync_qc_rules_sections.py --write 필요"


def test_trip_wire_policy_documented():
    """자기증명 신뢰금지 + ANU clean worktree 재실행 대조 정책이 문서화되어야 한다."""
    text = _qc_rules_text()
    assert "§trip-wire" in text
    assert "clean worktree" in text
    assert "자기증명" in text
    for field in twe.TRIP_WIRE_FIELDS:
        assert field in text, f"result.json 백업 필드 {field} 미문서화"


# ---------------------------------------------------------------------------
# 3. 프롬프트에는 참조만 (재잠식 방지)
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("level", ("critical", "security"))
def test_prompt_has_no_transferred_bodies(level):
    prompt = _prompt("dev1-team", level)
    assert _gate(3) not in prompt, "게이트 본문이 프롬프트에 재잠식"
    assert tp.SANITIZE_GATE_HEADING not in prompt, "sanitize 본문이 프롬프트에 재잠식"
    assert "codex_gate_check.py" not in prompt, "Codex 명령 본문이 프롬프트에 재잠식"
    assert "3 Step Why 자문" not in prompt, "3문서 산문이 프롬프트에 재잠식"


@pytest.mark.parametrize("level", _LEVELS)
def test_prompt_pins_actual_qc_rules_sha(level):
    prompt = _prompt("dev1-team", level)
    actual = hashlib.sha256(Path(tp.QC_RULES_PATH).read_bytes()).hexdigest()[:12]
    assert f"sha256:{actual}" in prompt
    assert tp.QC_RULES_PATH in prompt


def test_sha_helper_is_failsafe(monkeypatch):
    """★ 무손상: 핀 계산 실패가 dispatch 를 막아서는 안 된다."""
    monkeypatch.setattr(tp, "QC_RULES_PATH", "/nonexistent/QC-RULES.md")
    assert tp.qc_rules_sha_short() == tp.QC_RULES_SHA_UNAVAILABLE
    # 프롬프트 생성 자체도 예외 없이 계속되어야 한다
    assert tp.QC_RULES_SHA_UNAVAILABLE in tp._build_verification_section("critical", _TASK_ID)


@pytest.mark.parametrize("level", ("critical", "security"))
def test_lv3_reference_points_to_both_sections(level):
    section = tp._build_verification_section(level, _TASK_ID)
    assert tp.QC_RULES_LV3_SECTION in section
    assert tp.QC_RULES_SANITIZE_SECTION in section
    assert _TASK_ID in section, "task_id 치환 대상이 명시되어야 한다"


def test_normal_level_has_no_lv3_reference():
    section = tp._build_verification_section("normal", _TASK_ID)
    assert tp.QC_RULES_LV3_SECTION not in section
    assert tp.QC_RULES_GATE_SECTION in section  # 게이트 참조는 전 레벨 공통


# ---------------------------------------------------------------------------
# 4. 인지 검증 압축본 — 핵심 동사 verbatim
# ---------------------------------------------------------------------------
_COGNITIVE_VERBS = (
    "단정하지 않는다",
    "확신도를 드러내고",
    "먼저 실행한다",
    "직접 확인한다",
    "교차",
    "좁히는 법",
    "보고에 밝힌다",
)


@pytest.mark.parametrize("verb", _COGNITIVE_VERBS)
@pytest.mark.parametrize("level", _LEVELS)
def test_cognitive_doctrine_verbs_kept_verbatim(verb, level):
    assert verb in tp._build_verification_section(level, _TASK_ID), f"'{verb}' 유실"


def test_cognitive_doctrine_is_compressed():
    """552자 → 약 120자 압축. 재비대 방지 상한 250자."""
    section = tp._build_verification_section("normal", _TASK_ID)
    body = section.split("## 인지 검증", 1)[1]
    assert len(body) <= 250, f"인지 검증 본문 {len(body)}자 — 재비대"


# ---------------------------------------------------------------------------
# 5. trip-wire 5종 인라인
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
    "token",
    ("Critical7=0", "PII net-new=0", "회귀실패=0", "forbidden_paths침범=0", "nonce"),
)
@pytest.mark.parametrize("level", _LEVELS)
def test_trip_wire_tokens_inline(token, level):
    assert token in tp._build_verification_section(level, _TASK_ID)


def test_trip_wire_block_is_small():
    assert len(tp.TRIP_WIRE_BLOCK) <= 200, "trip-wire 블록 재비대"


def test_trip_wire_result_json_backup_is_instructed():
    assert "result.json" in tp.TRIP_WIRE_BLOCK
    assert twe.TRIP_WIRE_RESULT_KEY in tp.TRIP_WIRE_BLOCK


# ---------------------------------------------------------------------------
# 6. dispatch 가드 정책
# ---------------------------------------------------------------------------
def test_operating_limit_is_3900_with_ceiling_4096():
    assert dispatch.DISPATCH_PROMPT_MAX_CHARS == 3900
    assert dispatch.DISPATCH_PROMPT_HARD_CEILING == 4096


@pytest.mark.parametrize("raw", ["5000", "8192", "999999"])
def test_env_raise_is_clamped_to_ceiling(monkeypatch, raw):
    monkeypatch.setenv(dispatch.DISPATCH_PROMPT_MAX_CHARS_ENV, raw)
    assert dispatch._resolve_prompt_max_chars() == dispatch.DISPATCH_PROMPT_HARD_CEILING


@pytest.mark.parametrize("raw", ["0", "-1", "abc", "", "  "])
def test_env_cannot_disable_guard(monkeypatch, raw):
    monkeypatch.setenv(dispatch.DISPATCH_PROMPT_MAX_CHARS_ENV, raw)
    assert dispatch._resolve_prompt_max_chars() == dispatch.DISPATCH_PROMPT_MAX_CHARS


def test_env_lowering_is_allowed(monkeypatch):
    """더 엄격하게 조이는 방향은 허용한다(안전한 방향)."""
    monkeypatch.setenv(dispatch.DISPATCH_PROMPT_MAX_CHARS_ENV, "1000")
    assert dispatch._resolve_prompt_max_chars() == 1000
    assert dispatch.check_prompt_length("x" * 1001, "task-1", "dev1-team") is not None


def test_guard_blocks_just_over_operating_limit():
    err = dispatch.check_prompt_length("x" * 3901, _TASK_ID, "dev8-team")
    assert err is not None and err["prompt_limit"] == 3900


def test_measurement_is_characters_not_bytes():
    korean = "가" * 3800  # 11,400 바이트지만 문자수는 상한 이하
    assert len(korean.encode("utf-8")) > 4096
    assert dispatch.check_prompt_length(korean, _TASK_ID, "dev1-team") is None


def test_callback_byte_guard_untouched():
    """콜백 바이트 가드는 별개 계층 — 중복 추가/변경 금지 (회장 지시)."""
    from dispatch import normal_fallback_callback_helper as nf

    assert nf.CALLBACK_PROMPT_MAX_BYTES == 3900


# ---------------------------------------------------------------------------
# 7. sanitize 중복 append 재발 방지
# ---------------------------------------------------------------------------
def test_detector_accepts_section_reference():
    assert dispatch._prompt_has_sanitize_instruction(f"...{tp.QC_RULES_SANITIZE_SECTION}...")


def test_detector_accepts_legacy_heading():
    assert dispatch._prompt_has_sanitize_instruction(tp.SANITIZE_GATE_BLOCK)


def test_detector_rejects_absent_instruction():
    assert not dispatch._prompt_has_sanitize_instruction("지시 없음")
    assert not dispatch._prompt_has_sanitize_instruction("")
    assert not dispatch._prompt_has_sanitize_instruction(None)  # type: ignore[arg-type]


def test_coding_lv3_prompt_would_not_get_duplicate_block():
    """★ 핵심 회귀: 본문 이관 후 dispatch 가 240자를 도로 붙이면 절벽이 재발한다."""
    assert dispatch._prompt_has_sanitize_instruction(_prompt("dev8-team", "critical"))


def test_research_task_still_receives_block():
    """검증 섹션이 없는 경로(research)에는 종전대로 블록이 삽입되어야 한다."""
    prompt = tp.build_prompt("dev1-team", _TASK_ID, _DESC, "critical", task_type="research")
    assert not dispatch._prompt_has_sanitize_instruction(prompt)


# ---------------------------------------------------------------------------
# 8. trip-wire 증거 헬퍼
# ---------------------------------------------------------------------------
def test_build_evidence_keeps_unmeasured_as_none():
    ev = twe.build_trip_wire_evidence(task_id=_TASK_ID)
    assert ev["nonce"] == _TASK_ID
    for field in twe.TRIP_WIRE_COUNTER_FIELDS:
        assert ev[field] is None, "미측정을 0 으로 승격하면 안 된다"


def test_verify_passes_only_when_all_zero_and_nonce_matches():
    ev = twe.build_trip_wire_evidence(_TASK_ID, 0, 0, 0, 0)
    result = twe.verify_trip_wire_evidence(ev, _TASK_ID)
    assert result["passed"] is True and result["violations"] == []


def test_verify_flags_unmeasured_as_violation():
    ev = twe.build_trip_wire_evidence(_TASK_ID, 0, None, 0, 0)
    result = twe.verify_trip_wire_evidence(ev, _TASK_ID)
    assert result["passed"] is False
    assert any("pii_net_new" in v for v in result["violations"])


def test_verify_flags_nonzero_counter_and_nonce_mismatch():
    ev = twe.build_trip_wire_evidence("task-0001", 1, 0, 0, 0)
    result = twe.verify_trip_wire_evidence(ev, _TASK_ID)
    assert result["passed"] is False
    assert any("critical7" in v for v in result["violations"])
    assert any("nonce" in v for v in result["violations"])


@pytest.mark.parametrize("bad", (None, "x", 3, [], True))
def test_verify_is_failsafe_on_bad_input(bad):
    result = twe.verify_trip_wire_evidence(bad, _TASK_ID)
    assert result["passed"] is False  # 예외 대신 위반 처리


def test_attach_does_not_mutate_original():
    original = {"status": "ok"}
    ev = twe.build_trip_wire_evidence(_TASK_ID, 0, 0, 0, 0)
    merged = twe.attach_trip_wire_evidence(original, ev)
    assert twe.TRIP_WIRE_RESULT_KEY not in original
    assert merged[twe.TRIP_WIRE_RESULT_KEY] == ev
    assert merged["status"] == "ok"
    assert twe.extract_trip_wire_evidence(merged) == ev


def test_compare_adopts_observed_on_mismatch():
    """★ 자기증명 신뢰금지: 불일치 시 ANU 재실행 관측치를 채택한다."""
    claimed = twe.build_trip_wire_evidence(_TASK_ID, 0, 0, 0, 0)
    observed = twe.build_trip_wire_evidence(_TASK_ID, 0, 0, 2, 0)
    cmp = twe.compare_trip_wire_evidence(claimed, observed)
    assert cmp["match"] is False
    assert cmp["mismatches"]["regression_failures"] == {"claimed": 0, "observed": 2}
    assert cmp["adopted"]["regression_failures"] == 2


def test_compare_matches_when_identical():
    ev = twe.build_trip_wire_evidence(_TASK_ID, 0, 0, 0, 0)
    cmp = twe.compare_trip_wire_evidence(ev, dict(ev))
    assert cmp["match"] is True and cmp["mismatches"] == {}


def test_extract_is_failsafe():
    assert twe.extract_trip_wire_evidence(None) == {}
    assert twe.extract_trip_wire_evidence({"trip_wire": "bad"}) == {}


def test_prompt_and_helper_field_names_agree():
    """프롬프트 상수와 헬퍼 필드명이 갈라지면 증거 대조가 불가능해진다."""
    assert tuple(tp.TRIP_WIRE_FIELDS) == tuple(twe.TRIP_WIRE_FIELDS)
