"""entity_extractor_v4 모듈 테스트 — 3턴 타입별 순차 추출."""
import pytest
from unittest.mock import patch, MagicMock, call


# ── Test: extract_entities_by_type calls call_structured with correct step ──

@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_extract_by_type_calls_call_structured_with_correct_step():
    """call_structured가 step='entity_extract'로 호출되는지 확인."""
    from app.modules.pipeline.entity_extractor_v4 import extract_entities_by_type

    mock_result = {
        "characters": [
            {
                "name": "홍길동",
                "description": "30대 남성",
                "visual_traits": ["검은 머리", "키 큰"],
                "scene_appearances": [1, 3, 5],
            }
        ]
    }

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

        result = extract_entities_by_type("시나리오 전문", "character")

        mock_call.assert_called_once()
        _, kwargs = mock_call.call_args
        assert kwargs["step"] == "entity_extract"
        assert kwargs["schema_name"] == "entity_character"
        assert len(result) == 1
        assert result[0]["name"] == "홍길동"


def test_extract_by_type_location_uses_correct_root_key():
    """location 타입은 'locations' 루트 키를 사용하는지 확인."""
    from app.modules.pipeline.entity_extractor_v4 import extract_entities_by_type

    mock_result = {
        "locations": [
            {
                "name": "서울역",
                "description": "대형 기차역",
                "visual_traits": ["높은 천장", "대리석 바닥"],
                "scene_appearances": [2, 4],
            }
        ]
    }

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

        result = extract_entities_by_type("시나리오", "location")
        assert len(result) == 1
        assert result[0]["name"] == "서울역"


def test_extract_by_type_prop_uses_correct_root_key():
    """prop 타입은 'props' 루트 키를 사용하는지 확인."""
    from app.modules.pipeline.entity_extractor_v4 import extract_entities_by_type

    mock_result = {
        "props": [
            {
                "name": "마법 검",
                "description": "빛나는 장검",
                "visual_traits": ["은색 칼날", "보석 손잡이"],
                "scene_appearances": [1, 5, 7],
            }
        ]
    }

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

        result = extract_entities_by_type("시나리오", "prop")
        assert len(result) == 1
        assert result[0]["name"] == "마법 검"


# ── Test: 2+ scene filter works ──

@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_filter_removes_entity_with_single_scene():
    """씬 1개만 등장하는 요소는 필터링되어야 함."""
    from app.modules.pipeline.entity_extractor_v4 import extract_entities_by_type

    mock_result = {
        "characters": [
            {
                "name": "주인공",
                "description": "30대 남성",
                "visual_traits": ["검정 코트"],
                "scene_appearances": [1, 3, 5],
            },
            {
                "name": "행인",
                "description": "50대 남성",
                "visual_traits": ["회색 모자"],
                "scene_appearances": [1],  # 1씬만 → 필터됨
            },
        ]
    }

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

        result = extract_entities_by_type("시나리오", "character")
        assert len(result) == 1
        assert result[0]["name"] == "주인공"


@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_filter_removes_entity_with_duplicate_single_scene():
    """같은 씬 번호가 중복되면 unique 씬이 1개이므로 필터됨."""
    from app.modules.pipeline.entity_extractor_v4 import extract_entities_by_type

    mock_result = {
        "characters": [
            {
                "name": "중복씬인물",
                "description": "20대 여성",
                "visual_traits": ["갈색 머리"],
                "scene_appearances": [3, 3, 3],  # unique 1개 → 필터됨
            },
        ]
    }

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

        result = extract_entities_by_type("시나리오", "character")
        assert len(result) == 0


def test_filter_keeps_entity_with_exactly_two_scenes():
    """정확히 2개 씬에 등장하면 유지."""
    from app.modules.pipeline.entity_extractor_v4 import extract_entities_by_type

    mock_result = {
        "characters": [
            {
                "name": "경계인물",
                "description": "40대 남성",
                "visual_traits": ["수염"],
                "scene_appearances": [2, 5],
            },
        ]
    }

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

        result = extract_entities_by_type("시나리오", "character")
        assert len(result) == 1
        assert result[0]["name"] == "경계인물"


# ── Test: extract_all_entities calls 3 times ──

def test_extract_all_entities_calls_three_types():
    """extract_all_entities가 character, location, prop 순서로 3회 호출."""
    from app.modules.pipeline.entity_extractor_v4 import extract_all_entities

    call_count = {"n": 0}
    type_order = []

    def mock_extract(fulltext, entity_type, visual_rules="", segments_json="",
                     project_config=None, opik_metadata=None):
        call_count["n"] += 1
        type_order.append(entity_type)
        return [{"name": f"{entity_type}_item", "description": "test",
                 "visual_traits": [], "scene_appearances": [1, 2]}]

    with patch("app.modules.pipeline.entity_extractor_v4.extract_entities_by_type",
               side_effect=mock_extract) as mock_fn:

        result = extract_all_entities("시나리오 전문", "rules", "segments")

        assert call_count["n"] == 3
        assert type_order == ["character", "location", "prop"]
        assert "characters" in result
        assert "locations" in result
        assert "props" in result
        assert len(result["characters"]) == 1
        assert len(result["locations"]) == 1
        assert len(result["props"]) == 1


def test_extract_all_entities_passes_params():
    """extract_all_entities가 visual_rules, segments_json 등을 전달하는지 확인."""
    from app.modules.pipeline.entity_extractor_v4 import extract_all_entities

    with patch("app.modules.pipeline.entity_extractor_v4.extract_entities_by_type",
               return_value=[]) as mock_fn:

        extract_all_entities(
            "fulltext_data",
            visual_rules="rule1",
            segments_json="[{\"id\":1}]",
            project_config={"entity_extract": {"model": "gpt"}},
            opik_metadata={"tags": ["test"]},
        )

        assert mock_fn.call_count == 3
        for c in mock_fn.call_args_list:
            args, kwargs = c
            assert args[0] == "fulltext_data"
            # entity_type is args[1], varies per call
            assert args[2] == "rule1"
            assert args[3] == "[{\"id\":1}]"
            assert args[4] == {"entity_extract": {"model": "gpt"}}
            assert args[5] == {"tags": ["test"]}
