"""outlook_extractor_v2 모듈 테스트 — 2단계 아웃룩 추출."""
import pytest
from unittest.mock import patch, MagicMock


# ── Fixtures ──

SEGMENTS = [
    {"scene_index": 1, "heading": "병원 로비", "start_char": 0, "end_char": 100},
    {"scene_index": 2, "heading": "사무실", "start_char": 100, "end_char": 200},
    {"scene_index": 3, "heading": "카페", "start_char": 200, "end_char": 300},
]

FULLTEXT = "A" * 300

CHARACTERS = [
    {"short_id": "C01", "name": "동녘"},
    {"short_id": "C02", "name": "수아"},
]


# ── Phase 1 Tests ──

def test_phase1_calls_call_structured_with_correct_step():
    """phase1이 call_structured를 step='outlook_extraction'으로 호출하는지 확인."""
    from app.modules.pipeline.outlook_extractor_v2 import extract_outlooks_phase1

    mock_result = {
        "outlooks": [
            {"name": "검은정장", "description": "검은색 더블 브레스트 정장", "character_id": "C01"},
            {"name": "흰블라우스", "description": "흰색 실크 블라우스", "character_id": "C02"},
        ]
    }

    with patch("app.modules.pipeline.outlook_extractor_v2.call_structured", return_value=mock_result) as mock_call, \
         patch("app.modules.pipeline.outlook_extractor_v2.load_prompt", return_value="system prompt"), \
         patch("app.modules.pipeline.outlook_extractor_v2.load_schema", return_value={"type": "object"}):

        result = extract_outlooks_phase1(
            segments=SEGMENTS,
            fulltext=FULLTEXT,
            characters=CHARACTERS,
        )

        mock_call.assert_called_once()
        _, kwargs = mock_call.call_args
        assert kwargs["step"] == "outlook_extraction"
        assert kwargs["schema_name"] == "outlook_phase1"
        assert len(result["outlooks"]) == 2


def test_phase1_includes_character_block_in_user_prompt():
    """phase1 user_prompt에 캐릭터 short_id와 이름이 포함되는지 확인."""
    from app.modules.pipeline.outlook_extractor_v2 import extract_outlooks_phase1

    captured_prompt = {}

    def capture_call(**kwargs):
        captured_prompt["user_prompt"] = kwargs["user_prompt"]
        return {"outlooks": []}

    with patch("app.modules.pipeline.outlook_extractor_v2.call_structured", side_effect=capture_call), \
         patch("app.modules.pipeline.outlook_extractor_v2.load_prompt", return_value="system"), \
         patch("app.modules.pipeline.outlook_extractor_v2.load_schema", return_value={"type": "object"}):

        extract_outlooks_phase1(
            segments=SEGMENTS,
            fulltext=FULLTEXT,
            characters=CHARACTERS,
        )

        prompt = captured_prompt["user_prompt"]
        assert "C01" in prompt
        assert "C02" in prompt
        assert "동녘" in prompt
        assert "수아" in prompt


def test_phase1_includes_scene_block():
    """phase1 user_prompt에 씬 블록(heading)이 포함되는지 확인."""
    from app.modules.pipeline.outlook_extractor_v2 import extract_outlooks_phase1

    captured_prompt = {}

    def capture_call(**kwargs):
        captured_prompt["user_prompt"] = kwargs["user_prompt"]
        return {"outlooks": []}

    with patch("app.modules.pipeline.outlook_extractor_v2.call_structured", side_effect=capture_call), \
         patch("app.modules.pipeline.outlook_extractor_v2.load_prompt", return_value="system"), \
         patch("app.modules.pipeline.outlook_extractor_v2.load_schema", return_value={"type": "object"}):

        extract_outlooks_phase1(
            segments=SEGMENTS,
            fulltext=FULLTEXT,
            characters=CHARACTERS,
        )

        prompt = captured_prompt["user_prompt"]
        assert "병원 로비" in prompt
        assert "사무실" in prompt
        assert "카페" in prompt


def test_phase1_passes_project_config_and_opik():
    """phase1이 project_config와 opik_metadata를 전달하는지 확인."""
    from app.modules.pipeline.outlook_extractor_v2 import extract_outlooks_phase1

    config = {"outlook_extraction": {"model": "gpt"}}
    opik = {"tags": ["test"]}

    with patch("app.modules.pipeline.outlook_extractor_v2.call_structured", return_value={"outlooks": []}) as mock_call, \
         patch("app.modules.pipeline.outlook_extractor_v2.load_prompt", return_value="system"), \
         patch("app.modules.pipeline.outlook_extractor_v2.load_schema", return_value={"type": "object"}):

        extract_outlooks_phase1(
            segments=SEGMENTS,
            fulltext=FULLTEXT,
            characters=CHARACTERS,
            project_config=config,
            opik_metadata=opik,
        )

        _, kwargs = mock_call.call_args
        assert kwargs["project_config"] == config
        assert kwargs["opik_metadata"] == opik


def test_phase1_returns_empty_outlooks():
    """phase1이 빈 outlooks를 반환해도 정상 처리."""
    from app.modules.pipeline.outlook_extractor_v2 import extract_outlooks_phase1

    with patch("app.modules.pipeline.outlook_extractor_v2.call_structured", return_value={"outlooks": []}), \
         patch("app.modules.pipeline.outlook_extractor_v2.load_prompt", return_value="system"), \
         patch("app.modules.pipeline.outlook_extractor_v2.load_schema", return_value={"type": "object"}):

        result = extract_outlooks_phase1(
            segments=SEGMENTS,
            fulltext=FULLTEXT,
            characters=CHARACTERS,
        )
        assert result["outlooks"] == []


# ── Phase 2 Tests ──

@pytest.mark.skip(reason="W3-3 cluster B drift — v3/v2 계약이 현재(v4/shot-more) 구현과 어긋남. docs/review-codex-1/11-fix-plan.md §8.2 참조. 복원/재작성은 Wave 5 이후 재평가.")
def test_phase2_calls_call_structured_with_correct_step():
    """phase2가 call_structured를 step='outlook_extraction'으로 호출하는지 확인."""
    from app.modules.pipeline.outlook_extractor_v2 import extract_outlooks_phase2

    outlooks = [
        {"name": "검은정장", "description": "검은색 정장", "character_id": "C01"},
        {"name": "흰블라우스", "description": "흰색 블라우스", "character_id": "C02"},
    ]

    mock_result = {
        "scene_assignments": [
            {"scene_index": 1, "assignments": [
                {"character_id": "C01", "outlook_name": "검은정장"},
            ]},
        ]
    }

    with patch("app.modules.pipeline.outlook_extractor_v2.call_structured", return_value=mock_result) as mock_call, \
         patch("app.modules.pipeline.outlook_extractor_v2.load_prompt", return_value="system prompt"), \
         patch("app.modules.pipeline.outlook_extractor_v2.load_schema", return_value={"type": "object"}):

        result = extract_outlooks_phase2(
            segments=SEGMENTS,
            fulltext=FULLTEXT,
            outlooks=outlooks,
            characters=CHARACTERS,
        )

        mock_call.assert_called_once()
        _, kwargs = mock_call.call_args
        assert kwargs["step"] == "outlook_extraction"
        assert kwargs["schema_name"] == "outlook_phase2"
        assert len(result["scene_assignments"]) == 1


@pytest.mark.skip(reason="W3-3 cluster B drift — v3/v2 계약이 현재(v4/shot-more) 구현과 어긋남. docs/review-codex-1/11-fix-plan.md §8.2 참조. 복원/재작성은 Wave 5 이후 재평가.")
def test_phase2_builds_char_outlook_lines():
    """phase2가 인물+아웃룩 조합을 올바르게 구성하는지 확인."""
    from app.modules.pipeline.outlook_extractor_v2 import extract_outlooks_phase2

    outlooks = [
        {"name": "검은정장", "description": "검은색 정장", "character_id": "C01"},
        {"name": "회색코트", "description": "회색 롱코트", "character_id": "C01"},
        {"name": "흰블라우스", "description": "흰색 블라우스", "character_id": "C02"},
    ]

    captured_prompt = {}

    def capture_call(**kwargs):
        captured_prompt["user_prompt"] = kwargs["user_prompt"]
        return {"scene_assignments": []}

    with patch("app.modules.pipeline.outlook_extractor_v2.call_structured", side_effect=capture_call), \
         patch("app.modules.pipeline.outlook_extractor_v2.load_prompt", return_value="system"), \
         patch("app.modules.pipeline.outlook_extractor_v2.load_schema", return_value={"type": "object"}):

        extract_outlooks_phase2(
            segments=SEGMENTS,
            fulltext=FULLTEXT,
            outlooks=outlooks,
            characters=CHARACTERS,
        )

        prompt = captured_prompt["user_prompt"]
        # C01의 두 아웃룩
        assert "C01O01" in prompt
        assert "C01O02" in prompt
        assert "검은정장" in prompt
        assert "회색코트" in prompt
        # C02의 아웃룩
        assert "C02O03" in prompt
        assert "흰블라우스" in prompt


@pytest.mark.skip(reason="W3-3 cluster B drift — v3/v2 계약이 현재(v4/shot-more) 구현과 어긋남. docs/review-codex-1/11-fix-plan.md §8.2 참조. 복원/재작성은 Wave 5 이후 재평가.")
def test_phase2_sets_temp_short_id_on_outlooks():
    """phase2가 outlook에 temp_short_id를 설정하는지 확인."""
    from app.modules.pipeline.outlook_extractor_v2 import extract_outlooks_phase2

    outlooks = [
        {"name": "검은정장", "description": "검은색 정장", "character_id": "C01"},
        {"name": "흰블라우스", "description": "흰색 블라우스", "character_id": "C02"},
    ]

    with patch("app.modules.pipeline.outlook_extractor_v2.call_structured", return_value={"scene_assignments": []}), \
         patch("app.modules.pipeline.outlook_extractor_v2.load_prompt", return_value="system"), \
         patch("app.modules.pipeline.outlook_extractor_v2.load_schema", return_value={"type": "object"}):

        extract_outlooks_phase2(
            segments=SEGMENTS,
            fulltext=FULLTEXT,
            outlooks=outlooks,
            characters=CHARACTERS,
        )

        assert outlooks[0].get("temp_short_id") == "O01"
        assert outlooks[1].get("temp_short_id") == "O02"


@pytest.mark.skip(reason="W3-3 cluster B drift — v3/v2 계약이 현재(v4/shot-more) 구현과 어긋남. docs/review-codex-1/11-fix-plan.md §8.2 참조. 복원/재작성은 Wave 5 이후 재평가.")
def test_phase2_matches_by_character_name():
    """phase2가 character_name으로도 매칭되는지 확인."""
    from app.modules.pipeline.outlook_extractor_v2 import extract_outlooks_phase2

    outlooks = [
        {"name": "캐주얼셔츠", "description": "파란 셔츠", "character_name": "동녘"},
    ]

    captured_prompt = {}

    def capture_call(**kwargs):
        captured_prompt["user_prompt"] = kwargs["user_prompt"]
        return {"scene_assignments": []}

    with patch("app.modules.pipeline.outlook_extractor_v2.call_structured", side_effect=capture_call), \
         patch("app.modules.pipeline.outlook_extractor_v2.load_prompt", return_value="system"), \
         patch("app.modules.pipeline.outlook_extractor_v2.load_schema", return_value={"type": "object"}):

        extract_outlooks_phase2(
            segments=SEGMENTS,
            fulltext=FULLTEXT,
            outlooks=outlooks,
            characters=CHARACTERS,
        )

        prompt = captured_prompt["user_prompt"]
        assert "C01O01" in prompt
        assert "캐주얼셔츠" in prompt


@pytest.mark.skip(reason="W3-3 cluster B drift — v3/v2 계약이 현재(v4/shot-more) 구현과 어긋남. docs/review-codex-1/11-fix-plan.md §8.2 참조. 복원/재작성은 Wave 5 이후 재평가.")
def test_phase2_passes_project_config_and_opik():
    """phase2가 project_config와 opik_metadata를 전달하는지 확인."""
    from app.modules.pipeline.outlook_extractor_v2 import extract_outlooks_phase2

    config = {"outlook_extraction": {"model": "gpt"}}
    opik = {"tags": ["outlook"]}

    with patch("app.modules.pipeline.outlook_extractor_v2.call_structured", return_value={"scene_assignments": []}) as mock_call, \
         patch("app.modules.pipeline.outlook_extractor_v2.load_prompt", return_value="system"), \
         patch("app.modules.pipeline.outlook_extractor_v2.load_schema", return_value={"type": "object"}):

        extract_outlooks_phase2(
            segments=SEGMENTS,
            fulltext=FULLTEXT,
            outlooks=[{"name": "x", "description": "y", "character_id": "C01"}],
            characters=CHARACTERS,
            project_config=config,
            opik_metadata=opik,
        )

        _, kwargs = mock_call.call_args
        assert kwargs["project_config"] == config
        assert kwargs["opik_metadata"] == opik


# ── _build_scene_block Tests ──

@pytest.mark.skip(reason="W3-3 cluster B drift — v3/v2 계약이 현재(v4/shot-more) 구현과 어긋남. docs/review-codex-1/11-fix-plan.md §8.2 참조. 복원/재작성은 Wave 5 이후 재평가.")
def test_build_scene_block_limits_text_to_400_chars():
    """_build_scene_block이 씬 텍스트를 400자로 제한하는지 확인."""
    from app.modules.pipeline.outlook_extractor_v2 import _build_scene_block

    long_fulltext = "X" * 1000
    segments = [
        {"scene_index": 1, "heading": "테스트", "start_char": 0, "end_char": 800},
    ]

    block = _build_scene_block(segments, long_fulltext)
    # header line + text, text should be limited to 400 chars
    assert "X" * 400 in block
    assert "X" * 401 not in block


@pytest.mark.skip(reason="W3-3 cluster B drift — v3/v2 계약이 현재(v4/shot-more) 구현과 어긋남. docs/review-codex-1/11-fix-plan.md §8.2 참조. 복원/재작성은 Wave 5 이후 재평가.")
def test_build_scene_block_uses_defaults_for_missing_fields():
    """씬에 heading/start_char/end_char가 없어도 기본값으로 처리."""
    from app.modules.pipeline.outlook_extractor_v2 import _build_scene_block

    segments = [
        {"scene_index": 5},
    ]
    fulltext = "test content here"

    block = _build_scene_block(segments, fulltext)
    assert "씬 5" in block
    assert "test content here" in block


def test_build_scene_block_multiple_scenes():
    """여러 씬이 올바르게 연결되는지 확인."""
    from app.modules.pipeline.outlook_extractor_v2 import _build_scene_block

    block = _build_scene_block(SEGMENTS, FULLTEXT)
    assert "씬 1: 병원 로비" in block
    assert "씬 2: 사무실" in block
    assert "씬 3: 카페" in block
