"""★★★**어려운 단발 대상을 실제 엔티티 행으로 만든다** (사용자 확정 2026-08-30).

사용자 설계:

    기존 축      2번 이상 나오면 중요 엔티티로 추출  ← 그대로 둔다
    얹는 예외    이미지 모델이 만들기 어려우면      ← **1번만 나와도 등록**
                 (오래된 화폐 · 옛날 특정 브랜드 제품 · 잘 알려진 장소)

★내가 「보호 통로가 이미 등록까지 해 준다」고 보고했는데 **틀렸다** (Codex).
`protected_short_ids` 는 **이미 있는 행**의 이름을 찾아 보호할 뿐이고,
`promoted` subject 는 `short_id` 가 **없다**. 그러면 옛 화폐가 조사 대상에는
있는데 **엔티티·카드에는 없는 반쪽**이 된다.
"""
from app.modules.pipeline.grounding_overlay import (materialize_missing_entities,
                                                    protected_short_ids)


def _cand(sid, form, owner="prop", **over):
    d = {"research_subject_id": sid, "surface_form": form,
         "owner_type": owner, "source_anchor": "S1",
         "why_candidate": "옛 인쇄물", "scene_indices": [3]}
    d.update(over)
    return d


def _decided(sid, difficulty="hard", route="research"):
    return {"research_subject_id": sid, "route": route,
            "generation_difficulty": difficulty}


class TestTheOldChannelCouldNotRegister:
    """★먼저 **구멍이 진짜인지** 보인다 — 안 그러면 새 기구를 왜 만드는지 모른다."""

    def test_protection_only_finds_rows_that_already_exist(self):
        rows = {"props": [{"short_id": "P01", "name": "요금통"}]}
        got = protected_short_ids([{"route": "research", "_surface_form": "옛 지폐"}],
                                  rows)
        assert got == set(), "★없는 행을 보호할 수는 없다 — 그게 구멍이다"


class TestItRegistersTheHardOnes:
    def test_a_hard_candidate_with_no_row_becomes_one(self):
        out = materialize_missing_entities(
            [_cand("rs_a", "1983년 오백원 지폐")],
            [_decided("rs_a", "hard")],
            {"prop": [{"short_id": "P01", "name": "요금통"}]})
        assert list(out) == ["prop"]
        row = out["prop"][0]
        assert row["name"] == "1983년 오백원 지폐"
        assert row["short_id"] == "P02", "★기존 번호 다음부터 — 겹치면 하류가 엉뚱한 행을 가리킨다"
        assert row["grounding_materialized"] is True
        assert row["research_subject_id"] == "rs_a"

    def test_uncertain_counts_as_hard(self):
        out = materialize_missing_entities(
            [_cand("rs_a", "옛 승차권")], [_decided("rs_a", "uncertain")],
            {"prop": []})
        assert out["prop"][0]["name"] == "옛 승차권"

    def test_not_hard_is_left_alone(self):
        """★흔한 것까지 등록하면 저빈도 필터를 없앤 것과 같다."""
        out = materialize_missing_entities(
            [_cand("rs_a", "자전거 체인")], [_decided("rs_a", "not_hard")],
            {"prop": []})
        assert out == {}

    def test_a_candidate_that_survived_extraction_is_not_duplicated(self):
        out = materialize_missing_entities(
            [_cand("rs_a", "요금통")], [_decided("rs_a", "hard")],
            {"prop": [{"short_id": "P01", "name": "쇠사슬로 묶인 요금통"}]})
        assert out == {}, "★이미 있는 것을 또 만들면 같은 대상이 둘이 된다"


class TestItDoesNotUseRouteAsAProxy:
    """★★실측에서 아홉 축이 **전부** `research` 였다. route 로 고르면 전부 등록된다."""

    def test_research_route_alone_does_not_register(self):
        out = materialize_missing_entities(
            [_cand("rs_a", "자전거 체인")],
            [_decided("rs_a", "not_hard", route="research")],
            {"prop": []})
        assert out == {}


class TestFacetsAreNotPromotedToBaseTypes:
    """★§2-6.5 통과 조건 — 「base location 을 prop 으로 우회 등록한 행 0건」."""

    def test_a_location_part_is_not_registered_here(self):
        out = materialize_missing_entities(
            [_cand("rs_a", "정비소 벽면", owner="location_part")],
            [_decided("rs_a", "hard")], {"location": [], "prop": []})
        assert out == {}

    def test_an_outlook_is_not_registered_here(self):
        out = materialize_missing_entities(
            [_cand("rs_a", "차장 제복", owner="outlook")],
            [_decided("rs_a", "hard")], {"character": [], "prop": []})
        assert out == {}


class TestNumbersDoNotCollide:
    def test_two_new_rows_get_two_free_numbers(self):
        out = materialize_missing_entities(
            [_cand("rs_a", "옛 지폐"), _cand("rs_b", "옛 승차권")],
            [_decided("rs_a"), _decided("rs_b")],
            {"prop": [{"short_id": "P01", "name": "요금통"},
                      {"short_id": "P03", "name": "가방"}]})
        got = [r["short_id"] for r in out["prop"]]
        assert got == ["P02", "P04"], got


class TestItWorksWithTheKeysTheCallerActuallyUses:
    """★★★호출부(`entity_filter`)의 dict 는 **복수 키**다 —
    `characters`/`locations`/`props`. 이 모듈의 다른 함수는 단수를 쓴다.

    단수만 받게 두면 `get()` 이 `None` 을 내고 **아무것도 안 만들면서 조용히
    통과**한다. 시험을 단수로만 쓰면 그 죽음을 못 본다 — 실제로 그럴 뻔했다.
    """

    def test_plural_keys_register_too(self):
        out = materialize_missing_entities(
            [_cand("rs_a", "1983년 오백원 지폐")], [_decided("rs_a", "hard")],
            {"characters": [], "locations": [], "props": [
                {"short_id": "P01", "name": "요금통"}]})
        assert "props" in out, f"★복수 키에서 조용히 아무것도 안 만들었다 — {out}"
        assert out["props"][0]["short_id"] == "P02"

    def test_it_gives_back_the_key_it_was_given(self):
        """★받은 키 그대로 돌려준다 — 호출부가 변환표를 또 두면 한쪽만 고쳐진다."""
        singular = materialize_missing_entities(
            [_cand("rs_a", "옛 지폐")], [_decided("rs_a")], {"prop": []})
        plural = materialize_missing_entities(
            [_cand("rs_a", "옛 지폐")], [_decided("rs_a")], {"props": []})
        assert list(singular) == ["prop"] and list(plural) == ["props"]
        assert singular["prop"][0]["name"] == plural["props"][0]["name"]
