"""GROUNDING-V2 §4a — claims / gaps 계약.

★계약 §3: **검색 실패는 `no` 가 아니라 `unresolved`** 다.
`no` 는 「차이가 없다」는 **긍정적 출처**가 있을 때만 나온다.
"""
import hashlib

import pytest

from app.modules.pipeline.grounding_claims import (
    CLAIM_FACT,
    CLAIM_PROHIBITION,
    DELTA_NO,
    DELTA_UNRESOLVED,
    DELTA_YES,
    GAP_CONFLICT,
    GAP_NOT_FOUND,
    GAP_NO_SOURCE,
    GAP_TIME_CAPPED,
    LIMIT_ADMISSION,
    DELTA_EFFECT_DIFF,
    DELTA_EFFECT_NEUTRAL,
    DELTA_EFFECT_NO_DIFF,
    build_gap,
    claim_id,
    decide_delta,
    validate_claim,
)

SID = "rs_abc"
KW = dict(era="1983년", region="대한민국",
          claims_pack_version="claims/1.0", policy_version="pol_v1")


def _ok(**over):
    raw = {"statement_native": "1983년 승차권은 등사 인쇄였다",
           "required": True, "sources": ["https://example.org/a"],
           "kind": CLAIM_FACT, "delta_effect": DELTA_EFFECT_DIFF, **over}
    c, why = validate_claim(raw, subject_id=SID)
    assert c is not None, why
    return c


class TestASourcelessRequiredClaimIsNotAClaim:
    """★그걸 통과시키면 「조사했다」가 거짓이 된다 (계획 §2-4 통과 조건)."""

    def test_required_without_sources_is_refused(self):
        c, why = validate_claim(
            {"statement_native": "x", "required": True, "sources": []},
            subject_id=SID)
        assert c is None and "출처 없는 required" in why

    def test_optional_without_sources_is_allowed_but_kept_optional(self):
        c, _ = validate_claim(
            {"statement_native": "x", "required": False, "sources": []},
            subject_id=SID)
        assert c is not None and c["required"] is False and c["sources"] == []

    @pytest.mark.parametrize("bad", [None, "", "  ", "\n"])
    def test_a_blank_statement_is_refused(self, bad):
        c, _ = validate_claim({"statement_native": bad, "required": True,
                               "sources": ["https://u.org/a"]}, subject_id=SID)
        assert c is None

    @pytest.mark.parametrize("bad", ["true", 1, 0, None, "yes"])
    def test_required_must_be_a_real_bool(self, bad):
        """★`bool("false")` 가 True 인 부류의 사고를 미리 막는다."""
        c, why = validate_claim({"statement_native": "x", "required": bad,
                                 "sources": ["https://u.org/a"]}, subject_id=SID)
        assert c is None and "bool" in why

    def test_an_unknown_kind_is_refused(self):
        c, why = validate_claim({"statement_native": "x", "required": True,
                                 "sources": ["https://u.org/a"], "kind": "guess"},
                                subject_id=SID)
        assert c is None and "enum 밖" in why


class TestClaimIdIsStableAcrossBatching:
    """★batch 크기를 바꿔도 같은 id 여야 progressive admission 이 성립한다."""

    def test_the_same_statement_gives_the_same_id(self):
        assert claim_id(SID, "가 나 다") == claim_id(SID, " 가  나  다 ")

    def test_a_different_subject_gives_a_different_id(self):
        assert claim_id("rs_1", "x") != claim_id("rs_2", "x")

    def test_a_prohibition_is_not_the_same_claim_as_the_fact(self):
        assert claim_id(SID, "x") != claim_id(SID, "x", kind=CLAIM_PROHIBITION)

    def test_the_id_does_not_depend_on_call_order_or_batch(self):
        ids = [claim_id(SID, f"문장 {i}") for i in range(5)]
        assert ids == [claim_id(SID, f"문장 {i}") for i in reversed(range(5))][::-1]


class TestNotFoundIsNeverNo:
    """★★계약 §3 — 「검색에서 아무것도 안 나옴 → A=no」는 **금지**다."""

    @pytest.mark.parametrize("reason", [GAP_NOT_FOUND, GAP_NO_SOURCE,
                                        GAP_CONFLICT, GAP_TIME_CAPPED])
    def test_every_gap_reason_lands_on_unresolved_not_no(self, reason):
        # ★`time_capped` 만 `limit_kind` 를 요구한다 — 그게 계약이다.
        extra = ({"limit_kind": LIMIT_ADMISSION}
                 if reason == GAP_TIME_CAPPED else {})
        d, _ = decide_delta([], [build_gap(SID, reason=reason, query="q",
                                           **extra)], subject_id=SID)
        assert d == DELTA_UNRESOLVED

    def test_nothing_at_all_is_unresolved(self):
        d, why = decide_delta([], [], subject_id=SID)
        assert d == DELTA_UNRESOLVED
        assert "「차이 없음」으로 안 읽는다" in why["why"]

    def test_a_conflict_beats_a_sourced_claim(self):
        """★출처가 서로 어긋나면 **모른다**다 — 한쪽을 골라 주지 않는다."""
        d, _ = decide_delta([_ok()], [build_gap(SID, reason=GAP_CONFLICT)], subject_id=SID)
        assert d == DELTA_UNRESOLVED

    def test_a_time_cap_beats_a_sourced_claim(self):
        """★상한에 걸린 판을 「다 봤다」로 세면 통과가 거짓이 된다."""
        d, _ = decide_delta([_ok()], [build_gap(SID, reason=GAP_TIME_CAPPED,
                       limit_kind=LIMIT_ADMISSION)], subject_id=SID)
        assert d == DELTA_UNRESOLVED

    def test_an_unknown_gap_reason_is_refused(self):
        with pytest.raises(ValueError):
            build_gap(SID, reason="몰라")


class TestYesNeedsASourcedRequiredClaim:
    def test_one_sourced_required_fact_is_enough(self):
        d, why = decide_delta([_ok()], subject_id=SID)
        assert d == DELTA_YES and why["fact"] == 1

    def test_a_prohibition_also_counts(self):
        """★「이건 이 시대에 없었다」가 그림을 더 많이 고친다 (계약 §6)."""
        d, why = decide_delta([_ok(kind=CLAIM_PROHIBITION,
                                   statement_native="플라스틱 카드는 없었다")],
                              subject_id=SID)
        assert d == DELTA_YES and why["prohibition"] == 1

    def test_optional_claims_alone_do_not_make_yes(self):
        c, _ = validate_claim({"statement_native": "곁가지", "required": False,
                               "sources": ["https://u.org/a"]}, subject_id=SID)
        assert decide_delta([c], subject_id=SID)[0] == DELTA_UNRESOLVED

    def test_a_sourceless_required_claim_sneaking_in_is_caught(self):
        """★`validate_claim` 을 안 거친 것이 들어와도 막는다."""
        d, why = decide_delta([{**_ok(), "sources": []}], subject_id=SID)
        assert d == DELTA_UNRESOLVED and "계약 위반" in why["why"]


class TestNoNeedsPositiveEvidence:
    """★`no` 는 「차이가 없다」를 **출처가 지지할 때만**이다."""

    def test_a_sourced_no_delta_claim_gives_no(self):
        c, _ = validate_claim(
            {"statement_native": "이 종류는 시대에 따라 안 변했다",
             "required": False, "sources": ["https://example.org/b"],
             "delta_effect": DELTA_EFFECT_NO_DIFF}, subject_id=SID)
        assert decide_delta([c], subject_id=SID)[0] == DELTA_NO

    def test_an_unsourced_no_delta_claim_is_refused_outright(self):
        c, _ = validate_claim(
            {"statement_native": "안 변했을 것이다", "required": False,
             "sources": [], "delta_effect": DELTA_EFFECT_NO_DIFF}, subject_id=SID)
        assert c is None, "출처 없는 「차이 없음」은 애초에 안 선다"

    def test_both_effects_together_are_unresolved(self):
        """★한쪽을 골라 주지 않는다 — 계약의 「충돌은 unresolved」."""
        nod, _ = validate_claim(
            {"statement_native": "안 변했다", "required": False,
             "sources": ["https://u.org/a"], "delta_effect": DELTA_EFFECT_NO_DIFF},
            subject_id=SID)
        assert decide_delta([_ok(), nod],
                            subject_id=SID)[0] == DELTA_UNRESOLVED


from app.modules.pipeline.grounding_claims import (  # noqa: E402
    STATUS_COMPLETED,
    STATUS_RETRYABLE,
    STATUS_UNRESOLVED_TERMINAL,
    INPUT_IDENTITY_FIELDS,
    plan_admission,
    research_input_hash,
)

#: ★hash 칸은 **실제 hash 모양**이어야 한다. 전에는 `"ph1"` 같은 자리끼움을
#:  썼고, 그래서 자리끼움이 통과하는 결함을 시험이 **정답으로 못박고 있었다**.
def _h(seed: str) -> str:
    return hashlib.sha256(seed.encode()).hexdigest()[:16]


RI = dict(era="1983년", region="대한민국", claims_pack_version="claims/1.0",
          prompt_raw_hash=_h("prompt"), schema_raw_hash=_h("schema"),
          policy_version="pol_v1", policy_contract_hash=_h("policy"),
          provider="openai", model="gpt-5.6-sol", model_version="1")
PAY = {f"rs_{c}": _h(f"pay_{c}") for c in "abcdez"}
PAY.update({f"rs_{i:02}": _h(f"pay_{i:02}") for i in range(12)})
KW = dict(subject_payload_hashes=PAY, research_inputs=RI)


def _rev(sid, **over):
    return research_input_hash(research_subject_id=sid,
                               subject_payload_hash=PAY[sid],
                               **{**RI, **over})


def _row(sid, status=STATUS_COMPLETED, **over):
    return {"research_subject_id": sid,
            "research_input_hash": _rev(sid, **over), "status": status}


class TestAdmissionIsDeterministicAndResumable:
    """★상한만 두고 이어가는 계약이 없으면 **영구 partial** 이 된다 — 매번
    앞의 것만 사고 뒤는 영원히 안 산다 (Codex).
    """

    IDS = ["rs_c", "rs_a", "rs_e", "rs_b", "rs_d"]

    def test_the_order_does_not_depend_on_input_order(self):
        a = plan_admission(self.IDS, **KW, cap=3)
        b = plan_admission(list(reversed(self.IDS)), **KW, cap=3)
        assert a["admitted"] == b["admitted"] == ["rs_a", "rs_b", "rs_c"]

    def test_completed_ones_are_never_rebought(self):
        got = plan_admission(self.IDS, **KW, done_rows=[_row("rs_a"), _row("rs_b")], cap=2)
        assert got["admitted"] == ["rs_c", "rs_d"]
        assert got["already_done"] == ["rs_a", "rs_b"]

    def test_the_next_run_continues_from_pending(self):
        """★첫 판이 3개를 샀으면 다음 판은 **그 뒤부터** 이어간다."""
        first = plan_admission(self.IDS, **KW, cap=3)
        second = plan_admission(self.IDS, **KW, done_rows=[_row(i) for i in first["admitted"]], cap=3)
        assert second["admitted"] == ["rs_d", "rs_e"]
        assert set(first["admitted"]) & set(second["admitted"]) == set()

    def test_two_runs_cover_everything(self):
        first = plan_admission(self.IDS, **KW, cap=3)
        second = plan_admission(self.IDS, **KW, done_rows=[_row(i) for i in first["admitted"]], cap=3)
        assert sorted(first["admitted"] + second["admitted"]) == sorted(self.IDS)
        assert second["time_capped_count"] == 0

    def test_the_overflow_is_counted_not_silently_dropped(self):
        got = plan_admission(self.IDS, **KW, cap=2)
        assert got["capped"] == ["rs_c", "rs_d", "rs_e"]
        assert got["time_capped_count"] == 3

    def test_a_capped_run_is_not_an_acceptance_baseline(self):
        """★「상한에 걸렸다」를 「다 봤다」로 세면 통과가 거짓이 된다."""
        assert plan_admission(self.IDS, **KW, cap=2)["is_acceptance_eligible"] is False
        assert plan_admission(self.IDS, **KW, cap=99)["is_acceptance_eligible"] is True
        assert plan_admission(self.IDS, **KW)["is_acceptance_eligible"] is True

    def test_duplicate_ids_are_bought_once(self):
        got = plan_admission(["rs_a", "rs_a", "rs_b"], **KW)
        assert got["admitted"] == ["rs_a", "rs_b"]


class TestBatchingOnlySlicesIt:
    """★batch 는 자르기만 한다 — 무엇을 사는지도, id 도 안 바꾼다."""

    IDS = [f"rs_{i:02}" for i in range(9)]

    @pytest.mark.parametrize("bs", [1, 2, 4, 8, 12])
    def test_whatever_the_batch_size_the_same_subjects_are_bought(self, bs):
        got = plan_admission(self.IDS, **KW, batch_size=bs)
        assert got["admitted"] == sorted(self.IDS)
        assert [s for b in got["batches"] for s in b] == sorted(self.IDS)

    @pytest.mark.parametrize("bs,want", [(1, 9), (2, 5), (4, 3), (8, 2), (12, 1)])
    def test_the_logical_call_count_is_ceil(self, bs, want):
        assert plan_admission(self.IDS, **KW, batch_size=bs)["logical_text_calls"] == want

    def test_no_subject_appears_in_two_batches(self):
        got = plan_admission(self.IDS, **KW, batch_size=4)
        flat = [s for b in got["batches"] for s in b]
        assert len(flat) == len(set(flat))

    def test_the_cap_is_applied_before_batching(self):
        """★팬아웃 **전**에 자른다. 뒤에 세면 재시도가 상한 밖으로 샌다."""
        got = plan_admission(self.IDS, **KW, cap=5, batch_size=4)
        assert len(got["admitted"]) == 5
        assert got["logical_text_calls"] == 2

    @pytest.mark.parametrize("bad", [0, -1])
    def test_a_bad_batch_size_is_refused(self, bad):
        with pytest.raises(ValueError):
            plan_admission(self.IDS, **KW, batch_size=bad)

    def test_a_zero_cap_buys_nothing_but_counts_everything(self):
        got = plan_admission(self.IDS, **KW, cap=0)
        assert got["admitted"] == [] and got["time_capped_count"] == 9
        assert got["is_acceptance_eligible"] is False


class TestDeltaEffectIsAnEnumNotAMagicString:
    """★자유 문자열에 특별한 값을 박고 그 글자로 뜻을 가르면, 오타·번역·다른
    표기 하나에 판정이 무너진다 (저장소 규칙: 글자로 의미 판단 금지).
    """

    def test_the_family_string_no_delta_does_nothing_by_itself(self):
        c, _ = validate_claim(
            {"statement_native": "안 변했다", "required": False,
             "sources": ["https://u.org/a"], "discriminator_family": "no_delta"},
            subject_id=SID)
        assert c["delta_effect"] == DELTA_EFFECT_NEUTRAL
        assert decide_delta([c], subject_id=SID)[0] == DELTA_UNRESOLVED

    @pytest.mark.parametrize("bad", ["true", "yes", "no_delta", "diff"])
    def test_an_effect_outside_the_enum_is_refused(self, bad):
        c, why = validate_claim(
            {"statement_native": "x", "required": False, "sources": ["https://u.org/a"],
             "delta_effect": bad}, subject_id=SID)
        assert c is None and "enum 밖" in why

    def test_the_decider_reads_the_field_not_the_string(self):
        import inspect

        from app.modules.pipeline import grounding_claims as gc

        src = inspect.getsource(gc.decide_delta)
        assert '"no_delta"' not in src, "글자로 뜻을 가른다"
        assert 'DELTA_EFFECT_' in src


class TestClaimsFromAnotherSubjectNeverDecideThisOne:
    """★batch 로 여러 subject 를 한 호출에 넣으므로, 받은 목록에 남의 행이
    섞여 들어오기 쉽다. 그대로 접으면 **한 subject 의 출처가 다른 subject 의
    route 를 정한다** (Codex).
    """

    def test_a_foreign_claim_blocks_instead_of_deciding(self):
        c, _ = validate_claim({"statement_native": "남의 것", "required": True,
                               "sources": ["https://u.org/a"]}, subject_id="rs_other")
        d, why = decide_delta([c], subject_id=SID)
        assert d == DELTA_UNRESOLVED and "다른 subject" in why["why"]

    def test_a_foreign_gap_also_blocks(self):
        g = build_gap("rs_other", reason=GAP_NOT_FOUND)
        d, why = decide_delta([], [g], subject_id=SID)
        assert d == DELTA_UNRESOLVED and "다른 subject" in why["why"]

    def test_it_does_not_silently_filter(self):
        """★조용히 걸러 내면 호출부 결함이 감춰진다 — 막고 알린다."""
        mine, _ = validate_claim(
            {"statement_native": "내 것", "required": True, "sources": ["https://u.org/a"],
             "delta_effect": DELTA_EFFECT_DIFF}, subject_id=SID)
        theirs, _ = validate_claim(
            {"statement_native": "남의 것", "required": True,
             "sources": ["https://u.org/a"], "delta_effect": DELTA_EFFECT_DIFF},
            subject_id="rs_other")
        assert decide_delta([mine, theirs], subject_id=SID)[0] == DELTA_UNRESOLVED
        assert decide_delta([mine], subject_id=SID)[0] == DELTA_YES

    def test_subject_id_is_required(self):
        with pytest.raises(TypeError):
            decide_delta([])


class TestCompletionIsBoundToTheResearchRevision:
    """★subject id 만으로 「완료」를 믿으면, claims 팩·시대·지역이 바뀌어도
    옛 조사가 영원히 살아남아 「조사했다」가 거짓이 된다 (Codex).

    ★그리고 판은 **subject 마다 다르다** — `content_hash` 안에
    `research_subject_id` 가 들어 있다. 값 하나를 전부에 대 보면 **두 대상이
    다 완료된 정상 상태를 표현할 수 없다** (Codex 재현).
    """

    IDS = ["rs_a", "rs_b", "rs_c"]

    def test_two_completed_subjects_are_both_recognised(self):
        """★이것이 값 하나로는 절대 안 되던 자리다."""
        got = plan_admission(
            self.IDS, **KW,
            done_rows=[_row("rs_a"), _row("rs_b")])
        assert got["already_done"] == ["rs_a", "rs_b"]
        assert got["admitted"] == ["rs_c"]
        assert got["stale_revision"] == []

    def test_one_subjects_hash_does_not_complete_another(self):
        got = plan_admission(self.IDS, **KW,
                             done_rows=[{"research_subject_id": "rs_b",
                                        "research_input_hash": _rev("rs_a"),
                                        "status": STATUS_COMPLETED}])
        assert got["already_done"] == []
        assert got["stale_revision"] == ["rs_b"]

    def test_a_stale_pack_makes_everyone_pending_again(self):
        got = plan_admission(self.IDS,
                             **{**KW, "research_inputs": {**RI, "claims_pack_version": "claims/2.0"}},
                             done_rows=[_row(i) for i in self.IDS])
        assert got["admitted"] == self.IDS
        assert got["stale_revision"] == self.IDS

    def test_the_expected_input_hashes_are_reported(self):
        got = plan_admission(self.IDS, **KW)
        assert set(got["expected_input_hashes"]) == set(self.IDS)
        assert got["expected_input_hashes"]["rs_a"] == _rev("rs_a")

    @pytest.mark.parametrize("field", list(INPUT_IDENTITY_FIELDS))
    def test_every_identity_field_changes_the_hash(self, field):
        """★하나라도 빠지면 그것이 바뀌어도 **옛 완료 행을 재사용**한다 —
        「조사했다」가 거짓이 된다 (Codex)."""
        if field == "research_subject_id":
            assert _rev("rs_a") != _rev("rs_b")
            return
        if field == "subject_payload_hash":
            assert _rev("rs_a") != research_input_hash(
                research_subject_id="rs_a",
                subject_payload_hash=_h("다른 원문"), **RI)
            return
        # ★hash 칸은 hash 모양이라야 통과한다 — 「다른값」으로는 모양 검증에
        #  걸려, 이 시험이 재려는 **신원 변화**를 못 재고 다른 이유로 죽는다.
        other = _h("다른값") if field.endswith("_hash") else "다른값"
        assert _rev("rs_a") != _rev("rs_a", **{field: other})

    @pytest.mark.parametrize("field", list(INPUT_IDENTITY_FIELDS))
    def test_a_blank_identity_field_is_refused(self, field):
        """★판을 안 주면 「무엇을 기준으로 완료인가」가 없다."""
        kw = {"research_subject_id": "rs_a",
              "subject_payload_hash": _h("p"), **RI}
        kw[field] = ""
        with pytest.raises(ValueError):
            research_input_hash(**kw)

    def test_a_missing_payload_hash_is_refused(self):
        """★원문이 바뀌었는지 모르면 「같은 조사인가」를 못 정한다."""
        with pytest.raises(ValueError):
            plan_admission(["rs_새것"], subject_payload_hashes={},
                           research_inputs=RI)

    def test_an_unknown_identity_field_is_refused(self):
        """★몰래 칸을 더하면 그 칸이 신원에 안 들어간 채로 통과한다."""
        with pytest.raises(ValueError):
            research_input_hash(research_subject_id="rs_a",
                                subject_payload_hash=_h("p"),
                                몰래="x", **RI)


class TestSourcesMustBeReachableAddresses:
    """★출처는 **되짚을 수 있어야** 한다. 아무 글자나면 「조사했다」가 거짓이 된다."""

    @pytest.mark.parametrize("bad", ["그냥 글자", "example.org", "ftp://a.org",
                                     "https://", "http://노슬래시",
                                     "https://a.org 여백"])
    def test_a_non_url_source_is_refused(self, bad):
        c, why = validate_claim(
            {"statement_native": "x", "required": True, "sources": [bad]},
            subject_id=SID)
        assert c is None and "URL" in why

    @pytest.mark.parametrize("ok", ["https://a.org", "http://a.co/b?c=1",
                                    "https://sub.a.or.kr/p/q"])
    def test_a_real_url_passes(self, ok):
        c, _ = validate_claim(
            {"statement_native": "x", "required": True, "sources": [ok]},
            subject_id=SID)
        assert c is not None and c["sources"] == [ok]


class TestTimeCappedIsNotDone:
    """★「상한에 걸렸다」를 「다 봤다」로 세면 통과가 거짓이 된다 (Codex).

    `unresolved` 하나로 뭉치면 **시간에 잘린 것**과 **다 보고도 못 정한 것**이
    같아진다. 앞의 것은 다음 판에서 다시 사야 한다.
    """

    def test_a_retryable_row_is_bought_again(self):
        got = plan_admission(["rs_a"], **KW,
                             done_rows=[_row("rs_a", STATUS_RETRYABLE)])
        assert got["admitted"] == ["rs_a"]
        assert got["already_done"] == []
        assert got["retryable"] == ["rs_a"]

    @pytest.mark.parametrize("st", [STATUS_COMPLETED,
                                    STATUS_UNRESOLVED_TERMINAL])
    def test_a_terminal_row_is_not_rebought(self, st):
        got = plan_admission(["rs_a"], **KW, done_rows=[_row("rs_a", st)])
        assert got["admitted"] == [] and got["already_done"] == ["rs_a"]

    def test_an_unknown_status_is_not_counted_as_done(self):
        """★모르는 상태를 완료로 세면 안 산 것이 샀다고 기록된다."""
        got = plan_admission(["rs_a"], **KW,
                             done_rows=[_row("rs_a", "몰라")])
        assert got["admitted"] == ["rs_a"]


class TestRowOrderNeverChangesTheAnswer:
    """★subject 하나를 hash 하나로 뭉개면 마지막 행이 앞 행을 덮어,
    **DB 가 돌려준 순서에 따라 재구매 여부가 바뀐다** (Codex).
    """

    def _rows(self):
        return [_row("rs_a", STATUS_COMPLETED),
                {"research_subject_id": "rs_a",
                 "research_input_hash": "옛판", "status": STATUS_COMPLETED},
                _row("rs_b", STATUS_RETRYABLE)]

    def test_every_permutation_gives_the_same_answer(self):
        import itertools

        outs = {tuple(plan_admission(["rs_a", "rs_b"], **KW,
                                     done_rows=list(p))["admitted"])
                for p in itertools.permutations(self._rows())}
        assert len(outs) == 1, outs
        assert outs == {("rs_b",)}

    def test_an_old_row_does_not_erase_the_current_one(self):
        got = plan_admission(["rs_a"], **KW, done_rows=[
            _row("rs_a", STATUS_COMPLETED),
            {"research_subject_id": "rs_a", "research_input_hash": "옛판",
             "status": STATUS_COMPLETED}])
        assert got["admitted"] == []

    def test_rows_missing_a_field_are_ignored(self):
        for bad in ({"research_subject_id": "rs_a", "status": "completed"},
                    {"research_input_hash": _rev("rs_a"),
                     "status": "completed"}):
            assert plan_admission(["rs_a"], **KW,
                                  done_rows=[bad])["admitted"] == ["rs_a"]


class TestRetryableIsNotStale:
    """★`retryable` 은 **기대 입력 hash 와 정확히 맞으면서 상태가 retryable
    인 것**만이다. 판이 다른 것(`stale`)을 섞으면 「다시 살 것」과 「판이
    바뀐 것」이 한 칸이 된다 (Codex).
    """

    def test_a_stale_row_is_stale_not_retryable(self):
        got = plan_admission(["rs_a"], **KW, done_rows=[
            {"research_subject_id": "rs_a", "research_input_hash": "옛판",
             "status": STATUS_COMPLETED}])
        assert got["stale_revision"] == ["rs_a"]
        assert got["retryable"] == []

    def test_a_retryable_row_is_retryable_not_stale(self):
        got = plan_admission(["rs_a"], **KW,
                             done_rows=[_row("rs_a", STATUS_RETRYABLE)])
        assert got["retryable"] == ["rs_a"]
        assert got["stale_revision"] == []

    def test_an_unknown_status_is_neither(self):
        got = plan_admission(["rs_a"], **KW, done_rows=[_row("rs_a", "몰라")])
        assert got["retryable"] == [] and got["admitted"] == ["rs_a"]

    def test_both_are_bought_again(self):
        for rows in ([{"research_subject_id": "rs_a",
                       "research_input_hash": "옛판",
                       "status": STATUS_COMPLETED}],
                     [_row("rs_a", STATUS_RETRYABLE)]):
            assert plan_admission(["rs_a"], **KW,
                                  done_rows=rows)["admitted"] == ["rs_a"]


class TestAnUnsettledIdentityIsNotAnIdentity:
    """★★**빈 칸만 막으면 부족하다** (Codex 실측).

    12칸을 전부 ``"unknown"`` 으로 채워도 hash 가 나왔다. 그러면 아무것도 모르는
    두 조사가 **같은 신원으로 접히고**, 그 하나가 완료되면 나머지가 전부 완료로
    읽힌다 — 「조사했다」가 거짓이 된다.

    ``"ph1"`` 같은 자리끼움도 마찬가지다. 자리끼움끼리는 늘 같아서 원문이
    바뀌어도 **영원히 같은 조사**가 된다. 그리고 그 결함을 **시험 fixture 가
    정답으로 못박고 있었다** — 이 파일의 `RI`·`PAY` 가 자리끼움이었다.
    """

    def _kw(self, **over):
        return {"research_subject_id": "rs_a",
                "subject_payload_hash": _h("pay"), **RI, **over}

    def test_the_baseline_still_hashes(self):
        """★positive control — 이게 빨개지면 아래 것들은 아무것도 안 잰다."""
        assert len(research_input_hash(**self._kw())) == 24

    @pytest.mark.parametrize("field", list(INPUT_IDENTITY_FIELDS))
    @pytest.mark.parametrize("sentinel", ["unknown", "none", "N/A", "TBD", "-"])
    def test_an_unsettled_value_is_refused(self, field, sentinel):
        with pytest.raises(ValueError, match="미확정"):
            research_input_hash(**self._kw(**{field: sentinel}))

    def test_all_twelve_unsettled_is_refused(self):
        """★Codex 가 실측한 그 입력 그대로."""
        with pytest.raises(ValueError):
            research_input_hash(**{k: "unknown"
                                   for k in INPUT_IDENTITY_FIELDS})

    @pytest.mark.parametrize("bad", ["ph1", "not-a-hash", "ABCDEF0123456789",
                                     "0123456", "g" * 16, "0123 4567 89ab"])
    def test_a_hash_field_must_look_like_a_hash(self, bad):
        """★`_hash` 라 이름 붙은 칸에 아무 글자나 들어가면 신원이 뜻을 잃는다."""
        with pytest.raises(ValueError, match="hash 모양"):
            research_input_hash(**self._kw(subject_payload_hash=bad))

    @pytest.mark.parametrize("bad", [None, 7, 1983, ["a"], {"a": 1}, True])
    def test_a_non_string_coordinate_is_refused(self, bad):
        """★`str()` 을 먹이면 dict·list 도 문자열이 되는데, 그 문자열은 원소
        순서에 따라 달라져 **신원이 흔들린다**."""
        with pytest.raises(ValueError, match="문자열이 아니다"):
            research_input_hash(**self._kw(era=bad))

    def test_a_normal_value_that_merely_contains_a_sentinel_word_survives(self):
        """★★글자가 **든 것**으로 뜻을 판단하지 않는다 — 정확히 그 값일 때만이다.

        부분 일치로 보면 `none-slip` 같은 실제 표기가 죽는다.
        """
        assert research_input_hash(**self._kw(region="none-slip 마을"))
        assert research_input_hash(**self._kw(era="unknown-era 이전"))

    def test_the_same_rule_guards_the_admission_path(self):
        """★★끝점 — admission 도 **같은 함수**를 지나야 한다.

        두 곳에 따로 적으면 한쪽만 고쳐진다. 여기서는 `plan_admission` 이
        `research_input_hash` 를 부르므로 규칙이 한 벌이다.
        """
        with pytest.raises(ValueError, match="hash 모양"):
            plan_admission(["rs_a"], subject_payload_hashes={"rs_a": "ph1"},
                           research_inputs=RI)
        with pytest.raises(ValueError, match="미확정"):
            plan_admission(["rs_a"], subject_payload_hashes={"rs_a": _h("p")},
                           research_inputs={**RI, "era": "unknown"})
