"""name_matcher 유틸 단위 테스트."""
from __future__ import annotations

from dataclasses import dataclass

from app.core.name_matcher import build_name_index, lookup_name, normalize_name


class TestNormalizeName:
    def test_empty_or_none(self):
        assert normalize_name("") == ""
        assert normalize_name(None) == ""  # type: ignore[arg-type]
        assert normalize_name(123) == ""  # type: ignore[arg-type]

    def test_strip_whitespace(self):
        assert normalize_name("  민숙  ") == "민숙"
        assert normalize_name("\t수리영\n") == "수리영"

    def test_collapse_internal_whitespace(self):
        assert normalize_name("김  형사") == "김 형사"
        assert normalize_name("이  도  령") == "이 도 령"

    def test_strip_paren_suffix_half_width(self):
        assert normalize_name("이도령(혼)") == "이도령"
        assert normalize_name("민숙(젊은)") == "민숙"
        assert normalize_name("사령[변형]") == "사령"

    def test_strip_paren_suffix_full_width(self):
        assert normalize_name("이도령（혼）") == "이도령"
        assert normalize_name("이도령【혼】") == "이도령"
        assert normalize_name("민숙〈변형〉") == "민숙"
        assert normalize_name("민숙《변형》") == "민숙"
        assert normalize_name("민숙「변형」") == "민숙"

    def test_paren_with_surrounding_whitespace(self):
        # 괄호 전에 공백이 있으면 strip으로 제거
        assert normalize_name("이도령 (혼)") == "이도령"
        assert normalize_name("  민숙 (변형)  ") == "민숙"

    def test_no_changes_for_plain_names(self):
        assert normalize_name("민숙") == "민숙"
        assert normalize_name("Jane Doe") == "Jane Doe"
        assert normalize_name("김형사") == "김형사"

    def test_preserves_josa(self):
        """조사는 제거하지 않음 — 이름 일부일 수 있음."""
        assert normalize_name("민숙이") == "민숙이"
        assert normalize_name("수리영이") == "수리영이"

    def test_preserves_case(self):
        """영문 대소문자 보존."""
        assert normalize_name("JANE") == "JANE"
        assert normalize_name("jane") == "jane"


@dataclass
class _FakeCanon:
    name: str
    short_id: str = ""


class TestBuildNameIndex:
    def test_identity_default(self):
        items = [_FakeCanon("민숙"), _FakeCanon("이도령")]
        idx = build_name_index(items, key_fn=lambda c: c.name)
        assert idx["민숙"] is items[0]
        assert idx["이도령"] is items[1]

    def test_value_fn(self):
        items = [_FakeCanon("민숙", "C01"), _FakeCanon("이도령", "C02")]
        idx = build_name_index(
            items,
            key_fn=lambda c: c.name,
            value_fn=lambda c: c.short_id,
        )
        assert idx["민숙"] == "C01"
        assert idx["이도령"] == "C02"

    def test_variant_does_not_register_bare_key(self):
        """variant 엔티티("이도령(혼)")는 bare key("이도령") 등록 안 함 —
        base 엔티티와의 silent 충돌 방지. lookup은 fallback으로 여전히 매칭되지만
        base가 함께 있을 때 variant가 base 자리를 빼앗지 않도록 한다."""
        items = [_FakeCanon("이도령(혼)", "C02v")]
        idx = build_name_index(items, key_fn=lambda c: c.name, value_fn=lambda c: c.short_id)
        # 원본
        assert idx["이도령(혼)"] == "C02v"
        # bare key는 variant가 등록 안 함
        assert "이도령" not in idx
        # variant 원본 query는 매칭
        assert lookup_name(idx, "이도령(혼)") == "C02v"
        # bare query는 base가 없으면 miss — 의도된 엄격 매칭
        # (variant 엔티티에 silent 귀속되지 않음. lookup 쪽 책임은 엄격 전제 유지.)
        assert lookup_name(idx, "이도령") is None

    def test_base_and_variant_coexist_without_collision(self):
        """base + variant 공존 시(cfb87551 실측 케이스: "서현" + "서현 (5세)"),
        base가 bare key "서현"을 차지하고 variant는 원본 키만 보유.
        lookup 시 각자 정확히 매칭."""
        base = _FakeCanon("서현", "C01")
        v5 = _FakeCanon("서현 (5세)", "C01_5y")
        v10 = _FakeCanon("서현 (10대)", "C01_10s")
        idx = build_name_index(
            [base, v5, v10],
            key_fn=lambda c: c.name,
            value_fn=lambda c: c.short_id,
        )
        # base는 bare "서현" 차지
        assert idx["서현"] == "C01"
        # variant는 원본 키로만
        assert idx["서현 (5세)"] == "C01_5y"
        assert idx["서현 (10대)"] == "C01_10s"
        # lookup 각자 정확
        assert lookup_name(idx, "서현") == "C01"
        assert lookup_name(idx, "서현 (5세)") == "C01_5y"
        # drift 공백 변화도 variant로
        assert lookup_name(idx, "서현  (5세)") == "C01_5y"

    def test_raw_preserved_when_same_as_normalized(self):
        items = [_FakeCanon("민숙")]
        idx = build_name_index(items, key_fn=lambda c: c.name)
        # 원본만 존재 (정규화 결과와 동일 → 추가 등록 안 함)
        assert len(idx) == 1
        assert "민숙" in idx

    def test_conflict_first_wins(self):
        """같은 정규화 결과로 충돌하면 먼저 등록된 쪽 우선."""
        a = _FakeCanon("민숙", "C01")
        b = _FakeCanon("민숙(혼)", "C01v")
        # 원본 "민숙"이 a, 정규화 후 "민숙"도 a이므로 b는 원본 키만 등록되고
        # 정규화 키 "민숙"은 a 우선.
        idx = build_name_index([a, b], key_fn=lambda c: c.name, value_fn=lambda c: c.short_id)
        assert idx["민숙"] == "C01"  # a 유지
        assert idx["민숙(혼)"] == "C01v"  # b 원본

    def test_skips_empty_names(self):
        items = [_FakeCanon(""), _FakeCanon("민숙")]
        idx = build_name_index(items, key_fn=lambda c: c.name)
        assert len(idx) == 1
        assert "민숙" in idx


class TestLookupName:
    def test_exact_raw_match(self):
        idx = {"민숙": "C01"}
        assert lookup_name(idx, "민숙") == "C01"

    def test_normalization_fallback(self):
        # idx에 정규화 키만 등록돼 있는 경우 (이론적), query 원본이 매칭 안 되면 정규화
        idx = {"이도령": "C02"}
        assert lookup_name(idx, "이도령(혼)") == "C02"
        assert lookup_name(idx, "이도령 ") == "C02"

    def test_query_with_paren_matched_via_norm(self):
        """index 원본이 깨끗, query 쪽이 지저분할 때."""
        idx = {"이도령": "C02"}
        assert lookup_name(idx, "이도령(혼)") == "C02"
        assert lookup_name(idx, "  이도령  ") == "C02"

    def test_miss_returns_none(self):
        idx = {"민숙": "C01"}
        assert lookup_name(idx, "임꺽정") is None
        assert lookup_name(idx, "") is None
        assert lookup_name(idx, None) is None  # type: ignore[arg-type]

    def test_full_width_paren_query(self):
        idx = {"민숙": "C01"}
        assert lookup_name(idx, "민숙（혼）") == "C01"
        assert lookup_name(idx, "민숙【변형】") == "C01"
