"""참조 획득 **대상 선별** — ★유료 0.

첫 유료 판에서 대상 13개 중 **8개가 다른 대상의 부분**이었고, 못 고른 3개가
**전부 그 8개 안**이었다. 표지를 찍은 사진에 그 글자와 가로줄이 **이미 들어
있는데** 잔부분을 따로 사려 한 것이다.
"""
from __future__ import annotations

import ast
import inspect

import pytest

from tools.grounding_audit import ref_canary as rc


def _row(lid, owner, form, *, hard=True, notice=True):
    return {"local_id": lid, "owner_type": owner, "surface_form": form,
            "hard_to_generate": hard, "viewers_would_notice": notice,
            "visual_brief": "겉모습", "occurrences": [],
            "search_terms_native": [], "language_lock_native": "ko"}


def _reduced(rows, part_of=(), reg=None):
    return {"rows": rows, "part_of": list(part_of),
            "registered": reg or {r["local_id"]: {"registered": True,
                                                  "final_id": r["local_id"]}
                                  for r in rows}}


class TestAPartIsNotBoughtWhenItsWholeIs:
    """★통째를 찍은 사진에 부분이 이미 들어 있다."""

    def test_the_part_is_skipped(self):
        red = _reduced(
            [_row("w", "location_part", "표지"),
             _row("p", "location_part", "표지의 글자")],
            part_of=[{"part": "p", "whole": "w"}])
        got = [t["local_id"] for t in rc.targets_from(red)]
        assert got == ["w"], f"★잔부분까지 산다: {got}"

    def test_the_skip_is_recorded_not_silent(self):
        """★조용히 사라지면 「왜 이것만 샀나」를 못 되짚는다."""
        red = _reduced(
            [_row("w", "location_part", "표지"),
             _row("p", "location_part", "표지의 글자")],
            part_of=[{"part": "p", "whole": "w"}])
        sk = rc.parts_skipped(red)
        assert len(sk) == 1
        assert sk[0]["part_form"] == "표지의 글자"
        assert sk[0]["whole_form"] == "표지"

    def test_a_part_whose_whole_is_not_a_target_is_still_bought(self):
        """★★부모가 대상이 아니면 **그대로 산다** — 잃으면 안 된다."""
        red = _reduced(
            [_row("w", "location", "방", hard=False, notice=False),
             _row("p", "location_part", "방의 걸상")],
            part_of=[{"part": "p", "whole": "w"}])
        got = [t["local_id"] for t in rc.targets_from(red)]
        assert got == ["p"], "★부모가 대상이 아닌데 부분을 뺐다"

    def test_a_row_without_both_axes_is_never_a_target(self):
        red = _reduced([_row("a", "prop", "가", notice=False)])
        assert rc.targets_from(red) == []

    def test_relations_come_from_the_ledger_not_from_names(self):
        """★이름·부분문자열로 짐작하지 않는다."""
        red = _reduced(
            # ★이름은 품고 있지만 `part_of` 기록이 **없다**
            [_row("w", "location_part", "표지"),
             _row("p", "location_part", "표지의 글자")])
        got = [t["local_id"] for t in rc.targets_from(red)]
        assert set(got) == {"w", "p"}, "★기록에 없는 관계를 이름으로 지어냈다"

    def test_the_code_does_not_read_surface_forms_for_this(self):
        tree = ast.parse(inspect.getsource(rc.targets_from))
        for n in ast.walk(tree):
            if isinstance(n, ast.Constant) and isinstance(n.value, str):
                assert "surface" not in n.value or "surface_form" == n.value, \
                    f"★표면형을 판정에 쓴다: {n.value!r}"


class TestAgainstTheRealPaidOutput:
    """★★합성만으로 끝내지 않는다 — **실제로 산 산출**에 물려 본다."""

    @pytest.fixture(scope="class")
    def reduced(self):
        from pathlib import Path

        from tests.grounding.fixtures import synthetic_episode as ep
        from tools.grounding_audit import cc_runner as rr

        p = (Path(__file__).resolve().parents[3] / "artifact"
             / "20260831_ref_canary" / "j.json")
        if not p.exists():
            pytest.skip("얼어붙은 유료 장부가 없다")
        try:
            return rr.replay(p, world_facts=ep.WORLD_FACTS)["reduced"]
        except LookupError:
            # ★★팩·계약을 올리면 신원이 달라져 옛 장부를 **못 찾는 것이 맞다**
            #  — 그것이 재구매를 막는 구조다. 여기서 「없다」로 읽고 넘어가면
            #  안 되므로 **왜 건너뛰는지** 적는다.
            pytest.skip("팩·계약이 올라가 옛 장부의 신원과 다르다 — "
                        "재채점하려면 그 팩으로 다시 사야 한다")

    def test_it_drops_the_eight_sub_parts(self, reduced):
        assert len(rc.parts_skipped(reduced)) == 8
        assert len(rc.targets_from(reduced)) == 5, "★13에서 5로 안 줄었다"

    def test_the_three_that_found_nothing_are_gone(self, reduced):
        """★못 고른 셋이 **전부** 잔부분이었다."""
        forms = {t["surface_form"] for t in rc.targets_from(reduced)}
        for gone in ("위에서 아래로 내려가는 글자",
                     "글자 왼편에 그어진 짧은 가로줄", "대합실 안"):
            assert gone not in forms, f"★{gone} 이 아직 대상이다"

    def test_the_ones_that_worked_are_kept(self, reduced):
        """★positive control — 잘 되던 것까지 빼면 안 된다."""
        forms = {t["surface_form"] for t in rc.targets_from(reduced)}
        for keep in ("정류장 승강장", "정류장 표지", "제복 상의", "차",
                     "옛 요금표"):
            assert keep in forms, f"★{keep} 이 빠졌다"

    def test_the_budget_shrinks_accordingly(self, reduced):
        n = len(rc.targets_from(reduced))
        assert rc.approved_logical(n) == 23, "★상한이 안 줄었다"
