"""ReferenceCompositeService 테스트 (W5 F24 Phase 3).

reference_image_service.py의 generate_composite_image +
regenerate_composites_for_entity를 ReferenceCompositeService로 이관.
"""
from __future__ import annotations

from unittest.mock import MagicMock

import pytest

from app.core.errors import AppError
from app.services.reference_composite_service import ReferenceCompositeService


@pytest.fixture
def svc() -> ReferenceCompositeService:
    """ReferenceCompositeService MagicMock 주입 인스턴스."""
    instance = ReferenceCompositeService.__new__(ReferenceCompositeService)
    instance._db = MagicMock()
    instance._project_id = "p1"
    instance._actor_id = "u1"
    instance._logger = MagicMock()
    return instance


def _build_query_side_effect(entity_canon_firsts, combos):
    """db.query.side_effect용 factory — EntityCanon first() 호출 시 iterator에서 순차 반환."""
    from app.models.project import CharacterOutlook, EntityCanon
    entity_iter = iter(entity_canon_firsts)

    def _side_effect(model):
        query_mock = MagicMock()
        if model is EntityCanon:
            query_mock.filter.return_value.first.side_effect = lambda: next(entity_iter)
        elif model is CharacterOutlook:
            query_mock.filter.return_value.all.return_value = combos
        return query_mock
    return _side_effect


def _stub_gemini_key(monkeypatch):
    import app.services.reference_composite_service as comp_mod
    monkeypatch.setattr(comp_mod.settings, "gemini_api_key", "fake-key")


# ──────────────────────────────────────────────────────────────────────
# regenerate_composites_for_entity — 분기 및 O00 스킵
# ──────────────────────────────────────────────────────────────────────


def test_regenerate_composites_unknown_entity_returns_empty(svc):
    """프로젝트에 없는 entity_id → 빈 리스트."""
    query_mock = MagicMock()
    query_mock.filter.return_value.first.return_value = None
    svc._db.query.return_value = query_mock

    result = svc.regenerate_composites_for_entity("missing_id")
    assert result == []


def test_regenerate_composites_character_iterates_outlooks(svc, monkeypatch):
    """character entity → 연관 outlook 전부 generate_composite_image 호출."""
    char_entity = MagicMock(entity_type="character", short_id="C01")
    outlook_a = MagicMock(short_id="O01")
    outlook_b = MagicMock(short_id="O02")
    # ★배정마다 **제 화**를 준다 (2026-09-04). 안 주면 「어느 화 아래에
    #  적히는가」를 잴 수 없고, 앞 판은 임의의 첫 링크를 집었다.
    combos = [
        MagicMock(outlook_id="oid_a", character_id="char_1", episode_id="ep1"),
        MagicMock(outlook_id="oid_b", character_id="char_1", episode_id="ep2"),
    ]

    svc._db.query.side_effect = _build_query_side_effect(
        [char_entity, outlook_a, outlook_b], combos,
    )

    captured_calls = []

    def _fake_composite(char_id, outlook_id, ip=None, episode_id=None):
        captured_calls.append((char_id, outlook_id, episode_id))
        return {"id": f"asset_{outlook_id}"}

    monkeypatch.setattr(svc, "generate_composite_image", _fake_composite)

    result = svc.regenerate_composites_for_entity("char_1")

    assert len(captured_calls) == 2
    # ★각 배정의 **제 화**가 그대로 실려야 한다 — 안 실으면 저장 자리를
    #  임의로 고르게 되고 2화 옷이 1화 폴더에 들어간다.
    assert captured_calls[0] == ("char_1", "oid_a", "ep1")
    assert captured_calls[1] == ("char_1", "oid_b", "ep2")
    assert len(result) == 2


def test_regenerate_composites_character_skips_o00_outlook(svc, monkeypatch):
    """outlook이 O00이면 generate_composite_image 호출하지 않음 (비인간형)."""
    char_entity = MagicMock(entity_type="character", short_id="C01")
    outlook_null = MagicMock(short_id="O00")
    outlook_normal = MagicMock(short_id="O01")
    combos = [
        MagicMock(outlook_id="oid_null", character_id="char_1", episode_id="ep1"),
        MagicMock(outlook_id="oid_norm", character_id="char_1", episode_id="ep1"),
    ]

    svc._db.query.side_effect = _build_query_side_effect(
        [char_entity, outlook_null, outlook_normal], combos,
    )

    captured_calls = []

    def _fake_composite(char_id, outlook_id, ip=None, episode_id=None):
        captured_calls.append((char_id, outlook_id, episode_id))
        return {}

    monkeypatch.setattr(svc, "generate_composite_image", _fake_composite)

    svc.regenerate_composites_for_entity("char_1")

    assert captured_calls == [("char_1", "oid_norm", "ep1")]


def test_regenerate_composites_o00_outlook_entity_returns_empty(svc):
    """entity_type='outlook' 이면서 short_id='O00' → 재생성 불가 (빈 리스트 즉시 반환)."""
    null_outlook = MagicMock(entity_type="outlook", short_id="O00")

    query_mock = MagicMock()
    query_mock.filter.return_value.first.return_value = null_outlook
    svc._db.query.return_value = query_mock

    result = svc.regenerate_composites_for_entity("null_outlook_id")
    assert result == []


def test_regenerate_composites_outlook_entity_iterates_characters(svc, monkeypatch):
    """outlook 엔티티(not O00) → 연관 character 모두 generate_composite_image 호출."""
    outlook = MagicMock(entity_type="outlook", short_id="O01")
    # ★같은 옷을 두 사람이 **다른 화**에서 입는다 — 각자 제 화로 가야 한다.
    combos = [
        MagicMock(character_id="char_a", outlook_id="out_x", episode_id="ep1"),
        MagicMock(character_id="char_b", outlook_id="out_x", episode_id="ep2"),
    ]
    svc._db.query.side_effect = _build_query_side_effect([outlook], combos)

    captured = []

    def _fake(char_id, outlook_id, ip=None, episode_id=None):
        captured.append((char_id, outlook_id, episode_id))
        return {"id": f"asset_{char_id}"}

    monkeypatch.setattr(svc, "generate_composite_image", _fake)

    result = svc.regenerate_composites_for_entity("out_x")

    assert captured == [("char_a", "out_x", "ep1"),
                        ("char_b", "out_x", "ep2")]
    assert len(result) == 2


# ──────────────────────────────────────────────────────────────────────
# generate_composite_image — 에러 경로
# ──────────────────────────────────────────────────────────────────────


def test_generate_composite_image_raises_when_entity_missing(svc, monkeypatch):
    """char 엔티티 없음 → entity.not_found."""
    _stub_gemini_key(monkeypatch)

    query_mock = MagicMock()
    query_mock.filter.return_value.first.side_effect = [None, None]
    svc._db.query.return_value = query_mock

    with pytest.raises(AppError) as exc_info:
        svc.generate_composite_image("char1", "out1")
    assert exc_info.value.code == "entity.not_found"


def test_generate_composite_image_raises_when_outlook_is_o00(svc, monkeypatch):
    """outlook.short_id == O00 → image.null_outlook."""
    _stub_gemini_key(monkeypatch)

    char_ent = MagicMock(short_id="C01")
    char_ent.name = "TestChar"
    outlook_ent = MagicMock(short_id="O00")

    query_mock = MagicMock()
    query_mock.filter.return_value.first.side_effect = [char_ent, outlook_ent]
    svc._db.query.return_value = query_mock

    with pytest.raises(AppError) as exc_info:
        svc.generate_composite_image("char1", "out1")
    assert exc_info.value.code == "image.null_outlook"


def test_generate_composite_image_raises_when_face_missing(svc, monkeypatch):
    """face asset 없음 → image.face_missing. HEAD 동작 보존 — `[composite:`만 제외."""
    _stub_gemini_key(monkeypatch)

    char_ent = MagicMock(short_id="C01")
    char_ent.name = "TestChar"
    outlook_ent = MagicMock(short_id="O01")
    outlook_ent.name = "TestOutfit"

    query_iter = iter([char_ent, outlook_ent, None])

    def _query_side(model):
        q = MagicMock()
        q.filter.return_value.first.side_effect = lambda: next(query_iter)
        return q

    svc._db.query.side_effect = _query_side

    with pytest.raises(AppError) as exc_info:
        svc.generate_composite_image("char1", "out1")
    assert exc_info.value.code == "image.face_missing"


# ──────────────────────────────────────────────────────────────────────
# Facade delegation
# ──────────────────────────────────────────────────────────────────────


def test_reference_image_service_facade_injects_composite_svc():
    """ReferenceImageService가 _composite_svc를 주입하고 delegate가 동일 함수 호출."""
    from app.services.reference_image_service import ReferenceImageService

    facade = ReferenceImageService.__new__(ReferenceImageService)
    facade._db = MagicMock()
    facade._project_id = "p1"
    facade._actor_id = "u1"
    facade._logger = MagicMock()
    facade._composite_svc = MagicMock(spec=ReferenceCompositeService)

    # ★facade 는 화를 **그대로 흘려야** 한다 — 여기서 끊기면 서비스가
    #  저장 자리를 임의로 고른다.
    facade.generate_composite_image("char", "out", ip="1.2.3.4", episode_id="ep7")
    facade._composite_svc.generate_composite_image.assert_called_once_with(
        "char", "out", ip="1.2.3.4", episode_id="ep7",
    )

    facade.regenerate_composites_for_entity("ent", ip="9.9.9.9")
    facade._composite_svc.regenerate_composites_for_entity.assert_called_once_with(
        "ent", ip="9.9.9.9",
    )


def test_reference_image_service_constructor_wires_composite_svc():
    """ReferenceImageService(__init__)가 실제 ReferenceCompositeService 인스턴스를 주입한다."""
    from app.services.reference_image_service import ReferenceImageService

    db = MagicMock()
    facade = ReferenceImageService(db=db, project_id="p1", actor_id="u1")

    assert isinstance(facade._composite_svc, ReferenceCompositeService)
    assert facade._composite_svc._db is db
    assert facade._composite_svc._project_id == "p1"
    assert facade._composite_svc._actor_id == "u1"
    assert facade._composite_svc._logger is facade._logger
