"""eid.compose 단위 테스트 — persona→rules→overlay→task 합성 (stdlib only, venv 불필요).

실행: python3 tests/eid/test_compose.py   (또는 pytest tests/eid/test_compose.py)
"""

from __future__ import annotations

import sys
from pathlib import Path

# app/ 를 import 루트로 (repo_root/tests/eid/ → repo_root/app)
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "app"))

from eid.compose import (  # noqa: E402
    SEP,
    SubstrateOverflow,
    _persona,
    compose,
    is_composed_surface,
    rules_present,
)

_TASK = "<<<TASK_SENTINEL>>>"


def test_order_persona_rules_task():
    out = compose("react_ask", _TASK)
    # persona(이드 정체성) · rules(생성 가드, '보수적'=conservative 룰) · task 모두 존재
    assert "이드" in out, "persona 미주입"
    assert "보수적" in out, "rules(생성 서브셋) 미주입"
    assert _TASK in out, "task 미포함"
    # 순서: persona < rules < task
    assert out.index("이드") < out.index("보수적") < out.index(_TASK), "persona→rules→task 순서 위반"


def test_base_surface_has_no_overlay():
    out = compose("study_subject_note", _TASK)
    assert "학습 진단 코치" not in out, "base 표면에 기능 overlay 누출"
    assert "뉴스 큐레이터" not in out


def test_overlay_surface_includes_overlay_between_rules_and_task():
    out = compose("study_diagnosis", _TASK)
    assert "학습 진단 코치" in out, "study overlay 미주입"
    # overlay 는 rules 뒤, task 앞
    assert out.index("보수적") < out.index("학습 진단 코치") < out.index(_TASK)


def test_unknown_surface_falls_back_to_base():
    out = compose("totally_unknown_surface", _TASK)
    assert "이드" in out and _TASK in out  # persona+rules+task 유지
    assert "학습 진단 코치" not in out      # overlay 없음


def test_is_composed_surface():
    assert is_composed_surface("react_ask")
    assert is_composed_surface("study_diagnosis")
    assert not is_composed_surface("classify")          # 기계류 9종 = 미등록
    assert not is_composed_surface("briefing_comparative")  # JSON 기계류 = persona ZERO


def test_persona_quiet_on_unknown_variant():
    assert _persona("bogus_variant") == ""  # quiet fail-open


def test_sep_join_present():
    out = compose("react_ask", _TASK)
    assert SEP in out, "합본 구분자 SEP 누락"


def test_overflow_failloud_never_silent_drop():
    # 아주 작은 budget → non-droppable floor 초과 → SubstrateOverflow(절대 silent drop 안 함)
    raised = False
    try:
        compose("study_diagnosis", _TASK, budget_chars=50)
    except SubstrateOverflow:
        raised = True
    assert raised, "budget 초과인데 silent 통과 — fail-loud 위반"


def test_generous_budget_passes():
    out = compose("react_ask", _TASK, budget_chars=100_000)
    assert _TASK in out  # 넉넉한 예산 = 통과


def test_study_diagnosis_overlay_placeholders_survive_compose():
    # study_diagnosis = study overlay 경로. {weakness_snapshot_block}/{habit_signal_block} 가
    # compose 출력(system)에 리터럴로 남아야 surface 가 .replace 로 실데이터 치환 가능.
    out = compose("study_diagnosis", task="")
    assert "{weakness_snapshot_block}" in out, "약점 placeholder 누락(overlay degrade)"
    assert "{habit_signal_block}" in out, "태도 placeholder 누락"
    filled = out.replace("{weakness_snapshot_block}", "WB").replace("{habit_signal_block}", "HB")
    assert "{weakness_snapshot_block}" not in filled and "WB" in filled and "HB" in filled


def test_eid_chat_surface_registered():
    # eid-chat D-1: 채팅 표면 = 자유-prose(base), persona ON, 기능 overlay 없음 (불변식 #3)
    assert is_composed_surface("eid_chat"), "eid_chat ROUTE_MAP 미등록"
    out = compose("eid_chat", "")
    assert "이드" in out, "persona 미주입"
    assert "보수적" in out, "rules 미주입"
    assert out.index("이드") < out.index("보수적"), "persona→rules 순서 위반"
    assert "학습 진단 코치" not in out, "채팅 base 표면에 기능 overlay 누출"


def test_rules_present_true_then_false():
    # D-6 fail-closed 판정 재료 — vendored rules.md 존재 시 True, 부재 시 False.
    # _rules() 의 degraded 배너 동작(다른 표면)은 본 헬퍼와 무관하게 유지된다.
    import eid.compose as c

    assert rules_present() is True, "vendored rules.md 가 있는데 False"
    orig = c._SUBSTRATE_DIR
    try:
        c._SUBSTRATE_DIR = Path("/nonexistent-substrate-dir-for-test")
        assert c.rules_present() is False, "rules.md 부재인데 True — fail-closed 판정 불가"
    finally:
        c._SUBSTRATE_DIR = orig


def test_rules_present_live_judgment():
    # D-6 게이트 = 살아있는 판정 — lru_cache(_read) 동결 회귀 방지.
    # 같은 경로에서 생성→True, 삭제→False 가 즉시 반영돼야 한다.
    import tempfile

    import eid.compose as c

    orig = c._SUBSTRATE_DIR
    try:
        with tempfile.TemporaryDirectory() as td:
            c._SUBSTRATE_DIR = Path(td)
            rules = Path(td) / "rules.md"
            assert c.rules_present() is False
            rules.write_text("rule", encoding="utf-8")
            assert c.rules_present() is True, "생성이 반영 안 됨 — 캐시 동결"
            rules.unlink()
            assert c.rules_present() is False, "삭제가 반영 안 됨 — 캐시 동결"
    finally:
        c._SUBSTRATE_DIR = orig


def _run():
    fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
    fails = 0
    for fn in fns:
        try:
            fn()
            print(f"  PASS {fn.__name__}")
        except Exception as exc:  # noqa: BLE001
            fails += 1
            print(f"  FAIL {fn.__name__}: {type(exc).__name__}: {exc}")
    print(f"\n{len(fns) - fails}/{len(fns)} passed")
    return 1 if fails else 0


if __name__ == "__main__":
    raise SystemExit(_run())
