"""ASME 절(clause) 타이핑 + 라벨 정제 단위테스트 (A-1 / C-4, presegment-multigranularity).

핵심 불변식:
  - (A-1) ATX heading 의 제목이 ASME 절 식별자(UG-79·PG-27.4.1·UW-11·A-69 …)면 node_type='clause'.
    builder 가 과거 ATX 를 무조건 node_type=None 으로 반환해 ASME 절이 'clause' 로 안 잡히던 것을 고침.
  - (C-4) marker LaTeX/markdown/페이지번호 아티팩트('$\\textbf{PG-20.1 …}', '(25) **A-69**')를 절번호
    매칭 전에 정제 — 정제 없으면 패턴이 노이즈에 막혀 매칭 0.
  - (A-2) 큰 절(>LEAF_HARD_MAX)은 기존 window-split 로직으로 자동 'clause_split' 이 됨
    (char_start 보존 = 단일 점프 타깃). 추가 코드 없이 타이핑만으로 확보.
  - (무회귀) 한국 법령 제N조(_KO_JO 경로)·일반 ATX 헤딩은 영향 없음(정제 inert, 타이핑 None 유지).

pytest + 단독 실행 양쪽 지원:
  PYTHONPATH=. python3 tests/hier_decomp/test_asme_clause.py
"""
from __future__ import annotations

try:  # pytest 경로 (앱 패키지)
    from app.services.hier_decomp.builder import _detect_heading, _clean_label, build_hier_tree
except Exception:  # 단독 실행 (앱 deps 없이 builder.py 직접 로드 — stdlib only)
    import importlib.util
    import pathlib
    import sys

    _bp = pathlib.Path(__file__).resolve().parents[2] / "app/services/hier_decomp/builder.py"
    _spec = importlib.util.spec_from_file_location("_hier_builder_t", _bp)
    _m = importlib.util.module_from_spec(_spec)
    sys.modules[_spec.name] = _m  # dataclass __module__ 해소
    _spec.loader.exec_module(_m)
    _detect_heading, _clean_label, build_hier_tree = _m._detect_heading, _m._clean_label, _m.build_hier_tree


# 5180/5210 실데이터에서 뽑은 noisy 라벨 (marker LaTeX/markdown/페이지번호 범벅).
ASME_NOISY = [
    (r"# $\textbf{PG-20.1 Carbon and Carbon-Molybdenum Tube and} \hspace{0.2cm} \textbf{(25)}$", "PG-20.1"),
    ("# (25) **A-69**", "A-69"),
    ("# (25) PFT-14 GENERAL", "PFT-14"),
    ("## (25) PG-27.4.1", "PG-27.4.1"),
    ("### UG-79 Forming of Pressure Parts", "UG-79"),
    ("# UW-11 Radiographic Examination", "UW-11"),
    ("#### UCS-56 Requirements for Postweld Heat Treatment", "UCS-56"),
]


def test_asme_clause_typed_and_cleaned():
    for line, head in ASME_NOISY:
        r = _detect_heading(line)
        assert r is not None, f"미탐지: {line!r}"
        _lvl, title, nt = r
        assert nt == "clause", f"{line!r} → node_type={nt} (clause 여야)"
        assert title.startswith(head), f"{line!r} → 정제 라벨 {title!r} 가 {head!r} 로 시작 안 함"
        assert "\\textbf" not in title and "$" not in title and "**" not in title, f"라벨에 아티팩트 잔류: {title!r}"


def test_clean_label_strips_artifacts():
    assert _clean_label(r"$\textbf{PG-20.1 Foo} \hspace{0.2cm} \textbf{(25)}$").startswith("PG-20.1 Foo")
    assert _clean_label("(25) **A-69**") == "A-69"
    assert _clean_label("(25) PFT-14 GENERAL") == "PFT-14 GENERAL"


def test_korean_jo_unaffected():
    # 한국 법령 제N조 = _KO_JO 경로(ATX 아님) → clause 유지, _clean_label 미적용·inert.
    r = _detect_heading("제3조(정의) 이 규칙에서 사용하는 용어의 뜻은")
    assert r is not None and r[2] == "clause" and "제3조" in r[1], r
    assert _clean_label("제3조(정의)") == "제3조(정의)"  # 노이즈 없음 → inert(무회귀)


def test_plain_atx_not_clause():
    # ASME 절 식별자가 아닌 일반 ATX 헤딩은 node_type None 유지 + 라벨 무변.
    for line, want in [("# Introduction", "Introduction"), ("## Overview of Methods", "Overview of Methods")]:
        r = _detect_heading(line)
        assert r is not None and r[2] is None and r[1] == want, r


def test_large_clause_becomes_clause_split():
    # A-2: 큰 절(>5000자) → 기존 window-split 이 'clause' 를 'clause_split' 로(char_start 보존=점프 타깃) + window 자식.
    big = "# UG-22 Loadings\n\n" + ("This is a body paragraph describing loadings in detail. " * 30 + "\n\n") * 8
    nodes = build_hier_tree(big)
    splits = [n for n in nodes if n.node_type == "clause_split"]
    assert splits, f"clause_split 없음: {[n.node_type for n in nodes]}"
    assert all(n.char_start is not None for n in splits), "clause_split char_start(점프 타깃) 유실"
    assert any(n.node_type == "window" for n in nodes), "window 자식 없음"


def test_typing_ratio_sample():
    # V-1 스타일: 4 ASME 절 + 1 일반 → clause 4개만.
    md = "\n\n".join(f"# {x}\n\nbody for {x} here.\n"
                     for x in ["UG-1 Scope", "UG-79 Forming", "PG-5 Service", "Introduction", "UW-11 RT"])
    clauses = [n for n in build_hier_tree(md) if n.node_type in ("clause", "clause_split")]
    assert len(clauses) == 4, [n.section_title for n in clauses]


if __name__ == "__main__":
    import sys
    import traceback

    fns = [(k, v) for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
    failed = 0
    for name, fn in fns:
        try:
            fn()
            print(f"  PASS {name}")
        except Exception:
            failed += 1
            print(f"  FAIL {name}")
            traceback.print_exc()
    print(f"\n{len(fns) - failed}/{len(fns)} passed")
    sys.exit(1 if failed else 0)
