"""ImageResponse.actual_attached_refs + image_to_dict join — Task 5.

spec: docs/superpowers/specs/2026-05-08-single-batch-reference-contract-design.md §4.4
plan: docs/superpowers/plans/2026-05-08-single-batch-reference-contract-implementation.md Task 5

가드:
- ImageResponse field 추가 (actual_attached_refs: Optional[list[str]])
- reference_image_ids description 에 "(lineage)" 명시
- image_to_dict 가 db + actual_refs_cache 받음 (N+1 회피)
- _lookup_actual_refs_batch 신규 — episode 전체 still_id 1 query
- alembic 0 (DB schema 변경 X)
- functional index 추가 X
"""
from __future__ import annotations

from unittest.mock import MagicMock

import pytest


# ──────────────────────────────────────────────────────────────────────
# ImageResponse schema
# ──────────────────────────────────────────────────────────────────────


def test_image_response_has_actual_attached_refs_field():
    """ImageResponse 에 actual_attached_refs field 존재 + Optional[list[str]]."""
    from app.schemas.image import ImageResponse
    schema = ImageResponse.model_json_schema()
    props = schema["properties"]
    assert "actual_attached_refs" in props


def test_image_response_reference_image_ids_description_mentions_lineage():
    """reference_image_ids field description 에 'lineage' 명시."""
    from app.schemas.image import ImageResponse
    schema = ImageResponse.model_json_schema()
    props = schema["properties"]
    desc = (props.get("reference_image_ids", {}).get("description") or "").lower()
    assert "lineage" in desc, (
        f"reference_image_ids description must mention 'lineage' — got: {desc!r}"
    )


def test_image_response_default_actual_attached_refs_is_none():
    """ImageResponse 인스턴스 생성 시 actual_attached_refs default = None."""
    from app.schemas.image import ImageResponse
    img = ImageResponse(
        id="i1", asset_type="scene", file_path="x.png",
        status="generated", review_notes="{}", created_at="2026-05-09T00:00:00",
    )
    assert img.actual_attached_refs is None


# ──────────────────────────────────────────────────────────────────────
# image_to_dict — actual_refs_cache (batch) + db (single) 양쪽 path
# ──────────────────────────────────────────────────────────────────────


def _make_img_mock(*, still_id: str = "s1", project_id: str = "p1", episode_id: str = "e1"):
    img = MagicMock()
    img.id = "i1"
    img.asset_type = "scene"
    img.entity_id = None
    img.still_id = still_id
    img.episode_id = episode_id
    img.project_id = project_id
    img.file_path = ""
    img.prompt_used = ""
    img.generation_model = ""
    img.width = None
    img.height = None
    img.status = "generated"
    img.review_notes = "{}"
    img.validation_score = None
    img.validation_result = None
    img.sanitization_strategy = None
    img.original_prompt = None
    img.sanitization_note = None
    img.variant_type = None
    img.angle_applied = None
    img.color_applied = None
    img.source_image_id = None
    img.is_primary = 1
    img.prompt_type = None
    img.code_version = None
    img.prompt_file_version = None
    img.reference_image_ids = '["c01"]'
    img.theme_label = None
    img.created_at = "2026-05-09T00:00:00"
    return img


def test_image_to_dict_with_cache_returns_actual_refs():
    """actual_refs_cache 전달 시 image_to_dict 가 cache 의 still_id entry 반환."""
    from app.services.image_service_helpers import image_to_dict
    img = _make_img_mock(still_id="s1")
    cache = {"s1": ["character C01O02 in outfit", "BACKGROUND chain reference"]}

    result = image_to_dict(img, actual_refs_cache=cache)

    assert result["actual_attached_refs"] == ["character C01O02 in outfit", "BACKGROUND chain reference"]
    assert result["reference_image_ids"] == '["c01"]'  # lineage preserved


def test_image_to_dict_cache_miss_returns_none():
    """still_id 가 cache 에 없으면 actual_attached_refs = None."""
    from app.services.image_service_helpers import image_to_dict
    img = _make_img_mock(still_id="s1")
    cache = {"s2": ["other"]}  # different still_id

    result = image_to_dict(img, actual_refs_cache=cache)

    assert result["actual_attached_refs"] is None


def test_image_to_dict_no_cache_no_db_returns_none():
    """cache + db 둘 다 None → actual_attached_refs = None (backward compat)."""
    from app.services.image_service_helpers import image_to_dict
    img = _make_img_mock()

    result = image_to_dict(img)

    assert result["actual_attached_refs"] is None
    assert result["reference_image_ids"] == '["c01"]'


# ──────────────────────────────────────────────────────────────────────
# _lookup_actual_refs_batch — N+1 회피 (1 query 로 episode 전체)
# ──────────────────────────────────────────────────────────────────────


def test_lookup_actual_refs_batch_empty_still_ids_returns_empty():
    """still_ids 빈 리스트 → 빈 dict."""
    from app.services.image_service_helpers import _lookup_actual_refs_batch
    db_mock = MagicMock()
    result = _lookup_actual_refs_batch(db_mock, "p1", "e1", [])
    assert result == {}


def test_lookup_actual_refs_batch_single_query():
    """still_ids list → DB 에 1 query 만 실행 (N+1 회피)."""
    from app.services.image_service_helpers import _lookup_actual_refs_batch
    db_mock = MagicMock()
    # mock execute return — DISTINCT ON 결과 simulating
    db_mock.execute.return_value.all.return_value = [
        ("s1", '["character C01O02 in outfit"]'),
        ("s2", '["character C02O03 in outfit", "BACKGROUND chain reference"]'),
    ]
    result = _lookup_actual_refs_batch(db_mock, "p1", "e1", ["s1", "s2"])

    # 1 query (N+1 회피)
    assert db_mock.execute.call_count == 1
    assert result == {
        "s1": ["character C01O02 in outfit"],
        "s2": ["character C02O03 in outfit", "BACKGROUND chain reference"],
    }


def test_lookup_actual_refs_batch_invalid_json_isolates():
    """row 의 reference_image_ids parse 실패 시 해당 still_id = []."""
    from app.services.image_service_helpers import _lookup_actual_refs_batch
    db_mock = MagicMock()
    db_mock.execute.return_value.all.return_value = [
        ("s1", '["valid"]'),
        ("s2", "not_json"),
    ]
    result = _lookup_actual_refs_batch(db_mock, "p1", "e1", ["s1", "s2"])
    assert result["s1"] == ["valid"]
    assert result["s2"] == []
