"""세 자료원을 합쳐 한 화면으로 — 조회는 밖에서, 합치기는 순수 함수로."""
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[3]))

from tools.shot_influence import merge_shot_influence, render_text  # noqa: E402


def _fixture():
    return dict(
        record={
            "shot_run_uid": "0190-abc", "selected": "B",
            "ranking": ["B", "A"], "totals": {"A": 9, "B": 14},
            "input_fingerprint": "e6ad7c49",
            "ref_mode": "그룹 배경+엔티티",
            "refs": [{"label": "CHARACTER REFERENCE — 김선영",
                      "asset_id": "char-1", "role": "character_ref"}],
            "verdicts": [{"label": "B", "score": 7}],
            "fix_skipped": True, "fix_skip_reason": "no_critical_issue",
        },
        asset_row={"id": "asset-9", "file_path": "/p/S12sh3.png",
                   "generation_call_id": "call-3",
                   "shot_run_uid": "0190-abc"},
        opik_trace_row={"id": "0190-abc", "name": "still:0190abc · still_recipe",
                        "thread_id": "마지막 임무_1회_f5372927"},
        opik_spans=[{"name": "roll · grok-imagine", "usage": {"total_tokens": 0}},
                    {"name": "judge · grok-4.6",
                     "usage": {"total_tokens": 1500}}],
    )


def test_merge_keys_off_the_uid():
    m = merge_shot_influence(**_fixture())
    assert m["shot_run_uid"] == "0190-abc"
    assert m["sources"] == ["records.json", "image_asset", "opik"]


def test_merge_flags_uid_mismatch():
    """세 곳의 uid 가 다르면 조용히 넘어가면 안 된다."""
    f = _fixture()
    f["asset_row"]["shot_run_uid"] = "0190-DIFFERENT"
    m = merge_shot_influence(**f)
    assert m["uid_mismatch"] == ["image_asset"]


def test_merge_survives_missing_sources():
    f = _fixture()
    f["opik_trace_row"] = None
    f["opik_spans"] = []
    m = merge_shot_influence(**f)
    assert "opik" not in m["sources"]
    assert m["calls"] == []


def test_render_lists_refs_and_calls():
    out = render_text(merge_shot_influence(**_fixture()))
    assert "CHARACTER REFERENCE — 김선영" in out
    assert "char-1" in out
    assert "judge · grok-4.6" in out
    assert "선정: B" in out


def test_render_marks_unresolved_refs():
    f = _fixture()
    f["record"]["refs"].append({"label": "PROP REFERENCE", "asset_id": None})
    out = render_text(merge_shot_influence(**f))
    assert "자산 불명" in out


# ── 수리 판정·페이지 순회 (Codex BLOCK 2·3, 2026-08-24) ────────────────

def test_render_distinguishes_fix_loss_from_skip():
    """★「적용 안 됨」의 갈래가 둘이다 — 안 돌렸거나, 돌렸는데 졌거나."""
    f = _fixture()
    f["record"] = {**f["record"], "fix_skipped": False,
                   "fix_prompt": "P", "fix_rejudge": {"winner": "A"}}
    out = render_text(merge_shot_influence(**f))
    assert "수리 미적용" in out
    assert "수리 적용 —" not in out


def test_render_says_applied_when_fix_won():
    f = _fixture()
    f["record"] = {**f["record"], "fix_skipped": False,
                   "fix_prompt": "P", "fix_rejudge": {"winner": "B"}}
    out = render_text(merge_shot_influence(**f))
    assert "수리 적용" in out


def test_render_still_reports_skip_reason():
    out = render_text(merge_shot_influence(**_fixture()))
    assert "수리 건너뜀" in out and "no_critical_issue" in out


def _paged(total, uid_page):
    """페이지 응답을 흉내 내는 가짜 조회기 — 망을 안 탄다."""
    from tools.shot_influence import PAGE_SIZE

    calls = []

    def _get(path):
        calls.append(path)
        if "/projects" in path:
            return {"content": [{"id": "pid", "name": "x"}], "total": 1}
        page = int(path.split("page=")[1].split("&")[0])
        if "/spans" in path:
            # ★span 도 **2쪽이 실제로 있는** 응답을 준다 (2026-08-24 Codex
            #   비차단 지적). total=1 로 고정하면 1쪽에서 끝나 「페이지를
            #   넘는다」는 시험이 그 이름이 주장하는 것을 안 잰다.
            if page == 1:
                return {"content": [{"name": f"roll{i} · m"}
                                    for i in range(PAGE_SIZE)],
                        "total": PAGE_SIZE + 1}
            if page == 2:
                return {"content": [{"name": "judge · m"}],
                        "total": PAGE_SIZE + 1}
            return {"content": [], "total": PAGE_SIZE + 1}
        rows = [{"id": f"t{page}-{i}"} for i in range(PAGE_SIZE)]
        if page == uid_page:
            rows[3] = {"id": "0190-old"}
        return {"content": rows, "total": total}

    return _get, calls


def test_trace_lookup_walks_past_the_first_page():
    """★첫 페이지만 보면 오래된 uid 를 자산이 있어도 조용히 놓친다."""
    from tools.shot_influence import PAGE_SIZE, _find_trace_by_uid

    get, calls = _paged(total=PAGE_SIZE * 3, uid_page=2)
    row = _find_trace_by_uid(get, "pid", "0190-old")
    assert row == {"id": "0190-old"}
    assert any("page=2" in c for c in calls), "2쪽을 안 봤다"


def test_trace_lookup_stops_at_the_end():
    """끝까지 없으면 None — 무한히 돌지 않는다."""
    from tools.shot_influence import PAGE_SIZE, _find_trace_by_uid

    get, calls = _paged(total=PAGE_SIZE * 2, uid_page=99)
    assert _find_trace_by_uid(get, "pid", "0190-nope") is None
    assert len([c for c in calls if "/traces" in c]) == 2


def test_spans_are_paged_too():
    """★span 도 2쪽을 탄다 — 1쪽이 꽉 차면 다음 쪽이 있다."""
    from tools.shot_influence import PAGE_SIZE, _fetch_spans

    get, calls = _paged(total=1, uid_page=1)
    out = _fetch_spans(get, "pid", "0190-old")
    assert len(out) == PAGE_SIZE + 1, "2쪽 span 을 안 가져왔다"
    assert out[-1] == {"name": "judge · m"}
    assert any("/spans" in c and "page=2" in c for c in calls), "2쪽을 안 봤다"
