"""Review-TUI tests against a synthetic matches.csv, driven by scripted
input. BGG responses replay from the fixture cache; nothing hits the
network (the transport raises if it would)."""

from __future__ import annotations

import io
import json
from pathlib import Path

import httpx
import pytest
from rich.console import Console

from bggpipe.bgg_client import BGGClient
from bggpipe.config import Config
from bggpipe.resolve import read_matches, write_matches
from bggpipe.review import ReviewSession, run_review
from bggpipe.webreview import DismissStore

FIXTURES = Path(__file__).parent / "fixtures" / "bgg_cache"


def _no_network(request: httpx.Request) -> httpx.Response:
    raise AssertionError(f"test hit the network: {request.url}")


def fixture_client() -> BGGClient:
    return BGGClient(cache_dir=FIXTURES, transport=httpx.MockTransport(_no_network))


def unauthorized_client(tmp_path) -> BGGClient:
    """Client whose every request 401s — the no-token-yet world."""
    transport = httpx.MockTransport(
        lambda req: httpx.Response(401, text="Unauthorized")
    )
    return BGGClient(cache_dir=tmp_path / "empty_cache", transport=transport)


def scripted(*answers):
    it = iter(answers)

    def input_fn(prompt: str) -> str:
        try:
            return next(it)
        except StopIteration:
            raise EOFError from None

    return input_fn


def quiet_console() -> Console:
    return Console(file=io.StringIO(), width=100)


def _row(**overrides) -> dict:
    row = {
        "title_raw": "",
        "bgg_id": "",
        "bgg_name": "",
        "year": "",
        "type": "",
        "match_status": "auto",
        "version_id": "",
        "version_name": "",
        "version_status": "version_unknown",
        "candidates_json": "[]",
        "version_candidates_json": "[]",
        "source_photos": "hand-typed-test-list",
    }
    row.update(overrides)
    return row


CITADELS_CANDIDATES = json.dumps(
    [
        {
            "bgg_id": 478,
            "name": "Citadels",
            "year": 2000,
            "type": "boardgame",
            "owned": 85000,
            "rank": 250,
        },
        {
            "bgg_id": 205398,
            "name": "Citadels",
            "year": 2016,
            "type": "boardgame",
            "owned": 24000,
            "rank": 400,
        },
    ]
)

WINGSPAN_VERSION_CANDIDATES = json.dumps(
    [
        {
            "version_id": 465063,
            "name": "English first edition",
            "year": 2019,
            "publishers": ["Stonemaier Games"],
            "languages": ["English"],
            "score": 5,
        },
        {
            "version_id": 521212,
            "name": "English fourth printing",
            "year": 2020,
            "publishers": ["Stonemaier Games"],
            "languages": ["English"],
            "score": 5,
        },
    ]
)


def _cfg(tmp_path) -> Config:
    return Config(data_dir=tmp_path / "data")


def _setup(tmp_path, rows) -> Config:
    cfg = _cfg(tmp_path)
    write_matches(cfg.matches_path, rows)
    return cfg


def test_ambiguous_pick_approves_candidate(tmp_path):
    cfg = _setup(
        tmp_path,
        [
            _row(
                title_raw="Citadels",
                match_status="ambiguous",
                candidates_json=CITADELS_CANDIDATES,
            )
        ],
    )
    run_review(
        cfg, console=quiet_console(), input_fn=scripted("2"), client=fixture_client()
    )
    (row,) = read_matches(cfg.matches_path)
    assert row["match_status"] == "approved"
    assert row["bgg_id"] == "205398"
    assert row["year"] == "2016"


def test_reject_and_skip(tmp_path):
    cfg = _setup(
        tmp_path,
        [
            _row(title_raw="Blorvath", match_status="unmatched"),
            _row(
                title_raw="Citadels",
                match_status="ambiguous",
                candidates_json=CITADELS_CANDIDATES,
            ),
        ],
    )
    run_review(
        cfg,
        console=quiet_console(),
        input_fn=scripted("r", "s"),
        client=fixture_client(),
    )
    rows = {r["title_raw"]: r for r in read_matches(cfg.matches_path)}
    assert rows["Blorvath"]["match_status"] == "rejected"
    assert rows["Citadels"]["match_status"] == "ambiguous"  # skipped, still pending


def test_unmatched_manual_id_degrades_without_token(tmp_path):
    cfg = _setup(tmp_path, [_row(title_raw="Some Rare Game", match_status="unmatched")])
    run_review(
        cfg,
        console=quiet_console(),
        input_fn=scripted("m 99999"),
        client=unauthorized_client(tmp_path),
    )
    (row,) = read_matches(cfg.matches_path)
    assert row["match_status"] == "approved"
    assert row["bgg_id"] == "99999"
    assert row["bgg_name"] == ""  # lookup blocked; id recorded anyway


def test_unmatched_research_from_fixture_cache_with_version(tmp_path):
    cfg = _setup(tmp_path, [_row(title_raw="Wingspan", match_status="unmatched")])
    cfg.titles_path.parent.mkdir(parents=True, exist_ok=True)
    cfg.titles_path.write_text(
        json.dumps(
            [
                {
                    "title_raw": "Wingspan",
                    "publisher_hint": "Stonemaier Games",
                    "year_hint": 2019,
                    "language_hint": "English",
                    "source_photos": ["x.jpg"],
                }
            ]
        )
    )
    run_review(
        cfg,
        console=quiet_console(),
        # the real search lists a fan pack first; Wingspan proper is #2
        input_fn=scripted("f Wingspan", "2"),
        client=fixture_client(),
    )
    (row,) = read_matches(cfg.matches_path)
    assert row["match_status"] == "approved"
    assert row["bgg_id"] == "266192"
    # cues + the real version list -> a handful of English Stonemaier
    # printings stay plausible; the edition pass gets the ballot
    assert row["version_status"] == "version_ambiguous"
    assert "433233" in row["version_candidates_json"]


def test_resumable_quit_midway_then_continue(tmp_path):
    rows = [
        _row(
            title_raw="Citadels",
            match_status="ambiguous",
            candidates_json=CITADELS_CANDIDATES,
        ),
        _row(title_raw="Blorvath", match_status="unmatched"),
    ]
    cfg = _setup(tmp_path, rows)

    # first sitting: one decision, then input runs out (EOF == quit)
    run_review(
        cfg, console=quiet_console(), input_fn=scripted("1"), client=fixture_client()
    )
    by_title = {r["title_raw"]: r for r in read_matches(cfg.matches_path)}
    assert by_title["Citadels"]["match_status"] == "approved"
    assert by_title["Blorvath"]["match_status"] == "unmatched"  # untouched

    # second sitting resumes exactly where we left off
    run_review(
        cfg, console=quiet_console(), input_fn=scripted("r"), client=fixture_client()
    )
    by_title = {r["title_raw"]: r for r in read_matches(cfg.matches_path)}
    assert by_title["Blorvath"]["match_status"] == "rejected"


def test_version_pass_pick_and_unknown(tmp_path):
    cfg = _setup(
        tmp_path,
        [
            _row(
                title_raw="Wingspan",
                match_status="auto",
                bgg_id="266192",
                version_status="version_ambiguous",
                version_candidates_json=WINGSPAN_VERSION_CANDIDATES,
            ),
            _row(
                title_raw="Catan",
                match_status="auto",
                bgg_id="13",
                version_status="version_ambiguous",
                version_candidates_json=WINGSPAN_VERSION_CANDIDATES,
            ),
        ],
    )
    run_review(
        cfg,
        console=quiet_console(),
        input_fn=scripted("y", "1", "u"),
        client=fixture_client(),
    )
    by_title = {r["title_raw"]: r for r in read_matches(cfg.matches_path)}
    assert by_title["Wingspan"]["version_status"] == "version_approved"
    assert by_title["Wingspan"]["version_id"] == "465063"
    assert by_title["Catan"]["version_status"] == "version_unknown"
    assert by_title["Catan"]["version_id"] == ""


def test_version_pass_is_skippable(tmp_path):
    cfg = _setup(
        tmp_path,
        [
            _row(
                title_raw="Wingspan",
                match_status="auto",
                bgg_id="266192",
                version_status="version_ambiguous",
                version_candidates_json=WINGSPAN_VERSION_CANDIDATES,
            )
        ],
    )
    run_review(
        cfg, console=quiet_console(), input_fn=scripted("n"), client=fixture_client()
    )
    (row,) = read_matches(cfg.matches_path)
    assert row["version_status"] == "version_ambiguous"  # untouched, review later


# -- concurrent-rewrite and degradation safety --------------------------


def test_veto_merge_persists_against_future_dedupe(tmp_path):
    cfg = _setup(
        tmp_path,
        [
            _row(title_raw="CATAN", match_status="merged", merged_into="Catan"),
            _row(title_raw="Catan", match_status="auto", bgg_id="13"),
        ],
    )
    session = ReviewSession(
        cfg,
        console=quiet_console(),
        input_fn=scripted(),
        client=unauthorized_client(tmp_path),
    )
    merged = next(r for r in session.rows if r["match_status"] == "merged")
    session.veto_merge(merged)
    saved = {r["title_raw"]: r for r in read_matches(cfg.matches_path)}
    assert saved["CATAN"]["match_status"] == "approved"
    assert saved["CATAN"]["dedupe_veto"] == "1"  # survives resolve re-runs


def test_failed_save_never_leaves_memory_ahead_of_disk(tmp_path, monkeypatch):
    cfg = _setup(tmp_path, [_row(title_raw="Mystery", match_status="unmatched")])
    session = ReviewSession(
        cfg,
        console=quiet_console(),
        input_fn=scripted(),
        client=unauthorized_client(tmp_path),
    )
    row = session.rows[0]

    def exploding_write(path, rows):
        raise OSError("disk full")

    monkeypatch.setattr("bggpipe.review.write_matches", exploding_write)
    with pytest.raises(OSError):
        session.decide_reject(row)
    # memory was rolled back to what disk actually holds
    assert session.rows[0]["match_status"] == "unmatched"
    assert session.decisions == 0


def test_save_merges_own_decision_over_concurrent_external_rewrite(tmp_path):
    cfg = _setup(tmp_path, [_row(title_raw="Mystery", match_status="unmatched")])
    session = ReviewSession(
        cfg,
        console=quiet_console(),
        input_fn=scripted(),
        client=unauthorized_client(tmp_path),
    )
    row = session.rows[0]

    # resolve appends a new row in another terminal AFTER our session loaded
    external = read_matches(cfg.matches_path)
    external.append(_row(title_raw="Newcomer", match_status="auto", bgg_id="7"))
    write_matches(cfg.matches_path, external)

    session.decide_reject(row)
    saved = {r["title_raw"]: r for r in read_matches(cfg.matches_path)}
    assert saved["Mystery"]["match_status"] == "rejected"  # our decision
    assert "Newcomer" in saved  # their row survived too


def test_manual_id_unknown_to_bgg_warns_instead_of_crashing(tmp_path):
    empty_things = BGGClient(
        cache_dir=tmp_path / "cache",
        transport=httpx.MockTransport(
            lambda req: httpx.Response(200, text='<items total="0"></items>')
        ),
        sleep=lambda s: None,
    )
    cfg = _setup(tmp_path, [_row(title_raw="Mystery", match_status="unmatched")])
    session = ReviewSession(
        cfg, console=quiet_console(), input_fn=scripted(), client=empty_things
    )
    session.decide_manual(session.rows[0], 999999)
    assert session.rows[0]["match_status"] == "approved"
    assert session.rows[0]["bgg_id"] == "999999"
    assert any("no game with id 999999" in w for w in session.warnings)


def test_every_tui_decision_after_external_rewrite_is_saved(tmp_path):
    # run() iterates row references snapshotted before any reload; every
    # decision must be re-adopted into the current list or it would be
    # counted but never written.
    cfg = _setup(
        tmp_path,
        [
            _row(title_raw="Alpha", match_status="unmatched"),
            _row(title_raw="Beta", match_status="unmatched"),
            _row(title_raw="Gamma", match_status="unmatched"),
        ],
    )
    session = ReviewSession(
        cfg,
        console=quiet_console(),
        input_fn=scripted(),
        client=unauthorized_client(tmp_path),
    )
    stale_refs = list(session.pending_rows())  # what run() iterates

    external = read_matches(cfg.matches_path)
    external.append(_row(title_raw="Newcomer", match_status="auto", bgg_id="7"))
    write_matches(cfg.matches_path, external)

    for ref in stale_refs:  # decision 1 reloads; 2 and 3 are orphaned refs
        session.decide_reject(ref)

    saved = {r["title_raw"]: r for r in read_matches(cfg.matches_path)}
    assert [saved[t]["match_status"] for t in ("Alpha", "Beta", "Gamma")] == (
        ["rejected"] * 3
    )
    assert "Newcomer" in saved  # the external row survived too
    assert session.decisions == 3


def test_fill_version_uses_the_approved_rows_own_cues(tmp_path):
    # two same-title entries are two editions: the version lookup must use
    # the row's own photo-keyed cues, never a title-keyed last-wins dict
    entry_good = {
        "title_raw": "Wingspan",
        "publisher_hint": "Stonemaier",  # matches the fixture's version
        "year_hint": 2019,
        "source_photos": ["good.jpg"],
    }
    entry_bad = {
        "title_raw": "Wingspan",
        "publisher_hint": "Nobody Media",  # matches nothing
        "edition_hint": "Imaginary edition",
        "source_photos": ["bad.jpg"],
    }
    cfg = _setup(
        tmp_path,
        [
            _row(
                title_raw="Wingspan",
                match_status="ambiguous",
                source_photos="good.jpg",
                candidates_json=json.dumps(
                    [{"bgg_id": 266192, "name": "Wingspan", "year": 2019}]
                ),
            )
        ],
    )
    (cfg.data_dir / "titles.json").write_text(json.dumps([entry_good, entry_bad]))
    two_versions = """<items><item type="boardgame" id="266192">
      <name type="primary" value="Wingspan"/><yearpublished value="2019"/>
      <versions>
        <item type="boardgameversion" id="111">
          <name type="primary" value="English first edition"/>
          <yearpublished value="2019"/>
          <link type="boardgamepublisher" id="23202" value="Stonemaier Games"/>
          <link type="language" id="2184" value="English"/>
        </item>
        <item type="boardgameversion" id="222">
          <name type="primary" value="German edition"/>
          <yearpublished value="2019"/>
          <link type="boardgamepublisher" id="22160" value="Feuerland Spiele"/>
          <link type="language" id="2188" value="German"/>
        </item>
      </versions></item></items>"""
    crafted = BGGClient(
        cache_dir=tmp_path / "crafted_cache",
        transport=httpx.MockTransport(
            lambda req: httpx.Response(200, text=two_versions)
        ),
    )
    session = ReviewSession(
        cfg, console=quiet_console(), input_fn=scripted(), client=crafted
    )
    row = session.rows[0]
    session.decide_pick(row, {"bgg_id": 266192, "name": "Wingspan", "year": 2019})
    # the row's own photo (good.jpg) must select entry_good's cues
    assert row["version_status"] == "version_auto"
    assert row["version_id"] == "111"


def test_dismiss_failure_keeps_ticket_visible(tmp_path, monkeypatch):
    store = DismissStore(tmp_path / "dismissed.json")

    def exploding(path, text):
        raise OSError("disk full")

    monkeypatch.setattr("bggpipe.webreview.atomic_write_text", exploding)
    with pytest.raises(OSError):
        store.add("photo|loc|txt|art")
    assert store.keys == set()  # memory never claims what disk doesn't hold


def test_corrupt_dismiss_file_is_quarantined_not_fatal(tmp_path):
    path = tmp_path / "dismissed.json"
    path.write_text('["torn')
    with pytest.warns(UserWarning, match="unreadable"):
        store = DismissStore(path)
    assert store.keys == set()
    assert (tmp_path / "dismissed.json.corrupt").exists()


def test_split_row_makes_per_photo_vetoed_copies(tmp_path):
    from bggpipe.resolve import read_matches

    cfg = _setup(
        tmp_path,
        [
            _row(
                title_raw="Wiz-War",
                match_status="approved",
                bgg_id="104710",
                bgg_name="Wiz-War",
                source_photos="a.jpg;b.jpg;c.jpg",
                version_candidates_json='[{"version_id": 1, "name": "8th"}]',
            )
        ],
    )
    session = ReviewSession(
        cfg,
        console=quiet_console(),
        input_fn=scripted(),
        client=unauthorized_client(tmp_path),
    )
    copies = session.split_row(session.rows[0])
    saved = read_matches(cfg.matches_path)
    assert [r["source_photos"] for r in saved] == ["a.jpg", "b.jpg", "c.jpg"]
    assert all(r["dedupe_veto"] == "1" for r in saved)
    assert all(r["bgg_id"] == "104710" for r in saved)  # match survives
    # each copy picks its own edition: candidates kept, status reopened
    assert all(r["version_status"] == "version_ambiguous" for r in saved)
    assert all(r["version_id"] == "" for r in saved)
    assert len(copies) == 3


def test_split_copies_survive_resolve_rerun(tmp_path):
    from bggpipe.resolve import read_matches, run_resolve

    cfg = _setup(
        tmp_path,
        [
            _row(
                title_raw="Wiz-War",
                match_status="approved",
                bgg_id="104710",
                source_photos="a.jpg;b.jpg;c.jpg",
            )
        ],
    )
    (cfg.data_dir / "titles.json").write_text(
        json.dumps(
            [{"title_raw": "Wiz-War", "source_photos": ["a.jpg", "b.jpg", "c.jpg"]}]
        )
    )
    session = ReviewSession(
        cfg,
        console=quiet_console(),
        input_fn=scripted(),
        client=unauthorized_client(tmp_path),
    )
    session.split_row(session.rows[0])
    run_resolve(cfg, client=fixture_client())
    saved = read_matches(cfg.matches_path)
    assert len(saved) == 3  # not re-merged, not re-resolved
    assert [r["source_photos"] for r in saved] == ["a.jpg", "b.jpg", "c.jpg"]


def test_open_version_ballot_puts_every_edition_up_for_review(tmp_path):
    # a cue-less row is version_unknown by design — but the human knows
    # which printing the box is; the ballot fetches the FULL version list
    cfg = _setup(
        tmp_path,
        [
            _row(
                title_raw="Wingspan",
                match_status="auto",
                bgg_id="266192",
                bgg_name="Wingspan",
                version_status="version_unknown",
            )
        ],
    )
    session = ReviewSession(
        cfg, console=quiet_console(), input_fn=scripted(), client=fixture_client()
    )
    row = session.rows[0]
    count = session.open_version_ballot(row)
    assert count == 46  # the real recorded version list, complete
    assert row["version_status"] == "version_ambiguous"
    candidates = json.loads(row["version_candidates_json"])
    assert {c["version_id"] for c in candidates} >= {433233}
    saved = read_matches(cfg.matches_path)[0]
    assert saved["version_status"] == "version_ambiguous"  # persisted

    unmatched = _setup(tmp_path / "second", [_row(title_raw="Mystery")])
    session2 = ReviewSession(
        unmatched, console=quiet_console(), input_fn=scripted(), client=fixture_client()
    )
    with pytest.raises(ValueError, match="no BGG match"):
        session2.open_version_ballot(session2.rows[0])


def test_local_decision_keeps_a_real_game_off_bgg(tmp_path):
    cfg = _setup(
        tmp_path, [_row(title_raw="Obscure Homebrew", match_status="unmatched")]
    )
    run_review(
        cfg, console=quiet_console(), input_fn=scripted("l"), client=fixture_client()
    )
    (row,) = read_matches(cfg.matches_path)
    assert row["match_status"] == "local"
    assert row["bgg_id"] == ""  # a local citizen carries no BGG identity


def test_reopen_match_returns_a_fresh_ballot(tmp_path):
    """ "Wrong game" must not strand the row: it re-searches immediately
    and comes back ambiguous with the candidates on the card — resolve
    (which never touches unmatched rows) is not part of this loop."""
    cfg = _setup(
        tmp_path,
        [
            _row(
                title_raw="WIZ-WAR",
                match_status="auto",
                bgg_id="589",
                bgg_name="Wiz-War",
            )
        ],
    )
    session = ReviewSession(
        cfg, console=quiet_console(), input_fn=scripted(), client=fixture_client()
    )
    row = session.rows[0]
    session.reopen_match(row)
    assert row["match_status"] == "ambiguous"
    names = {c["name"] for c in json.loads(row["candidates_json"])}
    assert "Wiz-War" in names
    assert any("Eighth Edition" in n for n in names)  # the lineages surface
    # BGG unreachable: the row still reopens, bare but actionable
    dead = ReviewSession(
        cfg,
        console=quiet_console(),
        input_fn=scripted(),
        client=BGGClient(
            cache_dir=tmp_path / "empty",
            transport=httpx.MockTransport(
                lambda req: httpx.Response(401, text="Unauthorized")
            ),
        ),
    )
    row2 = dead.rows[0]
    dead.reopen_match(row2)
    assert row2["match_status"] == "unmatched"
    assert dead.warnings  # the degradation is visible


def test_local_rows_can_be_looked_up_again_on_rpggeek(tmp_path):
    """ "Local" means BGG has no board game by that name — but RPGGeek,
    the same database under type=rpgitem, often does. A local row must not
    be a one-way door."""
    cfg = _setup(
        tmp_path,
        [
            _row(
                title_raw="ALICE IS MISSING: A SILENT ROLE PLAYING GAME",
                match_status="local",
            )
        ],
    )
    session = ReviewSession(
        cfg, console=quiet_console(), input_fn=scripted(), client=fixture_client()
    )
    row = session.rows[0]
    session.reopen_match(row)
    assert row["match_status"] == "ambiguous"
    candidates = json.loads(row["candidates_json"])
    assert any(c["type"] == "rpgitem" for c in candidates)
    assert any("Alice is Missing" in c["name"] for c in candidates)


def test_research_can_target_rpggeek_explicitly(tmp_path):
    """BGG has board games called "Dungeons & Dragons", so the automatic
    cascade never reaches RPGGeek for them — only an explicit search can."""
    cfg = _setup(tmp_path, [_row(title_raw="Wingspan", match_status="unmatched")])
    session = ReviewSession(
        cfg, console=quiet_console(), input_fn=scripted(), client=fixture_client()
    )
    row = session.rows[0]
    # cached search, but the stats batch for these results was never
    # recorded: losing the decoration must not lose the search
    session.client = BGGClient(
        cache_dir=FIXTURES,
        transport=httpx.MockTransport(
            lambda req: httpx.Response(401, text="Unauthorized")
        ),
    )
    found = session.research(row, "Citadels")
    assert found and row["match_status"] == "ambiguous"
    ids = {c["bgg_id"] for c in json.loads(row["candidates_json"])}
    assert {478, 205398} <= ids
    assert session.warnings  # the missing stats are reported, not swallowed
    # a fresh ballot supersedes the previous verdict
    assert row["bgg_id"] == ""
    with pytest.raises(ValueError, match="needs some text"):
        session.research(row, "   ")


def test_find_row_prefers_the_version_open_sibling(tmp_path):
    """Two decided duplicate rows: a version pick with a stale row_ix must
    land on the sibling whose VERSION is still open, not overwrite the
    other's earlier human decision (mirrors _adopt)."""
    cfg = _setup(
        tmp_path,
        [
            _row(
                title_raw="Wiz-War",
                match_status="approved",
                bgg_id="589",
                version_status="version_approved",
                version_id="1",
            ),
            _row(
                title_raw="Wiz-War",
                match_status="approved",
                bgg_id="589",
                version_status="version_ambiguous",
            ),
        ],
    )
    session = ReviewSession(
        cfg, console=quiet_console(), input_fn=scripted(), client=fixture_client()
    )
    row = session.find_row("Wiz-War", "hand-typed-test-list", row_ix=None)
    assert row["version_status"] == "version_ambiguous"


def test_research_clears_the_previous_games_version(tmp_path):
    """A fresh ballot supersedes EVERYTHING about the old match: a
    surviving version_id would put the old game's edition on whatever the
    human picks next."""
    cfg = _setup(
        tmp_path,
        [
            _row(
                title_raw="Citadels",
                match_status="approved",
                bgg_id="478",
                version_status="version_approved",
                version_id="99999",
                version_name="Old Game's Edition",
            )
        ],
    )
    session = ReviewSession(
        cfg, console=quiet_console(), input_fn=scripted(), client=fixture_client()
    )
    # stats batch for these results isn't recorded; decoration may fail
    session.client = BGGClient(
        cache_dir=FIXTURES,
        transport=httpx.MockTransport(
            lambda req: httpx.Response(401, text="Unauthorized")
        ),
    )
    row = session.rows[0]
    session.research(row, "Citadels")
    assert row["version_status"] == ""
    assert row["version_id"] == "" and row["version_name"] == ""
    assert row["version_candidates_json"] == "[]"


def test_reopening_a_version_ballot_keeps_the_current_pick(tmp_path):
    """Change-edition must be lossless: opening the full printing list on
    an approved row keeps the pick until a new decision, and the lingering
    id is inert while ambiguous."""
    from bggpipe.models import is_confident_version

    cfg = _setup(
        tmp_path,
        [
            _row(
                title_raw="Wingspan",
                match_status="approved",
                bgg_id="266192",
                version_status="version_approved",
                version_id="465063",
                version_name="English first edition",
            )
        ],
    )
    session = ReviewSession(
        cfg, console=quiet_console(), input_fn=scripted(), client=fixture_client()
    )
    row = session.rows[0]
    assert session.open_version_ballot(row) > 1
    assert row["version_status"] == "version_ambiguous"
    assert row["version_id"] == "465063"  # survives until a new choice
    assert not is_confident_version(row)  # but inert for diff/upload
    session.decide_version(row, None)  # "can't tell" clears it properly
    assert row["version_id"] == ""
