"""중앙 조사 결과 → **하류가 읽는 한 벌**. ★유료 0.

Codex 2026-09-01: 「중앙 rows 의 `final_id`/raw status/outcome 을 소비할 때
내부 dict 를 여기서 다시 해석하지 마십시오. 공개 projection 하나가 검증된
`{final_id, raw status, outcome, disposition}` 을 내고 정책이 그것만 읽게
하십시오.」

★★줄을 **손으로 만들지 않는다** — 실제 `ca.run()` 이 낸 것을 그대로 넣는다.
앞서 제 probe 가 `{"status": "bound", "acquisition_outcome": …}` 라는 **없는
모양**을 지어냈다. 그 모양으로 시험을 짜면 실물이 안 도는 것을 통과시킨다.
"""
from __future__ import annotations

import pytest

from app.modules.pipeline import grounding_central_acquisition as ca
from app.modules.pipeline import grounding_outlook_binding as ob
from app.modules.pipeline import reference_acquisition as ra

from tests.grounding.test_central_acquisition import P3, _row, _Spy


@pytest.fixture
def journal(tmp_path):
    from app.modules.pipeline import grounding_chunk_journal as cj

    return cj.ChunkJournal(tmp_path / "j.json", contract={"v": 1})


def _cp(result):
    """실제 `run()` 결과를 **스텝 CP 모양**으로. ★`central_wrap` 과 같은 자리."""
    return {"data": {"rows": result["rows"]}}


def _live(journal, tmp_path, rows=("rs1",), **kw):
    spy = _Spy()
    led = ob.bind([_row(r) for r in rows], P3)
    got = ca.run(led, journal=journal, cap=9, workdir=tmp_path,
                 rel_root=tmp_path, search=spy.search, download=spy.download,
                 judge=spy.judge, **kw)
    return got, spy


class TestItReadsWhatProductionActuallyWrites:

    def test_a_selected_row_carries_its_final_id(self, journal, tmp_path):
        got, _spy = _live(journal, tmp_path)
        proj = ca.acquisition_projection(_cp(got))
        assert len(proj) == len(got["rows"])
        picked = [p for p in proj if p["outcome"] == ra.STATUS_SELECTED]
        assert picked, "★고른 줄이 없다 — 시험이 죽었다"
        assert all(p["final_id"] for p in picked)
        assert all(p["owner_type"] for p in picked)

    def test_every_field_is_declared(self, journal, tmp_path):
        got, _spy = _live(journal, tmp_path)
        for p in ca.acquisition_projection(_cp(got)):
            assert set(p) == set(ca.PROJECTION_FIELDS), (
                "★투영이 선언 밖의 칸을 낸다 — 하류가 그것을 읽기 시작한다")

    def test_no_checkpoint_is_the_old_road(self):
        """★없으면 **빈 목록**이다 — 지어내지 않는다."""
        assert ca.acquisition_projection(None) == []
        assert ca.acquisition_projection({"data": {}}) == []


class TestTheThreeAxesStaySeparate:
    """★`selected` · 못 구함 · 비대상을 **한 칸으로 뭉개지 않는다**."""

    def test_a_capped_run_is_unavailable_not_absent(self, journal, tmp_path):
        got, _spy = _live(journal, tmp_path, rows=("rs1", "rs2"), )
        # ★상한을 0 으로 둔 판을 따로 돌려 **못 구함**을 만든다
        from app.modules.pipeline import grounding_chunk_journal as cj

        j2 = cj.ChunkJournal(tmp_path / "j2.json", contract={"v": 1})
        spy = _Spy()
        led = ob.bind([_row("rs9")], P3)
        capped = ca.run(led, journal=j2, cap=0, workdir=tmp_path,
                        rel_root=tmp_path, search=spy.search,
                        download=spy.download, judge=spy.judge)
        proj = ca.acquisition_projection(_cp(capped))
        assert [p["outcome"] for p in proj] == [ra.STATUS_UNAVAILABLE]
        assert proj[0]["disposition"] == ca.DISP_CAP_REACHED
        assert proj[0]["why_unbought"] == ca.WHY_CAP_REACHED, (
            "★raw 사유가 사라졌다 — 왜 없는지 못 읽는다")
        assert proj[0]["status"] == ra.STATUS_RETRYABLE

    def test_a_not_applicable_row_has_no_outcome_at_all(self):
        """★비대상은 **결과가 없다** — 「못 구했다」로 세면 검색 실패로 보인다.

        ★★줄은 실제 `_passthrough` 가 만든다 — 모양을 손으로 안 짓는다.
        """
        made = ca._passthrough({"research_subject_id": "rsX",
                                "owner_type": "prop", "final_id": "P01"},
                               disposition=ca.DISP_NOT_APPLICABLE,
                               why="애초에 살 것이 아니다")
        proj = ca.acquisition_projection({"data": {"rows": [made]}})
        assert proj[0]["outcome"] is None
        assert proj[0]["status"] is None
        assert proj[0]["disposition"] == ca.DISP_NOT_APPLICABLE


class TestItStandsInsteadOfPassingAHalfTruth:

    def test_an_unknown_disposition_stops(self):
        proj = {"data": {"rows": [{"research_subject_id": "r",
                                   "disposition": "새로운 것"}]}}
        with pytest.raises(ca.ProjectionContractError):
            ca.acquisition_projection(proj)

    def test_an_unknown_raw_status_stops(self):
        """★새 enum 값이 옛 술어를 조용히 지나가지 않게."""
        made = ca._passthrough({"research_subject_id": "r",
                                "owner_type": "prop", "final_id": "P01"},
                               disposition=ca.DISP_SKIPPED, why="")
        made["status"] = "처음 보는 상태"
        with pytest.raises(ca.ProjectionContractError):
            ca.acquisition_projection({"data": {"rows": [made]}})

    def test_selected_without_a_final_id_stops(self):
        """★★붙일 신원이 없는데 「참조가 있다」로 내려보내지 않는다."""
        made = ca._passthrough({"research_subject_id": "r",
                                "owner_type": "prop", "final_id": ""},
                               disposition=ca.DISP_ACQUIRED, why="")
        made["status"] = ra.STATUS_SELECTED
        made["outcome"] = ra.STATUS_SELECTED
        with pytest.raises(ca.ProjectionContractError):
            ca.acquisition_projection({"data": {"rows": [made]}})

    def test_a_status_on_a_non_target_stops(self):
        made = ca._passthrough({"research_subject_id": "r",
                                "owner_type": "prop", "final_id": "P01"},
                               disposition=ca.DISP_NOT_APPLICABLE, why="")
        made["status"] = ra.STATUS_RETRYABLE
        with pytest.raises(ca.ProjectionContractError):
            ca.acquisition_projection({"data": {"rows": [made]}})
