"""`uncertain` 줄은 사람이 「안 샀다」로만 매듭짓고, 그러면 자리가 풀린다. 재판정 lane 은 그 줄 때문에
스텝을 세우지 않는다 (실측 2026-09-02 밤, attempt 4d649ab6)."""
from __future__ import annotations

import pytest

from app.modules.pipeline import grounding_chunk_journal as cj


def _j(tmp_path):
    return cj.ChunkJournal(tmp_path / "j.json", contract={"acquisition": {"x": 1}})


def test_settling_as_not_sent_frees_the_slot_and_keeps_the_line(tmp_path):
    j = _j(tmp_path)
    with pytest.raises(RuntimeError):
        cj.buy_or_reuse(j, "id1", cap=2, send=lambda: (_ for _ in ()).throw(RuntimeError("gate")))
    assert j.entries["id1"]["status"] == cj.STATUS_UNCERTAIN and j.bought() == 1
    with pytest.raises(cj.NeedsHumanDecision):
        cj.buy_or_reuse(j, "id1", cap=2, send=lambda: {"r": 1})
    got = j.settle_uncertain("id1", bought=False, why="문 앞에서 거절 — provider 안 감", decided_by="tester")
    assert got["status"] == cj.STATUS_NOT_SENT and got["decision"]["by"] == "tester"
    assert j.bought() == 0, "★안 산 것은 상한에 안 센다"
    assert cj.buy_or_reuse(j, "id1", cap=1, send=lambda: {"r": 2}) == {"r": 2}, "★자리가 풀려 다시 산다"
    # ★★append-only (Codex BLOCK 2026-09-02 밤): 파일을 다시 읽어도 원 uncertain 줄 · 사람의 정정 · 새 결과가
    #  **전부** 남아 있고, 유효 뷰는 새 결과다.
    again = _j(tmp_path)
    mine = [e for e in again.events if e["identity"] == "id1"]
    assert [(e["attempt"], e["status"]) for e in mine] == [(1, cj.STATUS_UNCERTAIN), (2, cj.STATUS_OK)]
    assert again.settlements[0]["to"] == cj.STATUS_NOT_SENT and again.settlements[0]["by"] == "tester"
    assert again.entries["id1"]["status"] == cj.STATUS_OK and again.get("id1") == {"r": 2}
    assert again.bought() == 1 and again.bought_slots() == 1


def test_a_settled_but_not_retried_line_survives_reload_as_not_sent(tmp_path):
    j = _j(tmp_path)
    with pytest.raises(RuntimeError):
        cj.buy_or_reuse(j, "id1", cap=2, send=lambda: (_ for _ in ()).throw(RuntimeError("gate")))
    j.settle_uncertain("id1", bought=False, why="문 앞", decided_by="t")
    again = _j(tmp_path)
    assert again.entries["id1"]["status"] == cj.STATUS_NOT_SENT and again.entries["id1"]["decision"]["why"] == "문 앞"
    assert again.events[0]["status"] == cj.STATUS_UNCERTAIN, "★로그의 원 줄은 그대로다"
    assert again.bought() == 0
    rec = again.append_settlement("id1", kind="recovery", by="t", why="앞 판 코드가 결정을 덮었다")
    assert rec["kind"] == "recovery" and len(_j(tmp_path).settlements) == 2


def test_settling_as_bought_is_refused_and_non_uncertain_lines_are_refused(tmp_path):
    j = _j(tmp_path)
    cj.buy_or_reuse(j, "ok1", cap=2, send=lambda: {"r": 1})
    with pytest.raises(ValueError):
        j.settle_uncertain("ok1", bought=False, why="x", decided_by="t")
    with pytest.raises(RuntimeError):
        cj.buy_or_reuse(j, "u1", cap=2, send=lambda: (_ for _ in ()).throw(RuntimeError("gate")))
    with pytest.raises(ValueError):
        j.settle_uncertain("u1", bought=True, why="x", decided_by="t")


def test_rejudge_rows_skips_an_uncertain_replay_instead_of_dying(monkeypatch, tmp_path):
    from app.modules.pipeline import grounding_central_acquisition as ca
    from app.modules.pipeline import reference_acquisition_rounds as rr

    j = _j(tmp_path)
    monkeypatch.setattr(rr, "resolve_cached_candidates", lambda c, root: [{"index": 1, "path": "p", "rel": "p", "sha256": "0"}])
    monkeypatch.setattr(rr, "replay_identity_inputs", lambda acq, resolved: {"k": 1})
    monkeypatch.setattr(rr, "cached_round", lambda acq: {"downloaded_candidates": [{"index": 1}]})
    row = {"identity": "base", "disposition": ca.DISP_ACQUIRED, "status": "retryable",
           "acquisition": {"chosen": None, "rounds": [{"round_no": 1, "downloaded_candidates": [{"index": 1}]}]}}
    rid = ca.replay_identity("base", {"k": 1})
    with pytest.raises(RuntimeError):
        cj.buy_or_reuse(j, rid, cap=3, send=lambda: (_ for _ in ()).throw(RuntimeError("gate")))
    got = ca.rejudge_rows([row], journal=j, root=tmp_path, judge=lambda c: {}, cap=3)
    r = got["rows"][0]
    assert r.get("replay_why_code") == ca.WHY_PRIOR_UNCONFIRMED and "샀는지 모른다" in r["replay_skipped"]
