"""검색 질의가 **선언된 좌표를 싣나**. ★유료 0.

실측 (2026-09-02 유료 canary ①) — chunk 팩은 「시대·지역은 **뒤 단계가**
붙인다」고 적어 두었는데 중앙 경로에 그 뒤 단계가 **없었다**. 저작기를
canary 도구만 넘겼고 production 은 `visual_brief` 를 질의로 그대로 썼다.

    시대가 빠진 질의 → 기록물은 **앞선 시기**가 훨씬 많아 옛것이 온다
    지역이 빠진 질의 → **같은 언어권 옆 나라** 것이 온다

★★사용자 확정 (2026-09-02): 「무조건 과거 것만 고증이 필요한 게 아니야.
주유소·편의점 등도 나라별 특징이 있는데 이미지 모델이 잘 몰라.」 —
그래서 **지역만 선언된 현대 판**도 그 지역으로 찾고, 시대를 지어내지 않는다.
"""
from __future__ import annotations

import ast
import inspect
import textwrap

import pytest

from app.modules.pipeline import grounding_search_brief as gsb

TARGET = {"subject_id": "rs1", "owner_type": "location",
          "coarse_type_label": "차에 기름을 넣는 곳",
          "surface_form": "그 가게", "visual_brief": "길가에 있다"}


def _sent(**kw):
    """저작기가 **실제로 보내는 것**을 잡는다. ★바깥으로 안 나간다."""
    got = {}

    def _call(tag, system, user, schema, **rest):
        got.update({"tag": tag, "system": system, "user": user,
                    "schema": schema, **rest})
        return {"search_directive_native": "지시", "search_terms_native": ["말"],
                "language_lock_native": "그 나라 말"}

    w = gsb.make_writer(call=_call, **kw)
    return w, got


class TestTheProductionCallerActuallyPassesIt:
    """★★★①canary 도구만 넘기는 판은 불합격이다."""

    def test_the_central_caller_wires_write_brief(self):
        from app.core.steps.reference_acquisition_step import (
            ReferenceAcquisitionStep as S)

        src = textwrap.dedent(inspect.getsource(S._central))
        wired = set()
        for n in ast.walk(ast.parse(src)):
            if (isinstance(n, ast.Call)
                    and ast.unparse(n.func).endswith("central_result")):
                wired |= {k.arg for k in n.keywords}
        assert "write_brief" in wired, "★중앙이 저작기를 안 넘긴다"

    def test_it_reaches_acquire_one(self):
        from app.modules.pipeline import grounding_central_acquisition as ca

        src = textwrap.dedent(inspect.getsource(ca.run))
        wired = set()
        for n in ast.walk(ast.parse(src)):
            if (isinstance(n, ast.Call)
                    and ast.unparse(n.func).endswith("acquire_one")):
                wired |= {k.arg for k in n.keywords}
        assert "write_brief" in wired

    def test_the_cap_reads_two_from_the_final_caller(self):
        """★⑥최종 caller 에서 2 를 읽고 계획이 그것으로 선다."""
        from tools.grounding_audit import canary_run as cr

        assert cr.central_calls_per_target_per_round() == 2
        g = cr.central_logical_cap("period_episode")
        # 2026-09-02 밤: 대상당 재판정 1 을 더해 5×(2×2+1) = 25 (앞 판 20 · 실측 40 에 죽음)
        assert g["logical_cap"] == 25 and g["one_round_each"] == 10


class TestBothCoordinatesSurviveEveryRound:
    """★②시대+지역 판 — 1·2라운드 질의 **모두** 두 좌표를 싣는다."""

    KW = dict(world_facts="창작자 확정", source_text="원문 표본",
              era="1960년대", region="대한민국")

    @pytest.mark.parametrize("narrow", [False, True])
    def test_the_user_block_carries_both(self, narrow):
        w, got = _sent(**self.KW)
        w(TARGET, narrow=narrow)
        assert "1960년대" in got["user"], got["user"][:200]
        assert "대한민국" in got["user"]

    def test_the_narrow_round_adds_the_hint(self):
        w, got = _sent(**self.KW)
        w(TARGET, narrow=True)
        plain = _sent(**self.KW)
        plain[0](TARGET, narrow=False)
        assert len(got["user"]) > len(plain[1]["user"])


class TestAModernRunKeepsTheRegionAndInventsNoEra:
    """★★★③지역만 선언된 현대 판 — 지역 보존 · 시대 **발명 0**."""

    KW = dict(world_facts="창작자 확정", source_text="원문 표본",
              era="", region="대한민국")

    def test_the_region_is_there(self):
        w, got = _sent(**self.KW)
        w(TARGET)
        assert "대한민국" in got["user"]

    def test_no_period_line_appears(self):
        w, got = _sent(**self.KW)
        w(TARGET)
        assert "THE PERIOD, as the creator declared it" not in got["user"]

    def test_a_modern_subject_is_not_skipped(self):
        """★시대가 없다고 **대상에서 빼지 않는다**."""
        w, got = _sent(**self.KW)
        assert w(TARGET)["search_terms_native"] == ["말"]

    def test_the_pack_tells_the_model_the_two_are_independent(self):
        from app.modules.pipeline import grounding_ref_brief as grb

        text = grb.load_brief_system()
        assert "independent" in text
        assert "set in the present" in text
        assert "language of the country this story is set in" not in text, (
            "★원고 언어 = 배경 나라 언어라는 단정이 남아 있다")

    def test_there_are_no_fixed_place_nouns_in_the_pack(self):
        """★하드프롬프트 금지 — 나라·가게 이름을 팩에 안 박는다."""
        from app.modules.pipeline import grounding_ref_brief as grb

        text = grb.load_brief_system()
        for banned in ("주유소", "편의점", "대한민국", "Korea", "Japan",
                       "7-Eleven"):
            assert banned not in text, banned


class TestChangingACoordinateChangesTheIdentity:
    """★④좌표가 바뀌면 **다시 산다**. ⑤같으면 재개가 안 산다."""

    def _ident(self, **kw):
        from app.modules.pipeline import grounding_central_acquisition as ca

        w = gsb.make_writer(world_facts="w", source_text="s", **kw)
        return ca.identity_of({"target": dict(TARGET)}, contract_sha="sha",
                              brief_inputs=w.identity_inputs)

    def test_a_different_region_is_a_different_purchase(self):
        assert self._ident(era="1960년대", region="가") != \
            self._ident(era="1960년대", region="나")

    def test_a_different_era_is_a_different_purchase(self):
        assert self._ident(era="1960년대", region="가") != \
            self._ident(era="1970년대", region="가")

    def test_a_different_pack_is_a_different_purchase(self, monkeypatch):
        """★저작 팩이 바뀌면 **다시 산다**.

        ★`PACK_VERSION` 은 `resolve_pack_version(version=PACK_VERSION)` 의
        **기본 인자로 일찍 묶여** 있어 그 상수를 갈아 끼워도 안 먹는다.
        실제로 변하는 자리는 **해석된 판**이므로 그것을 갈아 끼운다.
        """
        from app.modules.pipeline import grounding_ref_brief as grb

        before = self._ident(era="1960년대", region="가")
        monkeypatch.setattr(grb, "resolve_pack_version",
                            lambda *a, **k: "2.202608311930")
        assert self._ident(era="1960년대", region="가") != before

    def test_the_identity_carries_the_resolved_pack(self):
        from app.modules.pipeline import grounding_ref_brief as grb

        got = gsb.identity_inputs(world_facts="w", era="e", region="r")
        assert got["brief_pack"] == grb.resolve_pack_version()
        assert got["brief_pack"].startswith("5."), "★새 팩이 안 실린다"

    def test_the_same_coordinates_reuse(self):
        """★⑤같은 좌표·같은 계약이면 **같은 신원** — 재개가 안 산다."""
        assert self._ident(era="1960년대", region="가") == \
            self._ident(era="1960년대", region="가")


class TestItTakesTheCoordinatesFromTheStructuredFields:
    """★전문에서 짐작하지 않는다 — 제 칸에서 읽는다."""

    def test_it_reads_era_and_region(self):
        got = gsb.coordinates_of({"data": {"era": "1960년대",
                                           "region": "대한민국"}})
        assert got == {"era": "1960년대", "region": "대한민국"}

    def test_missing_fields_are_empty_not_invented(self):
        assert gsb.coordinates_of({"data": {}}) == {"era": "", "region": ""}
        assert gsb.coordinates_of(None) == {"era": "", "region": ""}

    def test_it_refuses_without_authoring_material(self):
        w, _got = _sent(world_facts="w", source_text="s", region="가")
        with pytest.raises(gsb.BriefInputsMissing):
            w({"subject_id": "x", "owner_type": "prop"})


class TestTheCanaryChecksTheOutboundQueries:
    """★★Codex 조건 — 실제 나간 질의에서 **좌표 누락 0** 을 기계로 센다."""

    def _rows(self, *queries):
        return [{"research_subject_id": "rs1",
                 "acquisition": {"rounds": [
                     {"round_no": 1, "queries": list(queries)}]}}]

    def test_both_present_passes(self):
        from tools.grounding_audit import canary_run as cr

        got = cr.assert_coordinates_in_queries(
            self._rows("1960년대 대한민국 기름 넣는 곳"),
            era="1960년대", region="대한민국")
        assert got["ok"] and got["queries"] == 1 and got["missing"] == []

    def test_a_missing_region_is_named(self):
        from tools.grounding_audit import canary_run as cr

        got = cr.assert_coordinates_in_queries(
            self._rows("1960년대 기름 넣는 곳"),
            era="1960년대", region="대한민국")
        assert not got["ok"]
        assert got["missing"][0]["missing"] == ["region"]

    def test_an_undeclared_coordinate_is_not_required(self):
        """★★현대 판 — 시대가 선언 안 됐으면 **요구하지 않는다**."""
        from tools.grounding_audit import canary_run as cr

        got = cr.assert_coordinates_in_queries(
            self._rows("대한민국 기름 넣는 곳"), era="", region="대한민국")
        assert got["ok"] and got["declared"] == {"region": "대한민국"}

    def test_it_counts_every_round(self):
        from tools.grounding_audit import canary_run as cr

        rows = [{"research_subject_id": "rs1", "acquisition": {"rounds": [
            {"round_no": 1, "queries": ["가 나"]},
            {"round_no": 2, "queries": ["가", "가 나"]}]}}]
        got = cr.assert_coordinates_in_queries(rows, era="가", region="나")
        assert got["queries"] == 3 and len(got["missing"]) == 1
        assert got["missing"][0]["round"] == 2


class TestTheWorldBlockComesFromProduction:
    """★★★같은 블록을 두 벌 만들지 않는다.

    실측 (2026-09-02): 이 함수를 `search_grounded_ref` 에서 찾고 없으면
    조용히 제 손으로 만들었다 — 실제 자리는 `app.core.world_context` 다.
    「못 찾았다」를 「없다」로 읽은 것이다.
    """

    def test_the_step_calls_the_real_builder(self):
        import textwrap

        from app.core.steps.reference_acquisition_step import (
            ReferenceAcquisitionStep as S)

        # ★2026-09-03: 공장이 모듈 함수 `world_facts_block_of` 로 옮겨졌다 — 스텝 메서드는 그것을 부른다
        from app.core.steps import reference_acquisition_step as ras
        assert "world_facts_block_of(" in textwrap.dedent(inspect.getsource(S._world_facts_block))
        src = textwrap.dedent(inspect.getsource(ras.world_facts_block_of))
        # ★★다섯 갈래 공용이므로 **`build_grounding_world_facts`** 다.
        #  `build_world_facts_block` 은 장소 물성 규칙만 남겨 costume·
        #  projection 을 버린다 — 인물·아웃룩이 그 규칙으로 판단된다
        #  (Codex 2026-09-02 · 실측 CP 61건에서 costume 76건).
        assert ("from app.core.world_context import "
                "build_grounding_world_facts") in src
        assert "getattr(" not in src, "★못 찾으면 제 손으로 만드는 자리가 남았다"
        assert "except" not in src, "★실패를 삼키는 자리가 남았다"

    def test_it_carries_each_coordinate_on_its_own(self):
        from app.core.world_context import build_grounding_world_facts as b

        both = b({"data": {"region": "가", "era": "나"}})
        assert "가" in both and "나" in both
        modern = b({"data": {"region": "가"}})
        assert "가" in modern and "나" not in modern, modern
        assert b(None) == ""

    def test_a_modern_block_still_anchors_the_place(self):
        """★현대여도 **지역 앵커**가 남는다 — 그것이 나라별 생김새를 부른다."""
        from app.core.world_context import build_grounding_world_facts as b

        got = b({"data": {"region": "어떤 나라"}})
        assert "Region" in got and "Era" not in got

    def test_it_keeps_the_rules_the_place_builder_throws_away(self):
        """★★★인물·아웃룩이 쓰는 규칙이 **안 버려진다**.

        실측 (2026-08-31, CP 61건): `costume` 76 · `projection` 76 ·
        `body_deformation` 62 · `transformation` 36 이 장소 필터에 안 든다.
        """
        from app.core.world_context import (build_grounding_world_facts,
                                            build_world_facts_block)

        cp = {"data": {"region": "가", "rules": [
            {"rule_type": "costume", "visual_guideline": "옷 규칙"}]}}
        assert "옷 규칙" in build_grounding_world_facts(cp)
        assert "옷 규칙" not in build_world_facts_block(cp)
