"""참조 획득 **장부** — 같은 신원이면 다시 안 산다. ★유료 0.

★★장부가 **없어서** 실제로 사고를 냈다 (2026-08-31): 일부만 다시 돌리며 기록
파일을 통째로 다시 써서 앞서 산 10개 기록이 사라졌다. 사진은 남았지만 나간
질의·후보별 판정은 복구 못 했다. `artifact/` 는 gitignore 라 되돌릴 곳도 없었다.
"""
from __future__ import annotations

import ast
import inspect

import pytest

from tools.grounding_audit import ref_canary as rc

T = {"subject_id": "P01", "surface_form": "가방", "owner_type": "prop",
     "coarse_type_label": "손에 드는 것", "visual_brief": "낡은 것"}
M = {"search": "gpt-5.6", "pick": "gemini-pro"}
#: 저작기로 **나가는 것**을 정하는 자리들. ★신원은 이것 전부를 접어야 한다.
CTX = {"world_facts": "세계 사실", "source_text": "원고 표본",
       "era_declaration": "1960년대", "region_declaration": "대한민국"}


def _id(t=None, *, pack="10", models=None, ctx=None):
    return rc.acquisition_identity(t or T, pack=pack, models=models or M,
                                   context={**CTX, **(ctx or {})})


class TestTheIdentityIsTheOutboundPayload:
    """★★★신원은 **나가는 payload 그대로**다 (Codex BLOCK 2026-08-31).

    앞 판은 필드를 **손으로 나열**했다 — `subject_id`·옛 directive·옛 terms·
    pack·models·저작 계약. 그런데 저작기로 실제 나가는 것은 그보다 넓었다:
    owner · `coarse_type_label` · **`visual_brief`** · world facts · 시대 ·
    지역 · 원문 언어 표본. 그래서 **같은 이름이 다른 시대·지역에 나와도**
    옛 검색·선택 결과를 되썼다 — 다른 검색인데 같은 것으로 본 것이다.
    """

    def test_the_same_everything_gives_the_same_identity(self):
        assert _id() == _id()

    @pytest.mark.parametrize("change", [
        {"surface_form": "다른 것"},
        {"visual_brief": "아주 다른 설명"},
        {"owner_type": "location"},
        {"coarse_type_label": "아주 다른 부류"},
    ])
    def test_a_changed_writer_input_moves_the_identity(self, change):
        """★★`visual_brief` 가 여기 있는 것이 **뒤집은 자리**다.

        앞 시험은 「`visual_brief` 는 질의가 아니라 판정 자료다 — 접으면
        재구매」라며 **안 움직이는 것을 계약으로 잠갔다**. v2 에서는
        `visual_brief` 가 **저작기의 질의 재료**다. 안 움직이면 다른 검색을
        같은 것으로 본다.
        """
        assert _id({**T, **change}) != _id(), f"★{change} 가 신원을 안 움직인다"

    @pytest.mark.parametrize("key", sorted(CTX))
    def test_a_changed_world_coordinate_moves_the_identity(self, key):
        """★같은 이름이 **다른 시대·지역**에 나오면 다른 검색이다."""
        assert _id(ctx={key: CTX[key] + " 다름"}) != _id(), \
            f"★{key} 가 바뀌어도 같은 신원이다"

    def test_the_pack_and_models_are_folded(self):
        assert _id(pack="11") != _id()
        assert _id(models={**M, "pick": "다른모델"}) != _id()

    def test_both_rounds_are_folded(self):
        """★좁힘 라운드는 **다른 것이 나간다** — 그것도 신원의 일부다."""
        a = rc.acquisition_identity(T, pack="10", models=M, context=CTX,
                                    rounds=1)
        b = rc.acquisition_identity(T, pack="10", models=M, context=CTX,
                                    rounds=2)
        assert a != b, "★라운드 수가 달라도 같은 신원이다"

    def test_it_does_not_hand_list_the_fields(self):
        """★★필드를 다시 나열하면 **다음에 늘어난 것을 또 놓친다**.

        신원은 `brief_outbound` 가 만든 payload 를 접어야 한다 — 그 함수가
        곧 전송이므로 저작 입력이 늘어도 신원이 저절로 따라 움직인다.
        """
        src = inspect.getsource(rc.acquisition_identity)
        assert "brief_outbound(" in src, "★나가는 payload 를 안 부른다"
        for gone in ("terms_native", "language_lock_native",
                     "build_directive"):
            assert gone not in src, f"★{gone} 를 손으로 나열한다"

    def test_post_processing_wording_does_not_move_it(self):
        """★파서·감사 문구를 고친 것만으로 **이미 산 것을 다시 사면** 안 된다."""
        base = _id()
        assert _id({**T, "note": "감사용 설명이 붙었다"}) == base
        assert _id({**T, "local_id": "c9#9"}) == base


class TestTheSendAndTheIdentitySeeTheSameThing:
    """★두 벌로 만들면 한쪽만 고쳐진다 — 한 함수가 둘을 다 만든다."""

    def test_write_brief_sends_exactly_what_the_identity_folds(self,
                                                               monkeypatch):
        seen = {}

        def _fake(tag, system, user, schema, **kw):
            seen.update(system=system, user=user, schema=schema,
                        model=(kw.get("project_config") or {}
                               ).get(tag, {}).get("model"))
            return {"source_language": "ko",
                    "search_directive_native": "찾아라" * 20,
                    "search_terms_native": ["가", "나", "다"],
                    "language_lock_native": "원어로만"}

        import app.modules.llm.llm_client as lc
        monkeypatch.setattr(lc, "call_structured", _fake)
        rc._write_brief(T, world_facts=CTX["world_facts"],
                        source_text=CTX["source_text"], narrow=False,
                        era_declaration=CTX["era_declaration"],
                        region_declaration=CTX["region_declaration"])
        want = rc.brief_outbound(T, narrow=False, **CTX)
        assert seen["system"] == want["system"]
        assert seen["user"] == want["user"]
        assert seen["schema"] == want["schema"]
        assert seen["model"] == want["model"]


class TestTheRunnerReusesInsteadOfRebuying:
    def test_it_looks_the_journal_up_before_acquiring(self):
        src = inspect.getsource(rc.main)
        i = src.index("ref_jr.get(ident)")
        j = src.index("rar.acquire_one")
        assert i < j, "★사고 나서 장부를 본다"

    def test_an_interrupted_buy_is_marked_uncertain(self):
        """★「보냈는데 답을 못 받은 것」은 **샀을 수도 있다**."""
        src = inspect.getsource(rc.main)
        assert 'status="uncertain"' in src
        assert "ref_jr.assert_no_uncertain()" in src, \
            "★앞 판의 미확정을 안 보고 다시 산다"

    def test_the_record_file_is_merged_not_overwritten(self):
        """★★일부만 다시 돌렸을 때 **앞서 산 것을 잃지 않는다**."""
        src = inspect.getsource(rc.main)
        assert "merged" in src and "prev" in src
        assert "앞 기록을 못 읽는다" in src, \
            "★못 읽는 것을 빈 것으로 읽으면 덮어써 잃는다"

    def test_the_journal_is_separate_from_the_chunk_one(self):
        assert rc.REF_JOURNAL_NAME != "_chunk_journal.json"


class TestTheSkippedPartsSurviveInTheRecord:
    def test_the_record_carries_what_was_not_bought(self):
        src = inspect.getsource(rc.main)
        assert '"parts_skipped": skipped' in src

    def test_the_journal_counts_are_recorded(self):
        src = inspect.getsource(rc.main)
        tree = ast.parse(src)
        keys = {n.value for n in ast.walk(tree)
                if isinstance(n, ast.Constant) and isinstance(n.value, str)}
        assert "journal" in keys and "reused" in keys
