"""공부 암기노트 Phase 1 — 정규화 + 카드 가드 단위 테스트 (W-3 / G-3).

card_normalize / study_memo_card_guards 는 stdlib 만 의존(DB/MLX 없음).
정량 토큰 정규화·dedup·근거(정량=evidence 원문)·누출·배치 dedup 동작(분기)을 검증.
정량 기대값은 hard gate 로 두지 않고 동작만 assert (메모리 규칙).
"""

from __future__ import annotations

import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "app"))

from services.study import card_normalize as cn  # noqa: E402
from services.study.study_memo_card_guards import (  # noqa: E402
    guard_card,
    guard_cards,
)


# ─── 정규화 (G-3) ───

def test_normalize_num_unit_space_removed():
    assert cn.normalize_token("0.5 MPa") == "0.5MPa"
    assert cn.normalize_token("100 ℃") == "100℃"


def test_normalize_thousands_separator_removed():
    assert cn.normalize_token("1,000kg") == "1000kg"
    assert cn.normalize_token("5,000 kPa") == "5000kPa"


def test_normalize_no_unit_conversion():
    # 단위 환산 절대 금지 — 원문 표기 보존.
    assert cn.normalize_token("1000mm") == "1000mm"
    assert "m" in cn.normalize_token("1000mm")


def test_normalize_decimal_comma_protected():
    # 천단위가 아닌 소수 콤마(3자리 그룹 아님)는 보존.
    assert cn.normalize_token("3,14") == "3,14"


def test_is_quantitative():
    assert cn.is_quantitative("0.5MPa") is True
    assert cn.is_quantitative("0종 장소") is True  # 숫자 0 포함
    assert cn.is_quantitative("안전간극") is False


def test_dedup_hash_stable_and_scoped():
    # 공백 차이는 정규화로 동일 hash.
    assert cn.compute_dedup_hash(7, "cloze", "0.5 MPa") == cn.compute_dedup_hash(7, "cloze", "0.5MPa")
    # format 다르면 다른 hash.
    assert cn.compute_dedup_hash(7, "cloze", "0.5MPa") != cn.compute_dedup_hash(7, "qa", "0.5MPa")
    # source 다르면 다른 hash.
    assert cn.compute_dedup_hash(7, "qa", "x") != cn.compute_dedup_hash(8, "qa", "x")


def test_leak_detection():
    assert cn.is_cue_leak("정답은 0.5MPa 이다", "0.5 MPa") is True
    assert cn.is_cue_leak("설계압력은 얼마인가", "0.5 MPa") is False
    assert cn.is_cloze_self_leak("설계압력 [____] 즉 0.5 MPa 이다", "0.5MPa") is True
    assert cn.is_cloze_self_leak("설계압력은 [____] 이상이다", "0.5MPa") is False


def test_evidence_match_normalized():
    refs = [{"snippet": "최고압력 0.5 MPa 이상", "source_id": 1, "source_type": "document"}]
    assert len(cn.matching_evidence("0.5MPa", refs)) == 1
    assert cn.matching_evidence("9.9MPa", refs) == []


# ─── 카드 가드 (W-3) ───

EVID = [{"snippet": "내압 방폭구조의 안전간극은 0.5 MPa 기준", "source_id": 1, "source_type": "document"}]
EXPL = "내압 방폭구조는 안전간극을 통해 화염 온도를 낮춘다. 0종 장소는 항상 존재하는 장소다."


def _g(card, evid=EVID, expl=EXPL):
    return guard_card(card, source_question_id=1, ai_explanation=expl, evidence_refs=evid)


def test_guard_valid_qa_via_explanation():
    # 비정량 fact 가 ai_explanation 에 등장 → 통과 (evidence 불필요).
    g = _g({"format": "qa", "cue": "내압 방폭구조의 화염온도를 낮추는 것은?", "fact": "안전간극"})
    assert g is not None and g.format == "qa" and g.dedup_hash


def test_guard_valid_cloze_quant_in_evidence():
    # 정량 토큰이 evidence 원문에 등장 → 통과 + 매칭 evidence 기록.
    g = _g({
        "format": "cloze",
        "cue": "안전간극 기준 압력",
        "fact": "0.5MPa",
        "cloze_text": "안전간극은 [____] 기준이다",
    })
    assert g is not None and g.format == "cloze"
    assert len(g.matched_evidence) == 1


def test_guard_drop_quant_not_in_evidence():
    # 정량 토큰이 evidence 에 없으면 drop (할루시네이션 차단).
    g = _g({"format": "cloze", "cue": "압력", "fact": "9.9MPa", "cloze_text": "압력은 [____]"})
    assert g is None


def test_guard_drop_cue_leak():
    g = _g({"format": "qa", "cue": "안전간극이 정답이다", "fact": "안전간극"})
    assert g is None


def test_guard_drop_cloze_self_leak():
    g = _g({
        "format": "cloze",
        "cue": "압력 기준",
        "fact": "0.5MPa",
        "cloze_text": "기준은 [____] 즉 0.5 MPa 이다",
    })
    assert g is None


def test_guard_drop_invalid_format_or_empty():
    assert _g({"format": "ox", "cue": "a", "fact": "안전간극"}) is None
    assert _g({"format": "qa", "cue": "", "fact": "안전간극"}) is None
    assert _g({"format": "qa", "cue": "a", "fact": ""}) is None


def test_guard_drop_cloze_without_blank():
    g = _g({"format": "cloze", "cue": "압력", "fact": "0.5MPa", "cloze_text": "빈칸 없는 문장"})
    assert g is None


def test_guard_drop_hallucinated_concept():
    # 비정량이지만 explanation/evidence 어디에도 없으면 drop.
    g = _g({"format": "qa", "cue": "무엇?", "fact": "존재하지않는개념용어XYZ"})
    assert g is None


def test_guard_cards_batch_dedup():
    # 같은 (qid, format, 정답) 2장 → dedup_hash 동일 → 1장만.
    cards = [
        {"format": "qa", "cue": "화염온도를 낮추는 것은?", "fact": "안전간극"},
        {"format": "qa", "cue": "내압 방폭의 핵심 원리는?", "fact": "안전간극"},
    ]
    out = guard_cards(cards, source_question_id=1, ai_explanation=EXPL, evidence_refs=EVID)
    assert len(out) == 1


def test_guard_cards_all_dropped_returns_empty():
    cards = [{"format": "qa", "cue": "x", "fact": "할루시네이션없는근거XYZ"}]
    out = guard_cards(cards, source_question_id=1, ai_explanation=EXPL, evidence_refs=EVID)
    assert out == []


_TESTS = [v for k, v in dict(globals()).items() if k.startswith("test_")]

if __name__ == "__main__":
    for t in _TESTS:
        t()
    print(f"OK ({len(_TESTS)} tests)")
