"""test_sync_v3 — STEP_CLASSES registry, _get_step_runner, V/A/H + scene_summary sync."""

import json
import os
import tempfile
import uuid
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest


# ── 1) STEP_CLASSES registry ──


class TestStepClassesRegistry:

    def test_at_least_18_analysis_entries(self):
        """STEP_CLASSES has at least 18 entries (v3 analysis steps)."""
        from app.core.steps import STEP_CLASSES

        assert len(STEP_CLASSES) >= 18, f"Expected >= 18, got {len(STEP_CLASSES)}"

    def test_core_analysis_keys_present(self):
        """STEP_MANIFEST의 lifecycle=active + category=analysis step 전부가 STEP_CLASSES에 등록돼 있다.

        W3-2 Codex M3: 이전 v3 시절 하드코딩 + 내 v4 하드코딩(deprecated scene_dependency/scene_verify
        포함, shot-more active step 다수 누락)을 manifest에서 derive하는 방식으로 대체.
        이렇게 하면 shot-more 흐름의 shot_validator/shot_director/shot_staging/scene_consistency
        등도 자동으로 검증 대상에 포함되고, deprecated step이 빠진다.
        """
        from app.core.step_manifest import STEP_MANIFEST
        from app.core.steps import STEP_CLASSES

        active_analysis = [
            sid
            for sid, info in STEP_MANIFEST.items()
            if info.get("category") == "analysis" and info.get("lifecycle") == "active"
        ]
        assert len(active_analysis) >= 18, (
            f"Expected >= 18 active analysis steps, got {len(active_analysis)}"
        )

        missing = [k for k in active_analysis if k not in STEP_CLASSES]
        assert missing == [], f"Active analysis steps without class: {missing}"

    def test_image_steps_present(self):
        """Image steps should be available."""
        from app.core.steps import STEP_CLASSES

        image_keys = ["world_guide", "ref_image_gen", "composite_image_gen", "scene_image_pipeline"]
        for key in image_keys:
            assert key in STEP_CLASSES, f"Missing image step key: {key}"

    def test_all_classes_are_step_runner_subclasses(self):
        """Every value in STEP_CLASSES is a StepRunner subclass."""
        from app.core.steps import STEP_CLASSES
        from app.core.step_runner import StepRunner

        for key, cls in STEP_CLASSES.items():
            assert issubclass(cls, StepRunner), f"{key} -> {cls} is not a StepRunner subclass"

    def test_text_cleanup_maps_to_correct_class(self):
        from app.core.steps import STEP_CLASSES
        from app.core.steps.text_steps import TextCleanupStep

        assert STEP_CLASSES["text_cleanup"] is TextCleanupStep

    def test_scene_director_maps_to_correct_class(self):
        from app.core.steps import STEP_CLASSES
        from app.core.steps.director_steps import SceneDirectorStep

        assert STEP_CLASSES["scene_director"] is SceneDirectorStep


# ── 2) _get_step_runner ──


class TestGetStepRunner:

    @patch("app.core.step_manifest.STEP_MANIFEST", {
        "text_cleanup": {"step_id": "text_cleanup", "label": "t", "category": "analysis", "order": 1, "depends_on": [], "provider": "gemini", "step_type": "transform", "lifecycle": "active"},
        "scene_director": {"step_id": "scene_director", "label": "d", "category": "analysis", "order": 8, "depends_on": [], "provider": "gemini", "step_type": "transform", "lifecycle": "active"},
    })
    def test_returns_v3_class_for_text_cleanup(self):
        """_get_step_runner returns TextCleanupStep for 'text_cleanup'."""
        from app.api.v1.steps import _get_step_runner
        from app.core.steps.text_steps import TextCleanupStep

        db = MagicMock()
        runner = _get_step_runner("text_cleanup", "proj1", "ep1", db, {})
        assert isinstance(runner, TextCleanupStep)

    @patch("app.core.step_manifest.STEP_MANIFEST", {
        "text_cleanup": {"step_id": "text_cleanup", "label": "t", "category": "analysis", "order": 1, "depends_on": [], "provider": "gemini", "step_type": "transform", "lifecycle": "active"},
        "scene_director": {"step_id": "scene_director", "label": "d", "category": "analysis", "order": 8, "depends_on": [], "provider": "gemini", "step_type": "transform", "lifecycle": "active"},
    })
    def test_returns_v3_class_for_scene_director(self):
        """_get_step_runner returns SceneDirectorStep for 'scene_director'."""
        from app.api.v1.steps import _get_step_runner
        from app.core.steps.director_steps import SceneDirectorStep

        db = MagicMock()
        runner = _get_step_runner("scene_director", "proj1", "ep1", db, {})
        assert isinstance(runner, SceneDirectorStep)

    @patch("app.core.step_manifest.STEP_MANIFEST", {
        "text_cleanup": {"step_id": "text_cleanup", "label": "t", "category": "analysis", "order": 1, "depends_on": [], "provider": "gemini", "step_type": "transform", "lifecycle": "active"},
    })
    def test_unknown_step_raises_error(self):
        """_get_step_runner raises AppError for unknown step_id."""
        from app.api.v1.steps import _get_step_runner
        from app.core.errors import AppError

        db = MagicMock()
        with pytest.raises(AppError) as exc_info:
            _get_step_runner("nonexistent_step", "proj1", "ep1", db, {})
        assert "Step class not found" in exc_info.value.message


# ── 3) V/A/H + scene_summary sync ──
# W3-2: TestVAHSync는 scene_still = 1 scene 가정으로 쓴 v3 구조. scene_still이 shot 단위로
# 바뀐 현재, V/A/H 컬럼 존재 자체는 test_pipeline_v3_e2e.py::test_scene_still_model_has_vah_columns가,
# 체크포인트 shape은 test_scene_director_checkpoint_has_vah_fields가 검증한다.
# scene_summary sync는 아래 TestSceneSummarySync로 shot-based(scene_index 매칭)로 재작성.
# Codex W3-2 H2: scene_summary → scene_still.scene_summary projection은 한 scene의 여러 shot에
# 동일 요약이 전파되는 계약이므로 별도 happy-path 필요.


def _make_checkpoint(base_dir, project_id, episode_id, step_id, data, status="completed"):
    """체크포인트 manifest.json 생성 헬퍼."""
    cp_dir = Path(base_dir) / project_id / "checkpoints" / "episodes" / episode_id / step_id
    cp_dir.mkdir(parents=True, exist_ok=True)
    manifest = {"status": status, "data": data}
    (cp_dir / "manifest.json").write_text(
        json.dumps(manifest, ensure_ascii=False), encoding="utf-8"
    )


class TestSceneSummarySync:

    def test_shot_based_scene_summary_projection(self, tmp_path):
        """scene_summary 체크포인트 → 같은 scene_index의 모든 scene_still row에 전파.

        scene 1에 2 shot, scene 2에 1 shot이 있을 때, 각 scene의 summary가 shot 모두에
        동일하게 반영돼야 한다.

        W4 P3-1: 로직이 SceneStillWriter로 이동. Writer._sync_scene_summary 직접 호출.
        (Codex W3-2 H2 유지) PostgreSQL FK/engine caching + monkeypatch 상호작용 이슈를
        피하기 위해 독립 in-memory SQLite engine 사용.
        """
        from sqlalchemy import create_engine, text as sql_text
        from sqlalchemy.orm import Session
        from app.services.checkpoint_sync.scene_still_writer import SceneStillWriter

        pid, eid = "proj-ss", "ep-ss"
        summaries = [
            {"scene_index": 1, "scene_summary": "S1 요약"},
            {"scene_index": 2, "scene_summary": "S2 요약"},
        ]

        # 독립 in-memory SQLite — FK 없이 scene_still의 sync 계약에 필요한 컬럼만.
        test_engine = create_engine("sqlite:///:memory:")
        with test_engine.begin() as conn:
            conn.execute(sql_text("""
                CREATE TABLE scene_still (
                    id TEXT PRIMARY KEY,
                    project_id TEXT NOT NULL,
                    episode_id TEXT NOT NULL,
                    scene_index INTEGER NOT NULL,
                    still_index INTEGER NOT NULL,
                    scene_summary TEXT,
                    created_at TEXT
                )
            """))
            for still_idx, scene_idx in [(0, 1), (1, 1), (2, 2)]:
                conn.execute(sql_text(
                    "INSERT INTO scene_still (id, project_id, episode_id, "
                    "scene_index, still_index, created_at) "
                    "VALUES (:id, :pid, :eid, :si, :sti, :ca)"
                ), {
                    "id": str(uuid.uuid4()), "pid": pid, "eid": eid,
                    "si": scene_idx, "sti": still_idx, "ca": "2026-01-01",
                })

        with Session(test_engine) as db:
            writer = SceneStillWriter(db, pid, eid, now="2026-04-22T00:00:00+00:00")
            writer._sync_scene_summary(summaries)
            db.commit()

            rows = db.execute(sql_text(
                "SELECT scene_index, still_index, scene_summary FROM scene_still "
                "WHERE project_id = :pid AND episode_id = :eid "
                "ORDER BY still_index"
            ), {"pid": pid, "eid": eid}).fetchall()

            assert len(rows) == 3
            assert (rows[0][0], rows[0][2]) == (1, "S1 요약")
            assert (rows[1][0], rows[1][2]) == (1, "S1 요약")
            assert (rows[2][0], rows[2][2]) == (2, "S2 요약")
