"""SceneImageService 단위 테스트 — Phase 3b.3 / 3b.5b.

검증 대상:
1. _validate_scene 경로별 동작 (파일 없음 / 고득점 / 저득점 needs_fix / 예외)
2. select_variant / select_original 에러 경로 (still not found / image not found / variant not found)
3. _get_visible_entities / _get_reference_image_map 파싱 동작
4. recommend_variations 최소 경로 (DB 없는 still)
5. 공개 API 시그니처 보존 (api/v1/images.py + image_steps.py 호환 계약)
"""
from __future__ import annotations

import inspect
from unittest.mock import MagicMock

import pytest

from app.services.scene_image_service import SceneImageService


# ──────────────────────────────────────────────────────────────────────
# Fixtures
# ──────────────────────────────────────────────────────────────────────


@pytest.fixture
def svc() -> SceneImageService:
    instance = SceneImageService.__new__(SceneImageService)
    instance._db = MagicMock()
    instance._project_id = "p1"
    instance._actor_id = "u1"
    instance._logger = MagicMock()
    return instance


# ──────────────────────────────────────────────────────────────────────
# 1) _validate_scene — validator 결과별 동작
# ──────────────────────────────────────────────────────────────────────


# W5 F22 Phase B.6: _validate_scene 로직 테스트는
# tests/services/test_scene_validation_service.py로 이관됨.


# W5 F22 Phase C (2026-04-22): _validate_scene / _create_validator shim 제거.
# 호출자는 self._validation_svc.{validate_scene,create_validator}를 직접 호출.
# 로직 테스트는 tests/services/test_scene_validation_service.py에 있음.

# W5 F22 Phase B.8: _get_visible_entities / _get_reference_image_map 로직 테스트는
# tests/services/test_scene_reference_service.py로 이관됨.


# W5 F22 Phase C (2026-04-22): _get_visible_entities / _get_reference_image_map /
# _build_scene_ref_image_map / _resolve_refs_for_prompt / _build_image_index /
# _rewrite_t2i_with_image_refs shim 전부 제거. 호출자는 self._reference_svc 또는
# module-level helper를 직접 호출. 로직 테스트는 test_scene_reference_service.py에 있음.


# ──────────────────────────────────────────────────────────────────────
# 3) select_variant / select_original — 에러 경로
# ──────────────────────────────────────────────────────────────────────


# W5 F22 Phase B.1: select_variant / select_original 로직 테스트는
# tests/services/test_scene_variation_service.py로 이관됨. facade는 아래
# delegation smoke test로만 커버한다.


def test_select_variant_delegates_to_variation_svc(svc):
    """select_variant가 self._variation_svc.select_variant로 위임되는지 검증."""
    svc._variation_svc = MagicMock()
    svc._variation_svc.select_variant.return_value = {"still_id": "s1", "selected_variant": "A"}

    result = svc.select_variant("s1", "A", ip="1.2.3.4")

    svc._variation_svc.select_variant.assert_called_once_with("s1", "A", ip="1.2.3.4")
    assert result == {"still_id": "s1", "selected_variant": "A"}


def test_select_original_delegates_to_variation_svc(svc):
    """select_original이 self._variation_svc.select_original로 위임되는지 검증."""
    svc._variation_svc = MagicMock()
    svc._variation_svc.select_original.return_value = {"id": "img1"}

    result = svc.select_original("s1", "img1", ip="1.2.3.4")

    svc._variation_svc.select_original.assert_called_once_with("s1", "img1", ip="1.2.3.4")
    assert result == {"id": "img1"}


def test_regenerate_variation_delegates_to_variation_svc(svc):
    """regenerate_variation이 self._variation_svc.regenerate_variation으로 위임 (W5 F22 Phase B.2)."""
    svc._variation_svc = MagicMock()
    svc._variation_svc.regenerate_variation.return_value = {"id": "new_img"}

    result = svc.regenerate_variation(
        "img1",
        angle_json='{"horizontal":10,"vertical":0,"zoom":1.0}',
        color_prompt=None,
        ip="1.2.3.4",
    )

    svc._variation_svc.regenerate_variation.assert_called_once_with(
        "img1",
        angle_json='{"horizontal":10,"vertical":0,"zoom":1.0}',
        color_prompt=None,
        ip="1.2.3.4",
    )
    assert result == {"id": "new_img"}


# W5 F22 Phase B.3: recommend_variations 오류 경로 테스트는
# tests/services/test_scene_variation_service.py로 이관됨.


def test_recommend_variations_delegates_to_variation_svc(svc):
    """recommend_variations가 self._variation_svc.recommend_variations로 위임 (W5 F22 Phase B.3)."""
    svc._variation_svc = MagicMock()
    svc._variation_svc.recommend_variations.return_value = {"recommended": "A"}

    result = svc.recommend_variations("still1", ip="1.2.3.4")

    svc._variation_svc.recommend_variations.assert_called_once_with("still1", ip="1.2.3.4")
    assert result == {"recommended": "A"}


def test_generate_variations_only_delegates_to_variation_svc(svc):
    """generate_variations_only가 self._variation_svc.generate_variations_only로 위임 (W5 F22 Phase B.4)."""
    svc._variation_svc = MagicMock()
    svc._variation_svc.generate_variations_only.return_value = None

    result = svc.generate_variations_only("ep_1")

    svc._variation_svc.generate_variations_only.assert_called_once_with("ep_1")
    assert result is None


# W5 F22 Phase B.8: _get_reference_image_map 로직 테스트는
# tests/services/test_scene_reference_service.py로 이관됨 (delegation smoke만 상단 유지).


# ──────────────────────────────────────────────────────────────────────
# 5) 공개 API 시그니처 보존 — Phase 3b.5b wrapper 제거 후 회귀 방지
# ──────────────────────────────────────────────────────────────────────
#
# api/v1/images.py 및 core/steps/image_steps.py가 SceneImageService의 각 메서드를
# 키워드로 직접 호출하므로, 파라미터 이름/순서를 계약으로 고정한다.


_REQUIRED = object()


def _param_names(method) -> list[str]:
    """self 제외한 파라미터 이름 목록."""
    params = list(inspect.signature(method).parameters.keys())
    return [p for p in params if p != "self"]


def _param_spec(method) -> list[tuple]:
    """(name, default, kind) tuple 목록 — Phase A freeze 강화 (W5 F22).

    Codex F22 Phase A Medium 1: defaults/keyword-safety까지 freeze.
    Phase B refactor가 required ↔ optional을 바꾸면 이 test가 실패한다.
    """
    specs = []
    for name, p in inspect.signature(method).parameters.items():
        if name == "self":
            continue
        default = _REQUIRED if p.default is inspect.Parameter.empty else p.default
        specs.append((name, default, p.kind))
    return specs


_POK = inspect.Parameter.POSITIONAL_OR_KEYWORD


def test_get_image_signature():
    """api/v1/images.py GET /images/{id} 경로 호환 — W5 F22 Phase A 추가."""
    assert _param_spec(SceneImageService.get_image) == [
        ("image_id", _REQUIRED, _POK),
    ]


def test_generate_images_signature():
    """api/v1/images.py::_run_image_gen_in_background + image_steps.py::SceneImagePipelineStep 호환.

    W5 F22 Phase A: ip/mode는 optional로 freeze. image_steps.py:317는 mode만 지정.
    """
    assert _param_spec(SceneImageService.generate_images) == [
        ("episode_id", _REQUIRED, _POK),
        ("ip", None, _POK),
        ("mode", "resume", _POK),
    ]


def test_generate_variations_only_signature():
    """api/v1/images.py::_run_var_gen 호환."""
    assert _param_spec(SceneImageService.generate_variations_only) == [
        ("episode_id", _REQUIRED, _POK),
    ]


def test_generate_single_scene_image_signature():
    """api/v1/images.py::generate_still_image 호환.

    W5 F22 Phase A: custom_prompt/ip는 optional로 freeze. scene_image_service.py:2211
    같은 내부 호출이 still_id만 지정하고 생략 가능.
    """
    assert _param_spec(SceneImageService.generate_single_scene_image) == [
        ("still_id", _REQUIRED, _POK),
        ("custom_prompt", None, _POK),
        ("ip", None, _POK),
    ]


def test_generate_scene_with_variations_signature():
    """api/v1/images.py::generate_with_variations 호환."""
    assert _param_spec(SceneImageService.generate_scene_with_variations) == [
        ("still_id", _REQUIRED, _POK),
        ("ip", None, _POK),
    ]


def test_recommend_variations_signature():
    """api/v1/images.py::recommend_variations 호환."""
    assert _param_spec(SceneImageService.recommend_variations) == [
        ("still_id", _REQUIRED, _POK),
        ("ip", None, _POK),
    ]


def test_regenerate_variation_signature():
    """api/v1/images.py::edit_angle / edit_color 호환.

    Codex Phase 3b.3 리뷰 Important #1 + W5 F22 Phase A:
    angle_json/color_prompt/ip 모두 optional로 freeze.
    주의: color_prompt 파라미터는 현재 구현에서 미사용(angle-only). Phase B에서
    color route 구현 시 semantics 추가.
    """
    assert _param_spec(SceneImageService.regenerate_variation) == [
        ("image_id", _REQUIRED, _POK),
        ("angle_json", None, _POK),
        ("color_prompt", None, _POK),
        ("ip", None, _POK),
    ]


def test_select_variant_signature():
    """api/v1/images.py::select_variant 호환."""
    assert _param_spec(SceneImageService.select_variant) == [
        ("still_id", _REQUIRED, _POK),
        ("variant", _REQUIRED, _POK),
        ("ip", None, _POK),
    ]


def test_select_original_signature():
    """api/v1/images.py::select_original 호환."""
    assert _param_spec(SceneImageService.select_original) == [
        ("still_id", _REQUIRED, _POK),
        ("image_id", _REQUIRED, _POK),
        ("ip", None, _POK),
    ]


# W5 F22 Phase C (2026-04-22): _set_variant_primary / _generate_variation shim 제거.
# 호출자는 module-level _set_variant_primary_helper + self._variation_svc._generate_variation_i2i를 직접 호출.
# 로직 테스트는 test_scene_variation_service.py 유지.



# ──────────────────────────────────────────────────────────────────────
# W5 F22 Phase B.25 — SceneGenerationCoordinator 주입 smoke
# ──────────────────────────────────────────────────────────────────────


def test_service_init_attaches_coordinator():
    """SceneImageService.__init__이 self._coord를 SceneGenerationCoordinator로 주입."""
    from unittest.mock import MagicMock as _MM
    from app.services.scene_generation_coordinator import SceneGenerationCoordinator

    svc_instance = SceneImageService(db=_MM(), project_id="p1", actor_id="u1")
    assert isinstance(svc_instance._coord, SceneGenerationCoordinator)
    # coord가 facade와 동일 db/project_id/actor_id 공유
    assert svc_instance._coord._db is svc_instance._db
    assert svc_instance._coord._project_id == "p1"
    assert svc_instance._coord._actor_id == "u1"
    # 5 svc 레퍼런스 공유 (동일 인스턴스)
    assert svc_instance._coord._persistence_svc is svc_instance._persistence_svc
    assert svc_instance._coord._reference_svc is svc_instance._reference_svc
    assert svc_instance._coord._validation_svc is svc_instance._validation_svc
    assert svc_instance._coord._variation_svc is svc_instance._variation_svc
    assert svc_instance._coord._provenance_svc is svc_instance._provenance_svc


# ──────────────────────────────────────────────────────────────────────
# 2026-05-10 — Fix C — typed failure summary helper
#
# scene_image_service 가 var_results_list 안 typed failure dict 들을 분리해
# `mark_failed` 메시지로 직렬화하는 helper. 24 shot deterministic 실패에서
# RefContractError 메시지가 cp.failed 에 보존되어 root cause 가시화.
# ──────────────────────────────────────────────────────────────────────


def test_summarize_failed_variations_returns_none_when_any_success():
    """성공 dict 가 1개라도 있으면 mark_failed 호출 안 함 → None 반환."""
    from app.services.scene_image_service import _summarize_failed_variations

    var_results_list = [
        {"id": "ok1", "asset_type": "scene", "still_id": "s1"},
        {"_failure_reason": "REF_CONTRACT_VIOLATION", "_failure_detail": "x"},
    ]
    msg = _summarize_failed_variations(var_results_list, scene_index=8)
    assert msg is None


def test_summarize_failed_variations_serializes_ref_contract_reason():
    """모두 typed failure → reason 들을 join 하여 cp.failed msg 작성."""
    from app.services.scene_image_service import _summarize_failed_variations

    var_results_list = [
        {
            "_failure_reason": "REF_CONTRACT_VIOLATION",
            "_failure_detail": "required character_outlook 'C01O01' missing",
        },
        {
            "_failure_reason": "REF_CONTRACT_VIOLATION",
            "_failure_detail": "required character_outlook 'C01O01' missing",
        },
    ]
    msg = _summarize_failed_variations(var_results_list, scene_index=5)
    assert msg is not None
    assert "scene_index=5" in msg
    assert "REF_CONTRACT_VIOLATION" in msg
    assert "C01O01" in msg


def test_summarize_failed_variations_empty_list_falls_back_to_unknown():
    """var_results_list 가 빈 list 면 (모든 future None 등) generic msg."""
    from app.services.scene_image_service import _summarize_failed_variations

    msg = _summarize_failed_variations([], scene_index=12)
    assert msg is not None
    assert "scene_index=12" in msg
    assert ("unknown" in msg.lower()) or ("no failure" in msg.lower())


def test_summarize_failed_variations_caps_msg_length():
    """매우 긴 detail 들이 있어도 cp.failed msg 가 적당히 truncate 되어 cp 손상 회피."""
    from app.services.scene_image_service import _summarize_failed_variations

    var_results_list = [
        {"_failure_reason": "REF_CONTRACT_VIOLATION", "_failure_detail": "x" * 2000},
    ] * 10
    msg = _summarize_failed_variations(var_results_list, scene_index=1)
    assert msg is not None
    assert len(msg) <= 800  # 운영자 readable 한 적정 한도
