"""받아 둔 후보 **전부**가 사람 검토 입력이 되고, 사람이 verified 한 후보가 `chosen` 으로 오른다
(Codex BLOCK 3, 2026-09-02 밤).

앞 판은 모델(VLM)이 고른 한 장만 사람 앞에 갔다 — `unsure` 하나면 후보 전부가 사람 눈에 안 닿았고
그대로 `reference_unavailable` 이 끝이었다(O01 넉 장). 이제 모델 선택은 **표시**다.
"""
from __future__ import annotations

import hashlib
from pathlib import Path

from app.modules.pipeline import grounding_fidelity_review as fr
from app.modules.pipeline import reference_acquisition as ra


def _photo(root: Path, rel: str) -> None:
    f = root / rel
    f.parent.mkdir(parents=True, exist_ok=True)
    f.write_bytes(b"\x89PNG" + hashlib.sha256(rel.encode()).digest())


def _sha_of(root: Path):
    from app.modules.pipeline.grounding_sidecar_writer import row_content_sha256
    return lambda row: row_content_sha256(row, root=root)


def _row(root: Path, *, chosen_index=None, status="no_match_after_retry"):
    cands = [{"index": i, "path": f"ref/rs_x_r1_0{i}.png", "url": f"https://x.invalid/{i}.jpg",
              "source_website_url": f"https://site.invalid/{i}"} for i in (1, 2, 3)]
    for c in cands:
        _photo(root, c["path"])
    chosen = dict(cands[chosen_index - 1]) if chosen_index else None
    return {"research_subject_id": "rs_x", "identity": "id_x",
            "outcome": ra.STATUS_SELECTED if chosen else ra.STATUS_UNAVAILABLE,
            "ledger_row": {"owner_type": "location", "final_id": "L01", "purpose": "context"},
            "acquisition": {"status": ra.STATUS_SELECTED if chosen else status, "chosen": chosen,
                            "chosen_path": chosen["path"] if chosen else None,
                            "rounds": [{"round_no": 1, "queries": [], "eligible": [chosen_index] if chosen_index else [],
                                        "downloaded_candidates": cands}]}}


def _cp(row):
    return {"data": {"rows": [row]}}


def _review(h, verdict, rid="r1"):
    return {"id": rid, "review_input_hash": h, "verdict": verdict, "reviewed_at": "2026-09-02T23:00:00+09:00",
            "reviewer_actor": "tester"}


COORDS = {"era": "", "region": ""}


def test_an_unchosen_row_still_puts_every_downloaded_photo_in_front_of_a_person(tmp_path):
    got = fr.review_inputs_for(_cp(_row(tmp_path)), project_id="p", episode_id="e",
                               coordinates=COORDS, sha_of=_sha_of(tmp_path))
    assert len(got) == 3 and len({x["hash"] for x in got}) == 3
    assert [x["model_pick"] for x in got] == [False, False, False]
    assert all(x["payload"]["image_sha256"] for x in got), "★사진 bytes 로 신원을 만든다"


def test_a_person_verifying_one_candidate_promotes_it_to_chosen(tmp_path):
    row = _row(tmp_path)
    inputs = fr.review_inputs_for(_cp(row), project_id="p", episode_id="e", coordinates=COORDS, sha_of=_sha_of(tmp_path))
    second = next(x for x in inputs if x["candidate"]["index"] == 2)
    got = fr.apply_reviews(_cp(row), [_review(second["hash"], fr.VERDICT_VERIFIED)],
                           project_id="p", episode_id="e", coordinates=COORDS, sha_of=_sha_of(tmp_path))
    r = got["data"]["rows"][0]
    assert r["outcome"] == ra.STATUS_SELECTED and r["acquisition"]["status"] == ra.STATUS_SELECTED
    assert r["acquisition"]["chosen"]["path"] == "ref/rs_x_r1_02.png"
    assert r["acquisition"]["chosen_by"] == "human" and r["acquisition"]["model_pick"] is None
    assert r["grounding_fidelity"]["state"] == ra.FIDELITY_VERIFIED
    assert ra.usable_as_reference(r), "★하류가 참조로 써야 한다 — 모델이 no_match 라 했어도"


def test_the_model_pick_is_marked_and_a_person_can_override_it(tmp_path):
    row = _row(tmp_path, chosen_index=1)
    inputs = fr.review_inputs_for(_cp(row), project_id="p", episode_id="e", coordinates=COORDS, sha_of=_sha_of(tmp_path))
    assert [x["model_pick"] for x in inputs] == [True, False, False]
    third = next(x for x in inputs if x["candidate"]["index"] == 3)
    got = fr.apply_reviews(_cp(row), [_review(third["hash"], fr.VERDICT_VERIFIED)],
                           project_id="p", episode_id="e", coordinates=COORDS, sha_of=_sha_of(tmp_path))
    r = got["data"]["rows"][0]
    assert r["acquisition"]["chosen"]["path"] == "ref/rs_x_r1_03.png"
    assert r["acquisition"]["model_pick"]["path"] == "ref/rs_x_r1_01.png", "★모델이 골랐던 것은 지우지 않는다"


def test_rejecting_the_model_pick_with_nothing_else_verified_is_rejected(tmp_path):
    row = _row(tmp_path, chosen_index=1)
    inputs = fr.review_inputs_for(_cp(row), project_id="p", episode_id="e", coordinates=COORDS, sha_of=_sha_of(tmp_path))
    pick = next(x for x in inputs if x["model_pick"])
    got = fr.apply_reviews(_cp(row), [_review(pick["hash"], fr.VERDICT_REJECTED)],
                           project_id="p", episode_id="e", coordinates=COORDS, sha_of=_sha_of(tmp_path))
    r = got["data"]["rows"][0]
    assert r["grounding_fidelity"]["state"] == ra.FIDELITY_REJECTED
    assert r["acquisition"]["chosen"]["path"] == "ref/rs_x_r1_01.png" and not ra.usable_as_reference(r)


def test_two_verified_candidates_auto_picks_one(tmp_path):
    """★HITL 0 (2026-09-03): 맞다 둘 · 선택 없음 → 다시 묻지 않는다."""
    row = _row(tmp_path)
    inputs = fr.review_inputs_for(_cp(row), project_id="p", episode_id="e", coordinates=COORDS, sha_of=_sha_of(tmp_path))
    got = fr.apply_reviews(_cp(row), [_review(inputs[0]["hash"], fr.VERDICT_VERIFIED, "r1"),
                                      _review(inputs[1]["hash"], fr.VERDICT_VERIFIED, "r2")],
                           project_id="p", episode_id="e", coordinates=COORDS, sha_of=_sha_of(tmp_path))
    r = got["data"]["rows"][0]
    assert r["grounding_fidelity"]["state"] == ra.FIDELITY_VERIFIED
    assert r["outcome"] == ra.STATUS_SELECTED
    assert r["acquisition"]["chosen"]["path"] in {inputs[0]["candidate"]["path"], inputs[1]["candidate"]["path"]}


def _selection(h, sid="rs_x", rid="s1", at="2026-09-02T23:10:00+09:00", supersedes=None):
    return {"id": rid, "research_subject_id": sid, "review_input_hash": h, "selected_at": at,
            "selected_by": "tester", "supersedes_id": supersedes}


def test_two_verified_plus_an_explicit_selection_promotes_the_selected_one(tmp_path):
    """★판정은 후보마다(둘 다 맞다가 옳다) · 쓸 사진은 선택 하나 (Codex 재리뷰 2026-09-02 밤)."""
    row = _row(tmp_path)
    inputs = fr.review_inputs_for(_cp(row), project_id="p", episode_id="e", coordinates=COORDS, sha_of=_sha_of(tmp_path))
    a, b = inputs[0], inputs[1]
    reviews = [_review(a["hash"], fr.VERDICT_VERIFIED, "r1"), _review(b["hash"], fr.VERDICT_VERIFIED, "r2")]
    got = fr.apply_reviews(_cp(row), reviews, selections=[_selection(b["hash"])],
                           project_id="p", episode_id="e", coordinates=COORDS, sha_of=_sha_of(tmp_path))
    r = got["data"]["rows"][0]
    assert r["acquisition"]["chosen"]["path"] == b["candidate"]["path"] and ra.usable_as_reference(r)
    assert r["grounding_fidelity"]["selection_id"] == "s1"


def test_choosing_a_then_b_leaves_only_b(tmp_path):
    row = _row(tmp_path)
    inputs = fr.review_inputs_for(_cp(row), project_id="p", episode_id="e", coordinates=COORDS, sha_of=_sha_of(tmp_path))
    a, b = inputs[0], inputs[1]
    reviews = [_review(a["hash"], fr.VERDICT_VERIFIED, "r1"), _review(b["hash"], fr.VERDICT_VERIFIED, "r2")]
    sels = [_selection(a["hash"], rid="s1", at="2026-09-02T23:10:00+09:00"),
            _selection(b["hash"], rid="s2", at="2026-09-02T23:11:00+09:00", supersedes="s1")]
    got = fr.apply_reviews(_cp(row), reviews, selections=sels,
                           project_id="p", episode_id="e", coordinates=COORDS, sha_of=_sha_of(tmp_path))
    r = got["data"]["rows"][0]
    assert r["acquisition"]["chosen"]["path"] == b["candidate"]["path"]
    assert fr.effective_selection(sels, subject_id="rs_x")["id"] == "s2"


def test_selecting_an_unverified_or_rejected_photo_does_not_attach(tmp_path):
    row = _row(tmp_path)
    inputs = fr.review_inputs_for(_cp(row), project_id="p", episode_id="e", coordinates=COORDS, sha_of=_sha_of(tmp_path))
    a, b = inputs[0], inputs[1]
    got = fr.apply_reviews(_cp(row), [_review(a["hash"], fr.VERDICT_VERIFIED)], selections=[_selection(b["hash"])],
                           project_id="p", episode_id="e", coordinates=COORDS, sha_of=_sha_of(tmp_path))
    r = got["data"]["rows"][0]
    assert not ra.usable_as_reference(r) and "아직 verified" in r["grounding_fidelity"]["fault"]
    got = fr.apply_reviews(_cp(row), [_review(b["hash"], fr.VERDICT_REJECTED)], selections=[_selection(b["hash"])],
                           project_id="p", episode_id="e", coordinates=COORDS, sha_of=_sha_of(tmp_path))
    assert "rejected" in got["data"]["rows"][0]["grounding_fidelity"]["fault"]


def test_the_digest_moves_when_the_selection_moves():
    r = [{"id": "r1", "review_input_hash": "h1", "verdict": "verified"}, {"id": "r2", "review_input_hash": "h2", "verdict": "verified"}]
    d0 = fr.reviews_digest(r)
    d1 = fr.reviews_digest(r, [_selection("h1", rid="s1")])
    d2 = fr.reviews_digest(r, [_selection("h1", rid="s1"), _selection("h2", rid="s2", at="2026-09-02T23:59:00+09:00", supersedes="s1")])
    assert len({d0, d1, d2}) == 3


def test_no_decision_yet_is_unverified_and_the_pipeline_is_not_blocked(tmp_path):
    row = _row(tmp_path)
    got = fr.apply_reviews(_cp(row), [], project_id="p", episode_id="e", coordinates=COORDS, sha_of=_sha_of(tmp_path))
    r = got["data"]["rows"][0]
    assert r["grounding_fidelity"]["state"] == ra.FIDELITY_UNVERIFIED and "안 골랐다" in r["grounding_fidelity"]["fault"]
    assert not ra.usable_as_reference(r)


def test_the_page_shows_every_candidate_with_the_model_mark_only(tmp_path):
    from tools.grounding_audit import fidelity_review_page as pg

    row = _row(tmp_path, chosen_index=2)
    inputs = fr.review_inputs_for(_cp(row), project_id="p", episode_id="e", coordinates=COORDS, sha_of=_sha_of(tmp_path))
    cards = pg.cards_from(inputs, key_of=lambda x: x["hash"])
    assert len(cards) == 3 and [c["candidate"]["model_pick"] for c in cards] == [False, True, False]
    html = pg.render(cards, api_base="/api", image_base="/img?key=", coords=COORDS)
    # ★뒤집음 (2026-09-03 HITL 0 · 대상당 한 물음): 후보마다 카드·단추 셋이 아니라 **라디오 하나**, 기계가 고른 장은 표시만
    import re
    # 후보 표시줄(r<라운드>#<번째> · 기계가 고른 장)에만 — 문안은 JS 안내문에도 있으니 표시줄 꼴로 센다
    assert len(re.findall(r"r\d+#\d+ · 기계가 고른 장", html)) == 1
    assert html.count('type="radio"') == 3 + 1                                          # 후보 3 + 없음
