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

검증 대상:
1. Public API 위임 계약: generate_base_references → skip_composite=True / generate_composites → skip_composite=False
2. generate_reference_images_only 시그니처 (api/v1/images.py 및 scripts 호환)
3. _validate_reference: 정상/낮은 점수/파일 없음/예외 경로
4. ImageService helper shim passthrough (image_service_helpers 위임)
5. 공개 API 시그니처 보존 (facade delegate 포함)

composite 관련 케이스(generate_composite_image 에러 경로,
regenerate_composites_for_entity 분기/O00 스킵)는
tests/services/test_reference_composite_service.py로 이관.
"""
from __future__ import annotations

import inspect
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from app.services.image_service import ImageService
from app.services.reference_image_service import ReferenceImageService


# ──────────────────────────────────────────────────────────────────────
# Fixture — 최소 mock으로 구성된 ReferenceImageService 인스턴스
# ──────────────────────────────────────────────────────────────────────


@pytest.fixture
def svc() -> ReferenceImageService:
    """생성자 전체 실행 없이 필요한 속성만 주입."""
    instance = ReferenceImageService.__new__(ReferenceImageService)
    instance._db = MagicMock()
    instance._project_id = "p1"
    instance._actor_id = "u1"
    instance._logger = MagicMock()
    return instance


# ──────────────────────────────────────────────────────────────────────
# 1) generate_base_references — skip_composite=True 강제
# ──────────────────────────────────────────────────────────────────────


def test_generate_base_references_delegates_with_skip_composite_true(svc, monkeypatch):
    captured = {}

    def _fake(episode_id, ip=None, mode="resume", *, skip_composite=False):
        captured.update({
            "episode_id": episode_id,
            "ip": ip,
            "mode": mode,
            "skip_composite": skip_composite,
        })
        return {"reference_count": 5, "skipped_composite_phase": True}

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

    result = svc.generate_base_references("e1", mode="resume", ip="127.0.0.1")

    assert captured == {
        "episode_id": "e1",
        "ip": "127.0.0.1",
        "mode": "resume",
        "skip_composite": True,
    }
    assert result["skipped_composite_phase"] is True


def test_generate_base_references_default_mode_resume(svc, monkeypatch):
    captured = {}

    def _fake(episode_id, ip=None, mode="resume", *, skip_composite=False):
        captured["mode"] = mode
        return {}

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

    svc.generate_base_references("e1")

    assert captured["mode"] == "resume"


# ──────────────────────────────────────────────────────────────────────
# 2) generate_composites — skip_composite=False 기본
# ──────────────────────────────────────────────────────────────────────


def test_generate_composites_delegates_with_skip_composite_false(svc, monkeypatch):
    captured = {}

    def _fake(episode_id, ip=None, mode="resume", *, skip_composite=False):
        captured.update({
            "episode_id": episode_id,
            "ip": ip,
            "mode": mode,
            "skip_composite": skip_composite,
        })
        return {"composite_generated": 3}

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

    result = svc.generate_composites("e1", mode="resume")

    assert captured["skip_composite"] is False
    assert captured["mode"] == "resume"
    assert result["composite_generated"] == 3


def test_generate_composites_full_mode_passthrough(svc, monkeypatch):
    captured = {}

    def _fake(episode_id, ip=None, mode="resume", *, skip_composite=False):
        captured["mode"] = mode
        return {}

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

    svc.generate_composites("e1", mode="resume")

    assert captured["mode"] == "resume"


# ──────────────────────────────────────────────────────────────────────
# 3) 시그니처 하위 호환
# ──────────────────────────────────────────────────────────────────────


def test_generate_reference_images_only_preserved_signature():
    """기존 호출자(backend/scripts/experiment_*) 호환을 위해 skip_composite KW-only 유지."""
    sig = inspect.signature(ReferenceImageService.generate_reference_images_only)
    params = sig.parameters
    assert "self" in params
    assert "episode_id" in params
    assert "ip" in params
    assert "mode" in params
    assert "skip_composite" in params
    assert params["skip_composite"].kind == inspect.Parameter.KEYWORD_ONLY
    assert params["skip_composite"].default is False


# ──────────────────────────────────────────────────────────────────────
# 4) _validate_reference — validator 결과별 동작
# ──────────────────────────────────────────────────────────────────────


def test_validate_reference_missing_file_returns_unchanged(svc, tmp_path):
    """파일 없으면 None/None + 원본 status/notes 유지."""
    validator = MagicMock()
    result = svc._validate_reference(
        validator, str(tmp_path / "nope.png"),
        entity_info={}, status="generated", review_notes="original",
    )
    assert result == (None, None, "generated", "original")
    validator.validate_reference_image.assert_not_called()


def test_validate_reference_high_score_keeps_status(svc, tmp_path):
    """score ≥ 60 → status/notes 유지."""
    fp = tmp_path / "ok.png"
    fp.write_bytes(b"fake_png")

    validator = MagicMock()
    validator.validate_reference_image.return_value = {"score": 85, "passed": True, "issues": []}

    score, result_json, status, notes = svc._validate_reference(
        validator, str(fp),
        entity_info={"name": "x"}, status="generated", review_notes="",
    )
    assert score == 85
    assert status == "generated"
    assert "85" in result_json


def test_validate_reference_low_score_flags_needs_fix(svc, tmp_path):
    """score < 60 → status='needs_fix', notes에 이슈 포함."""
    fp = tmp_path / "bad.png"
    fp.write_bytes(b"fake_png")

    validator = MagicMock()
    validator.validate_reference_image.return_value = {
        "score": 42, "passed": False, "issues": ["blurry", "off-model"],
    }

    score, result_json, status, notes = svc._validate_reference(
        validator, str(fp),
        entity_info={"name": "x"}, status="generated", review_notes="",
    )
    assert score == 42
    assert status == "needs_fix"
    assert "42" in notes
    assert "blurry" in notes
    assert "off-model" in notes


def test_validate_reference_exception_returns_none(svc, tmp_path):
    """validator 예외 → None/None + 원본 status/notes 유지 (graceful degradation)."""
    fp = tmp_path / "err.png"
    fp.write_bytes(b"fake_png")

    validator = MagicMock()
    validator.validate_reference_image.side_effect = RuntimeError("api down")

    result = svc._validate_reference(
        validator, str(fp),
        entity_info={}, status="generated", review_notes="original",
    )
    assert result == (None, None, "generated", "original")


# ──────────────────────────────────────────────────────────────────────
# 5) ImageService shim → module helper passthrough
# ──────────────────────────────────────────────────────────────────────


def test_image_service_image_to_dict_shim_passthrough():
    """ImageService._image_to_dict(img) → image_service_helpers.image_to_dict(img)."""
    img = MagicMock()
    with patch("app.services.image_service_helpers.image_to_dict") as mock_helper:
        mock_helper.return_value = {"id": "x"}
        result = ImageService._image_to_dict(img)
        mock_helper.assert_called_once_with(img)
        assert result == {"id": "x"}




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


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


def test_generate_base_references_signature():
    """image_steps.py::RefImageGenStep 호환."""
    assert _param_names(ReferenceImageService.generate_base_references) == [
        "episode_id", "mode", "ip",
    ]


def test_generate_composites_signature():
    """image_steps.py::CompositeImageGenStep 호환."""
    assert _param_names(ReferenceImageService.generate_composites) == [
        "episode_id", "mode", "ip",
    ]


def test_generate_single_entity_image_signature():
    """api/v1/images.py::generate_entity_image 호환."""
    assert _param_names(ReferenceImageService.generate_single_entity_image) == [
        "entity_id", "custom_prompt", "ip",
    ]


def test_generate_composite_image_signature():
    """api/v1/images.py::generate_composite_image 호환."""
    # ★`episode_id` 가 **서명에 있어야** 한다 (2026-09-04). 없으면 서비스가
    #  「이 합성을 어느 화 아래에 적을지」를 링크에서 임의로 고르게 되고,
    #  공유 인물의 2화 옷이 1화 폴더·DB·trace 에 기록된다.
    assert _param_names(ReferenceImageService.generate_composite_image) == [
        "character_id", "outlook_id", "ip", "episode_id",
    ]


def test_regenerate_composites_for_entity_signature():
    """api/v1/images.py::_trigger_composite_regen 호환."""
    assert _param_names(ReferenceImageService.regenerate_composites_for_entity) == [
        "entity_id", "ip",
    ]
