"""SceneProvenanceService 단위 테스트 — W5 F22 Phase B.10.

SceneImageService에서 분리된 PNG metadata embedding 로직의 실제 동작을 검증.
scene_image_service 쪽에는 delegation smoke test만 남긴다.

Monkeypatch target은 `app.modules.png_metadata.embed_png_metadata` — provenance
서비스가 method 내부에서 lazy import하므로 원본 모듈을 패치한다.
"""
from __future__ import annotations

import logging
from pathlib import Path
from unittest.mock import MagicMock

import pytest

from app.services.scene_provenance_service import SceneProvenanceService


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


def test_embed_scene_variations_metadata_calls_embed_per_variation(svc, monkeypatch):
    """N variation → embed_png_metadata N회 호출, 각 메타데이터 key 확인."""
    calls = []
    import app.modules.png_metadata as png_mod

    monkeypatch.setattr(png_mod, "embed_png_metadata",
                        lambda p, m: calls.append((p, m)))

    var_results = [
        {
            "id": "a1", "file_path": "/tmp/a.png",
            "prompt_used": "PROMPT_A",
            "variant_type": "base",
            "generation_model": "gemini",
            "created_at": "T0",
        },
        {
            "id": "a2", "file_path": "/tmp/b.png",
            "prompt_used": "PROMPT_B",
            "variant_type": "angle",
            "generation_model": "gemini",
            "created_at": "T1",
        },
    ]
    still = {
        "still_index": 7,
        "still_frame_prompt": "scene desc",
        "beat_title": "beat",
    }
    svc.embed_scene_variations_metadata(var_results, still, 99, "dep5", "prev.png")

    assert len(calls) == 2
    p1, m1 = calls[0]
    assert p1 == "/tmp/a.png"
    assert m1["scene_description"] == "scene desc"
    assert m1["scene_index"] == "7"
    assert m1["variant_type"] == "base"
    assert m1["dependent_scene"] == "dep5"
    assert m1["previous_scene"] == "prev.png"
    assert m1["created_at"] == "T0"
    assert m1["model"] == "gemini"
    _, m2 = calls[1]
    assert m2["variant_type"] == "angle"


def test_embed_scene_variations_metadata_truncates_prompt_at_2000(svc, monkeypatch):
    """2000자 초과 prompt → 정확히 2000자로 잘림."""
    calls = []
    import app.modules.png_metadata as png_mod
    monkeypatch.setattr(png_mod, "embed_png_metadata",
                        lambda p, m: calls.append((p, m)))

    huge = "x" * 5000
    svc.embed_scene_variations_metadata(
        [{"id": "a", "file_path": "/tmp/a.png", "prompt_used": huge}],
        {}, 0, "", "",
    )

    assert calls[0][1]["prompt"] == "x" * 2000


def test_embed_scene_variations_metadata_uses_fallback_index(svc, monkeypatch):
    """still_data에 still_index 없으면 fallback 사용."""
    calls = []
    import app.modules.png_metadata as png_mod
    monkeypatch.setattr(png_mod, "embed_png_metadata",
                        lambda p, m: calls.append((p, m)))

    var_results = [{"id": "a1", "file_path": "/tmp/a.png"}]
    svc.embed_scene_variations_metadata(var_results, {}, 42, "", "")

    assert calls[0][1]["scene_index"] == "42"
    assert calls[0][1]["variant_type"] == "var_0"


def test_embed_scene_variations_metadata_suppresses_and_logs(svc, monkeypatch, caplog):
    """embed 실패 → warning log + 예외 전파 안 함."""
    import app.modules.png_metadata as png_mod
    monkeypatch.setattr(png_mod, "embed_png_metadata",
                        lambda *a, **k: (_ for _ in ()).throw(RuntimeError("boom")))

    with caplog.at_level(logging.WARNING, logger="app.services.scene_provenance_service"):
        svc.embed_scene_variations_metadata(
            [{"id": "a", "file_path": "/tmp/a.png"}], {}, 0, "", "",
        )

    assert any("PNG metadata embedding failed" in r.message for r in caplog.records)


def test_embed_fal_angle_metadata_maps_angle_fields(svc, monkeypatch):
    """fal.ai angle metadata 저장 — source/angles/model/created_at 매핑."""
    captured = {}
    import app.modules.png_metadata as png_mod
    monkeypatch.setattr(
        png_mod, "embed_png_metadata",
        lambda p, m: captured.update({"path": p, "meta": m}),
    )

    svc.embed_fal_angle_metadata(
        file_path=Path("/tmp/fal.png"),
        source_image_name="src.png",
        horizontal_angle=10.0,
        vertical_angle=-5.0,
        zoom=1.2,
        created_at="T0",
    )

    assert captured["path"] == Path("/tmp/fal.png")
    m = captured["meta"]
    assert m["source_image"] == "src.png"
    assert m["angle_horizontal"] == "10.0"
    assert m["angle_vertical"] == "-5.0"
    assert m["angle_zoom"] == "1.2"
    assert m["model"] == "fal-ai/qwen-image-edit-2511-multiple-angles"
    assert m["created_at"] == "T0"


def test_embed_fal_angle_metadata_suppresses_and_logs(svc, monkeypatch, caplog):
    """fal embed 실패 → warning log + 예외 전파 안 함."""
    import app.modules.png_metadata as png_mod
    monkeypatch.setattr(png_mod, "embed_png_metadata",
                        lambda *a, **k: (_ for _ in ()).throw(RuntimeError("boom")))

    with caplog.at_level(logging.WARNING, logger="app.services.scene_provenance_service"):
        svc.embed_fal_angle_metadata(
            file_path="/tmp/fal.png",
            source_image_name="src",
            horizontal_angle=0.0, vertical_angle=0.0, zoom=1.0,
            created_at="T0",
        )

    assert any("PNG metadata embedding failed" in r.message for r in caplog.records)


# ──────────────────────────────────────────────────────────────────────
# compute_and_embed_scene_metadata — W5 F22 Phase B.22.8
# ──────────────────────────────────────────────────────────────────────


def test_compute_and_embed_no_dep_no_prev(svc, monkeypatch):
    """dep_scene_id 없고 scene_paths_by_index도 비어있으면 둘 다 '' 전달."""
    calls = []
    import app.modules.png_metadata as png_mod
    monkeypatch.setattr(png_mod, "embed_png_metadata",
                        lambda p, m: calls.append((p, m)))

    var_results = [{"id": "a", "file_path": "/tmp/a.png"}]
    still = {"still_index": 5, "dependent_scene_id": None}
    svc.compute_and_embed_scene_metadata(
        var_results, still, stills=[], si=5,
        scene_paths_by_index={}, scene_paths_by_index_by_id={},
    )

    assert calls[0][1]["dependent_scene"] == ""
    assert calls[0][1]["previous_scene"] == ""


def test_compute_and_embed_dep_scene_idx_from_stills(svc, monkeypatch):
    """dependent_scene_id가 stills에서 match → still_index 문자열."""
    calls = []
    import app.modules.png_metadata as png_mod
    monkeypatch.setattr(png_mod, "embed_png_metadata",
                        lambda p, m: calls.append((p, m)))

    var_results = [{"id": "a", "file_path": "/tmp/a.png"}]
    still = {"still_index": 7, "dependent_scene_id": "dep-uuid"}
    stills = [
        {"id": "other", "still_index": 1},
        {"id": "dep-uuid", "still_index": 3},
    ]
    svc.compute_and_embed_scene_metadata(
        var_results, still, stills=stills, si=7,
        scene_paths_by_index={}, scene_paths_by_index_by_id={},
    )

    assert calls[0][1]["dependent_scene"] == "3"


def test_compute_and_embed_dep_id_not_found_in_stills(svc, monkeypatch):
    """dependent_scene_id 있지만 stills에 match 없음 → ''."""
    calls = []
    import app.modules.png_metadata as png_mod
    monkeypatch.setattr(png_mod, "embed_png_metadata",
                        lambda p, m: calls.append((p, m)))

    var_results = [{"id": "a", "file_path": "/tmp/a.png"}]
    still = {"still_index": 7, "dependent_scene_id": "missing-uuid"}
    stills = [{"id": "other", "still_index": 1}]
    svc.compute_and_embed_scene_metadata(
        var_results, still, stills=stills, si=7,
        scene_paths_by_index={}, scene_paths_by_index_by_id={},
    )

    assert calls[0][1]["dependent_scene"] == ""


def test_compute_and_embed_prev_file_from_scene_paths_by_index(svc, monkeypatch):
    """scene_paths_by_index[si]가 있으면 해당 파일명 사용 (1순위)."""
    calls = []
    import app.modules.png_metadata as png_mod
    monkeypatch.setattr(png_mod, "embed_png_metadata",
                        lambda p, m: calls.append((p, m)))

    var_results = [{"id": "a", "file_path": "/tmp/a.png"}]
    still = {"still_index": 5, "dependent_scene_id": "dep-uuid"}
    svc.compute_and_embed_scene_metadata(
        var_results, still, stills=[], si=5,
        scene_paths_by_index={5: Path("/proj/prev_s5.png")},
        scene_paths_by_index_by_id={"dep-uuid": Path("/proj/dep.png")},
    )

    # si 1순위 → prev_s5.png (by_id의 dep.png가 아님)
    assert calls[0][1]["previous_scene"] == "prev_s5.png"


def test_compute_and_embed_prev_file_from_by_id_fallback(svc, monkeypatch):
    """scene_paths_by_index에 없고 by_id[dep_scene_id]가 있으면 2순위 fallback."""
    calls = []
    import app.modules.png_metadata as png_mod
    monkeypatch.setattr(png_mod, "embed_png_metadata",
                        lambda p, m: calls.append((p, m)))

    var_results = [{"id": "a", "file_path": "/tmp/a.png"}]
    still = {"still_index": 5, "dependent_scene_id": "dep-uuid"}
    svc.compute_and_embed_scene_metadata(
        var_results, still, stills=[], si=5,
        scene_paths_by_index={},  # si=5 없음
        scene_paths_by_index_by_id={"dep-uuid": Path("/proj/dep_s3.png")},
    )

    assert calls[0][1]["previous_scene"] == "dep_s3.png"


def test_compute_and_embed_without_dep_id_falls_back_to_empty(svc, monkeypatch):
    """dependent_scene_id 없으면 None in dict miss로 '' fallback."""
    calls = []
    import app.modules.png_metadata as png_mod
    monkeypatch.setattr(png_mod, "embed_png_metadata",
                        lambda p, m: calls.append((p, m)))

    var_results = [{"id": "a", "file_path": "/tmp/a.png"}]
    # dependent_scene_id 없음
    still = {"still_index": 5}
    svc.compute_and_embed_scene_metadata(
        var_results, still, stills=[], si=5,
        scene_paths_by_index={},  # si 없음
        scene_paths_by_index_by_id={"some-id": Path("/proj/x.png")},
    )

    # None이 dict key로 조회되어 miss → prev_file=""
    assert calls[0][1]["previous_scene"] == ""
    assert calls[0][1]["dependent_scene"] == ""
