"""판별 스텝(13.68) **끝점** — 조립 helper 가 아니라 실제 배선을 본다.

★조립부만 부르면 「멀쩡히 도는데 아무 데도 안 닿는」 부류를 놓친다. 여기서
보는 것:

    ① manifest 자리·의무·무효화 사슬
    ② `entity_filter` 가 **판별 CP** 를 읽는다 (옛 축이 아니라)
    ③ 미확정이 있으면 `failed_count` 가 서서 하류가 막힌다
    ④ resume 이 옛 판별을 **다시 안 산다**
"""
from pathlib import Path

import pytest

from app.core.step_manifest import STEP_MANIFEST, get_manifest_dict
from app.modules.pipeline import grounding_screen as gs

SRC = Path("app/core/steps/entity_steps.py").read_text(encoding="utf-8")
STEP_SRC = Path("app/core/steps/grounding_screen_step.py").read_text(encoding="utf-8")


class TestThreeAIsInert:
    """★★**3a 는 아직 안 켠다** (Codex 2026-08-31).

    지금 켜면 substring 결속 위에서 **돈을 사고 행을 승격**한다. producer
    쪽 opaque ID 사슬(entity_all → extract → merge)을 3b 에서 닫은 뒤에
    같이 켠다.
    """

    def test_the_step_runs_on_the_chunk_producer_now(self):
        """★★**뒤집었다** (2026-09-01 D 활성화) — `disabled` 에서 켰다.

        C(c) producer 를 타는 판에서만 돈다. legacy·shadow·v2 에서는
        dispatcher 관점에서 `not_applicable` 이라 **한 글자도 안 달라진다**.
        """
        assert get_manifest_dict("grounding_screen")["applicability"] == \
            "if_chunk_producer"

    def test_the_filter_does_not_consume_it_yet(self):
        assert "grounding_screen" not in get_manifest_dict("entity_filter")["depends_on"]
        assert "grounding_screen" not in SRC
        assert "promotable_obligations" not in SRC

    def test_the_old_channel_is_untouched(self):
        """★인접 코드를 지금 바꾸지 않는다 — 대체가 아직 없다."""
        assert "needs_reference_acquisition" in Path(
            "app/modules/pipeline/grounding_overlay.py").read_text(encoding="utf-8")

    def test_the_new_pack_is_not_pinned_yet(self):
        """★pin 하면 기존 era 캐시가 전부 갈리고 뒤쪽 판별이 그 자리에서 바뀐다."""
        from app.modules.pipeline import era_research as era

        assert era.ERA_RESEARCH_PACK_VERSION == "3"
        assert "4" in era.PACK_VERSION_MAP, "★팩 자체는 있어야 3b 가 pin 한다"


class TestTheNewPackHasNoHandWrittenList:
    """★사용자 계약 1·2 — 구체 대상 예시 0."""

    PACK = Path("../prompts/_base/era_research/4.202608311200/assess_sys.md")

    def test_it_exists_and_the_old_one_is_untouched(self):
        assert self.PACK.exists()
        old = Path("../prompts/_base/era_research/3.202608251500/assess_sys.md")
        assert old.exists(), "★옛 팩은 소비자가 없어질 때까지 보존한다"

    def test_it_names_no_object_category(self):
        """★옛 판이 손으로 적어 둔 것들 — 목록에 없는 부류를 못 건진다."""
        body = self.PACK.read_text(encoding="utf-8").lower()
        for word in ("currency", "vehicle", "uniform", "appliance", "footwear",
                     "fashion", "electronics", "storefront", "transit",
                     "chair", "cup", "tree", "security features"):
            assert word not in body, f"★손 목록이 남았다: {word}"

    def test_it_still_asks_the_two_conditions(self):
        body = self.PACK.read_text(encoding="utf-8")
        assert "era and region" in body
        assert "subject_native" in body and "search_terms_native" in body
        assert "language_lock_native" in body


class TestItSitsWhereItMust:
    def test_it_runs_before_the_low_frequency_filter(self):
        """★거르기 **전**이어야 한다 — 판별이 연 것을 필터가 지우기 전에."""
        assert (get_manifest_dict("grounding_screen")["order"]
                < get_manifest_dict("entity_filter")["order"])

    def test_it_runs_before_the_paid_claim_search(self):
        """★★사용자 설계는 「판별 → 기초 검색 → 이미지 검색」이다.

        뒤에 두면 판별이 「비대상」이라 할 것까지 **검색을 먼저 사 버린다**
        (Codex BLOCK-4).
        """
        assert (get_manifest_dict("grounding_screen")["order"]
                < get_manifest_dict("grounding_research")["order"])

    def test_it_is_invalidated_by_what_it_reads(self):
        deps = get_manifest_dict("grounding_screen")["depends_on"]
        assert {"entity_merge", "grounding_a0", "visual_world_rules"} <= set(deps)

    def test_it_does_not_take_the_old_axis_as_a_dependency(self):
        """★`grounding_plan` 을 dep 로 걸면 옛 축에 다시 매인다."""
        assert "grounding_plan" not in get_manifest_dict("grounding_screen")["depends_on"]

    def test_it_is_not_reachable_from_legacy(self):
        """★legacy·shadow·v2 에서는 안 돈다 — 그 판들은 한 글자도 안 달라진다."""
        from app.core.grounding_mode import (GROUNDING_MODE_LEGACY,
                                             GROUNDING_MODE_SHADOW_PLAN,
                                             GROUNDING_MODE_V2,
                                             uses_chunk_producer)

        assert get_manifest_dict("grounding_screen")["applicability"] == \
            "if_chunk_producer"
        for m in (GROUNDING_MODE_LEGACY, GROUNDING_MODE_SHADOW_PLAN,
                  GROUNDING_MODE_V2):
            assert uses_chunk_producer(m) is False, m

    def test_a_stuck_screen_blocks_downstream(self):
        m = get_manifest_dict("grounding_screen")
        assert m["allow_partial_downstream"] is False
        assert m["partial_override_config_key"] == "allow_missing_grounding"

    def test_its_order_is_free(self):
        """★겹치면 순서가 안 정해진다."""
        mine = get_manifest_dict("grounding_screen")["order"]
        others = [v["order"] for k, v in STEP_MANIFEST.items()
                  if k != "grounding_screen" and "order" in v]
        assert others.count(mine) == 0

    def test_the_registry_knows_it(self):
        from app.core.steps import STEP_CLASSES
        from app.core.steps.grounding_screen_step import GroundingScreenStep

        assert STEP_CLASSES["grounding_screen"] is GroundingScreenStep


class TestTheStepItself:
    """★③④ 실행 없이 **소스**로 보는 자리 — DB 없이 도는 시험이다."""

    def test_unresolved_counts_as_failed(self):
        assert "SCREEN_UNRESOLVED" in STEP_SRC and "failed_count" in STEP_SRC

    def test_the_cap_is_counted_as_failed_too(self):
        """★상한에 걸린 것을 완료로 세면 「안 사고 통과」가 된다."""
        i = STEP_SRC.index("failed = (counts.get")
        assert "SCREEN_CAPPED" in STEP_SRC[i:i + 300]

    def test_it_carries_the_cache_forward(self):
        """★resume 이 같은 것을 **다시 사면** 안 된다. 빈 목록도 캐시다."""
        assert "self._load_prev_checkpoint(self.step_id)" in STEP_SRC
        assert gs.__name__.split(".")[-1] in STEP_SRC or "_screen" in STEP_SRC

    def test_it_saves_what_it_bought_before_judging(self):
        """★★N번째에서 끊기면 앞의 N-1 유료 결과가 사라진다 — 겪은 부류다.

        체크포인트는 스텝이 **끝나야** 써진다. 그래서 호출마다 따로 남기고,
        다음 판이 그것을 이어받는다.
        """
        assert "on_row=_flush_row" in STEP_SRC
        assert "cache.update(self._load_partial())" in STEP_SRC

    def test_the_partial_is_never_deleted_by_the_step(self):
        """★★**뒤집힌 계약** (Codex BLOCK-2). 앞 판은 `_wrap` 뒤에 지웠는데,
        실제 저장은 `_execute` **가 돌아온 뒤** `save_checkpoint` 에서 난다.
        그래서 반환 직후 crash 면 산 것이 아무 데도 안 남았다.

        ★내 앞 시험은 「`_wrap` 앞이 아니다」만 봐서 **가짜 안전**을 잠갔다.
        """
        assert "_clear_partial" not in STEP_SRC
        assert ".unlink()" not in STEP_SRC

    def test_the_partial_write_is_atomic(self):
        """★쓰는 도중에 끊기면 다음 판이 반쪽 JSON 을 읽는다."""
        assert "os.replace(tmp, p)" in STEP_SRC

    def test_it_records_zero_purchases(self):
        assert '"search_calls": 0' in STEP_SRC
        assert '"image_calls": 0' in STEP_SRC

    def test_it_folds_the_assess_contract_into_the_fingerprint(self):
        """★안 접으면 팩·모델을 바꿔도 resume 이 옛 판별을 건너뛴다."""
        i = STEP_SRC.index("def _config_hash")
        tail = STEP_SRC[i:]
        for k in ("era_pack_hash", "assess_model", "assess_model_physical",
                  "screen_contract", "carry_contract", "era_policy"):
            assert k in tail, k

    def test_it_refuses_to_screen_without_world_context(self):
        """★빈 세계관으로 사면 그 돈이 그냥 버려진다."""
        assert "missing_world_context" in STEP_SRC

    def test_binding_comes_from_the_one_carry_function(self):
        """★결속을 여기서 다시 짝지으면 세 곳이 갈린다."""
        assert "_carry.build_subjects(" in STEP_SRC
        assert "match_candidate" not in STEP_SRC


class TestNoScenarioWordsInTheCode:
    """★사용자 계약 1·6 — 특정 작품·물건·연도를 코드가 알면 안 된다."""

    @pytest.mark.parametrize("src", [STEP_SRC, gs.__doc__ or ""])
    def test_the_source_holds_no_literal_target_list(self, src):
        import re

        # ★연도 네 자리처럼 **시나리오를 특정하는 값**이 코드에 있으면 안 된다.
        #  계약 좌표(팩 버전 `N.YYYYMMDDHHmm`)는 문자열 안에 있으므로 제외한다.
        bare = re.findall(r"(?<![\d.])(1[89]|20)\d{2}(?![\d.])", src)
        assert not bare, f"★연도가 코드에 박혔다: {bare[:3]}"

    def test_the_owner_set_is_a_controlled_enum_not_a_word_list(self):
        from app.modules.pipeline.grounding_carry import FACET_OWNERS

        assert FACET_OWNERS == frozenset({"location_part", "outlook"})


class TestTheProducerContractIsFoldedIntoResume:
    """★★★안 접으면 **payload 가 없던 옛 CP** 를 resume 이 그대로 쓴다
    (Codex NON-BLOCK · 09-01). 그러면 하류가 판정 칸 없이 돈다."""

    def test_the_config_hash_reads_the_producer_contract(self):
        import ast
        import inspect
        import textwrap

        from app.core.steps.grounding_screen_step import GroundingScreenStep

        src = inspect.getsource(GroundingScreenStep._config_hash)
        tree = ast.parse(textwrap.dedent(src))
        names = {ast.unparse(n) for n in ast.walk(tree)
                 if isinstance(n, ast.Attribute)}
        assert any("PRODUCER_CONTRACT_VERSION" in n for n in names), names

    def test_bumping_it_changes_the_hash(self):
        """★양성 대조 — 판이 바뀌면 지문이 **바뀌어야** 한다."""
        from unittest.mock import patch

        from app.core.steps.grounding_screen_step import GroundingScreenStep
        from app.modules.pipeline import grounding_entity_contract as ec

        step = GroundingScreenStep.__new__(GroundingScreenStep)
        step.project_config = {}
        before = step._config_hash()
        with patch.object(ec, "PRODUCER_CONTRACT_VERSION", "9.999"):
            after = step._config_hash()
        assert before != after, "★판을 올려도 지문이 그대로다"

    def test_the_field_split_is_only_a_declaration_for_now(self):
        """★★**과대보고 금지** — 이 두 표를 읽는 실제 발신부가 아직 없다."""
        import ast
        from pathlib import Path as _P

        root = _P(__file__).resolve().parents[2] / "app"
        hits = []
        for f in root.rglob("*.py"):
            if f.name == "grounding_entity_contract.py":
                continue
            try:
                tree = ast.parse(f.read_text(encoding="utf-8"))
            except SyntaxError:                     # noqa: PERF203
                continue
            for n in ast.walk(tree):
                if isinstance(n, ast.Name) and n.id in ("JUDGE_FIELDS",
                                                        "SEARCH_FIELDS"):
                    hits.append(f.name)
        assert hits == [], (
            f"★발신부가 생겼다: {hits} — 그러면 이 시험을 **뒤집고** 실제 "
            "전송이 갈리는지 끝점으로 잰다")
