"""outlook_phase2 재시도는 **빠진 (씬, 인물) 짝이 줄 때만** 계속한다 (2026-09-17 컨트리로드).

## 무엇이 결함이었나

91씬 중 6씬에서 소유 아웃룩이 있는 인물 9명이 비었다. 재시도 3번 모두 같은
6씬을 같은 입력으로 다시 물었고 모델은 같은 답을 냈다. 로그는 씬에 답이 하나라도
오면 「6/6 복구」라 적어, 그대로 빈 것이 안 보였다(단계는 91/91 완료).

## 잠그는 것

① 재시도가 빠진 짝을 줄이지 못하면 거기서 멈춘다
② 줄이는 동안은 계속한다(최대 3회 그대로)
③ 끝내 남은 (씬, 인물) 짝은 경고로 남는다
"""
import logging
from unittest.mock import patch

from app.core.steps import outlook_steps


def _run(answers, scenes, caplog):
    """answers[i] = i번째 호출에서 배정할 {scene_index: [character_id...]}."""
    from app.core.steps.outlook_steps import OutlookPhase2Step

    calls = []

    def _fake_extract(**kwargs):
        i = len(calls)
        calls.append(sorted(seg["scene_index"] for seg in kwargs["segments"]))
        plan = answers[min(i, len(answers) - 1)]
        return {"scene_assignments": [
            {"segment_key": f"SEG-{seg['scene_index']:03d}", "scene_index": seg["scene_index"],
             "assignments": [{"character_id": c, "outlook_id": "O01"}
                             for c in plan.get(seg["scene_index"], [])]}
            for seg in kwargs["segments"]]}

    step = OutlookPhase2Step.__new__(OutlookPhase2Step)
    step.project_config = {"project_id": "p"}
    step._load_segments = lambda: [
        {"scene_index": si, "heading": f"S#{si}", "start_char": 0, "end_char": 10} for si in scenes]
    step._load_characters_and_scene_map = lambda: (
        [{"short_id": "C01", "name": "가"}], {si: ["C01"] for si in scenes}, None)
    step.build_opik_metadata = lambda: {}
    step._load_prev_checkpoint = lambda sid: (
        {"data": {"outlooks": [{"short_id": "O01", "character_id": "C01", "name": "제복"}],
                  "null_outlook_chars": []}}
        if sid == "outlook_phase1" else None)

    with caplog.at_level(logging.WARNING, logger=outlook_steps.logger.name):
        with patch("app.modules.pipeline.outlook_extractor_v2.extract_outlooks_phase2", _fake_extract):
            step._execute()
    return calls


def test_retry_stops_when_it_recovers_nothing(caplog):
    """모델이 계속 비워 두면 첫 호출 + 재시도 1번에서 멈춘다 (옛 코드는 재시도 3번)."""
    calls = _run([{}], scenes=[1], caplog=caplog)
    assert len(calls) == 2, calls


def test_retry_continues_while_it_recovers(caplog):
    """재시도가 짝을 줄이면 계속하고, 못 줄이는 순간 멈춘다."""
    calls = _run([{}, {1: ["C01"]}, {}], scenes=[1, 2], caplog=caplog)
    assert calls == [[1, 2], [1, 2], [2]], calls


def test_leftover_pairs_are_logged(caplog):
    _run([{}], scenes=[1], caplog=caplog)
    assert any("끝내 아웃룩이 안 붙은" in r.getMessage() and "(1, 'C01')" in r.getMessage()
               for r in caplog.records), [r.getMessage() for r in caplog.records]
