"""t2i_review step 의 sentinel refresh 통합 — AC-A1, A2, A4, A5 (Block A T6).

V2 patch P2 (Codex BLOCKING #2 + spec V5 S1):
- _capture_card_hash_state 는 run_t2i_review 호출 **전** capture 의무.
- _refresh_scene_detail_sentinels 는 applied_indices 만 sentinel 갱신.
- _assert_card_hash_unchanged 는 card payload 변경 차단.
"""
from app.core.errors import AppError
from app.core.steps._owned_helpers import (
    OWNED_SENTINEL_SCHEMA_VERSION,
    OWNED_VALIDATOR_FULL,
    compute_camera_direction_hash,
    compute_owned_hash,
    compute_t2i_prompt_hash,
)
from app.core.steps.t2i_review_step import (
    _capture_card_hash_state,
    _refresh_scene_detail_sentinels,
)

import pytest


def _sentinel(t2i: str) -> dict:
    return {
        "schema_version": OWNED_SENTINEL_SCHEMA_VERSION,
        "t2i_prompt_hash": compute_t2i_prompt_hash(t2i),
        "owned_hash": compute_owned_hash(["C01"]),
        "camera_direction_hash": compute_camera_direction_hash("static"),
        "owned_usage_hash": "0123456789abcdef",  # C2 v1 sentinel v2
        "validator": OWNED_VALIDATOR_FULL,
        "violations": [],
    }


def test_refresh_updates_only_modified_indices():
    """AC-A1: applied_indices 외 sentinel 은 보존."""
    scene_data = {
        "scenes": [
            {
                "t2i_variations": [
                    {"t2i_prompt": "NEW", "owned_validation": _sentinel("OLD")},
                    {
                        "t2i_prompt": "untouched",
                        "owned_validation": _sentinel("untouched"),
                    },
                ]
            }
        ]
    }
    refreshed = _refresh_scene_detail_sentinels(
        scene_data, applied_indices=[(0, 0)]
    )
    assert refreshed == 1
    assert (
        scene_data["scenes"][0]["t2i_variations"][0]["owned_validation"][
            "t2i_prompt_hash"
        ]
        == compute_t2i_prompt_hash("NEW")
    )
    assert (
        scene_data["scenes"][0]["t2i_variations"][1]["owned_validation"][
            "t2i_prompt_hash"
        ]
        == compute_t2i_prompt_hash("untouched")
    )


def test_refresh_returns_zero_when_no_changes():
    """AC-A5: t2i_prompt 가 이미 hash 와 일치하면 no-op."""
    scene_data = {
        "scenes": [
            {
                "t2i_variations": [
                    {"t2i_prompt": "same", "owned_validation": _sentinel("same")},
                ]
            }
        ]
    }
    refreshed = _refresh_scene_detail_sentinels(
        scene_data, applied_indices=[(0, 0)]
    )
    assert refreshed == 0


def test_refresh_raises_when_owned_validation_missing():
    """AC-A2: sentinel 누락 → t2i_review failed (raise)."""
    scene_data = {
        "scenes": [
            {
                "t2i_variations": [
                    {"t2i_prompt": "x"},  # owned_validation 키 없음
                ]
            }
        ]
    }
    with pytest.raises(AppError) as exc_info:
        _refresh_scene_detail_sentinels(scene_data, applied_indices=[(0, 0)])
    # T1 implementer 패턴 — codebase AppError 의 .code 로 검증
    assert exc_info.value.code in (
        "t2i_review.owned_validation_missing",
        "t2i_review.sentinel_refresh_failed",
    )


def test_pre_card_state_captured_before_mutation():
    """V2 patch P2 (Codex BLOCKING #2): pre_card_state 는 mutation 전 capture.

    run_t2i_review 가 scene_detail_data 를 mutate 하는 monkey 를 깔아도
    _capture_card_hash_state 결과가 mutation 전 값이어야 freeze.
    """
    scene_data = {
        "scenes": [
            {
                "t2i_variations": [
                    {"render_prompt_card_hash": "PRE_HASH"},
                ]
            }
        ]
    }
    pre = _capture_card_hash_state(scene_data)
    # 가짜 mutation
    scene_data["scenes"][0]["t2i_variations"][0][
        "render_prompt_card_hash"
    ] = "POST_HASH"
    # pre 는 mutation 전 값으로 freeze
    assert pre[(0, 0)] == "PRE_HASH"


def test_full_cycle_refresh_then_verify_passes():
    """AC-A4 (Block A T7 closure): t2i_review fix → sentinel refresh 후 scene_detail
    의 sentinel.t2i_prompt_hash 가 새 t2i_prompt 와 일치 → verify_completion drift 0.

    end-to-end 시나리오: t2i_review 가 in-place mutation → caller 가 refresh helper 호출 →
    next resume 시 verify_completion 의 sentinel hash 비교가 통과해야 함.
    """
    # 1) 초기 상태 — sentinel 이 t2i_prompt 와 일치 (clean)
    scene_data = {"scenes": [{
        "t2i_variations": [{
            "t2i_prompt": "ORIGINAL prompt text",
            "owned_validation": {
                "schema_version": OWNED_SENTINEL_SCHEMA_VERSION,
                "t2i_prompt_hash": compute_t2i_prompt_hash("ORIGINAL prompt text"),
                "owned_hash": compute_owned_hash(["C01"]),
                "camera_direction_hash": compute_camera_direction_hash("static"),
                "owned_usage_hash": "0123456789abcdef",  # C2 v1 sentinel v2
                "validator": OWNED_VALIDATOR_FULL,
                "violations": [],
            },
        }]
    }]}

    # 2) t2i_review 가 t2i_prompt 변경 (in-place mutation 시뮬)
    scene_data["scenes"][0]["t2i_variations"][0]["t2i_prompt"] = "REVISED prompt text"

    # 3) sentinel refresh 호출 (T6 의 caller path)
    refreshed = _refresh_scene_detail_sentinels(scene_data, applied_indices=[(0, 0)])
    assert refreshed == 1

    # 4) post-refresh sentinel 이 새 t2i_prompt 와 일치 → next resume 시 verify drift 0
    sentinel = scene_data["scenes"][0]["t2i_variations"][0]["owned_validation"]
    assert sentinel["t2i_prompt_hash"] == compute_t2i_prompt_hash("REVISED prompt text")
    # 다른 sentinel 필드는 보존 (T1 primitive 의 행동)
    assert sentinel["owned_hash"] == compute_owned_hash(["C01"])
    assert sentinel["camera_direction_hash"] == compute_camera_direction_hash("static")
    assert sentinel["validator"] == OWNED_VALIDATOR_FULL


# ─────────────────────────────────────────────────────────────────────────────
# Block A closure stabilization (Codex IMPORTANT #1) — 실제 SceneDetailStep.
# verify_completion() 호출로 sentinel drift 분기 검증 (옛 hash equality only
# test 가 verify_completion 본체 변화에 무력해서 evidence gap 발생).
# ─────────────────────────────────────────────────────────────────────────────


def _make_scene_detail_result(*, t2i_prompt: str, sentinel_t2i_hash: str,
                              owned: list, cam_dir: str) -> dict:
    """SceneDetailStep.verify_completion 가 기대하는 minimum result shape.

    - schema_version 미지정 → _card_check_active=False (card recompute path skip).
      옛 g3.2 fixture 패턴 mirror (test_g3_2_close_skip_sentinel_drift.py).
    - sentinel.t2i_prompt_hash 의도적 stale (drift 시뮬).
    - sentinel.owned_hash / camera_direction_hash 는 정확히 매칭 (다른 drift 0).
    """
    return {
        "data": {
            "scenes": [
                {
                    "scene_index": 1,
                    "_shot_index": 0,
                    "t2i_variations": [
                        {
                            "variant_label": "var_1",
                            "camera_effect": "wide",
                            "t2i_prompt": t2i_prompt,
                            "outfit_assignments": [],
                            "source_facts": ["sf"],
                            "visual_inferences": ["vi"],
                            "creative_decisions": ["cd"],
                            "confidence": "high",
                            "owned_validation": {
                                "schema_version": OWNED_SENTINEL_SCHEMA_VERSION,
                                "t2i_prompt_hash": sentinel_t2i_hash,
                                "owned_hash": compute_owned_hash(owned),
                                "camera_direction_hash":
                                    compute_camera_direction_hash(cam_dir),
                                "owned_usage_hash": "0123456789abcdef",  # C2 v1 sentinel v2
                                "validator": OWNED_VALIDATOR_FULL,
                                "violations": [],
                            },
                        },
                    ],
                },
            ],
        },
    }


def _exercise_scene_detail_verify(result: dict, *, owned_by_shot: dict,
                                   staging_map: dict):
    """g3.2 fixture 패턴 mirror — SceneDetailStep instance + loader patch."""
    from unittest.mock import patch
    from app.core.steps.detail_steps import SceneDetailStep
    from app.core.steps.scene_context_loader import SceneContextLoader

    step = SceneDetailStep.__new__(SceneDetailStep)
    step._last_execute_result = result
    step.project_id = "p"
    step.episode_id = "e"
    step.db = None
    step.project_config = {}

    def fake_load(self):
        return owned_by_shot

    def fake_staging(self):
        return staging_map

    with patch.object(SceneContextLoader,
                      "_load_chain_bg_owned_by_shot", fake_load), \
         patch.object(SceneContextLoader, "_load_staging_map", fake_staging):
        return step.verify_completion()


def test_full_cycle_verify_completion_drift_detected_then_cleared(monkeypatch):
    """Codex IMPORTANT #1 (Block A T7 closure 보강): 실제 SceneDetailStep.verify_completion()
    의 sentinel drift 분기 검증.

    1. pre-refresh: sentinel.t2i_prompt_hash != computed → CompletionReport.is_complete=False,
       sentinel_drifted 에 ('t2i_prompt_hash') 표시.
    2. _refresh_scene_detail_sentinels 호출 (T6 의 caller path).
    3. post-refresh: sentinel.t2i_prompt_hash == computed → drift 0 → is_complete=True.

    옛 helper-level test (test_full_cycle_refresh_then_verify_passes) 는 hash equality
    만 검증 — verify_completion 본체 변경 시 무력. 본 test 가 evidence gap 차단.
    """
    owned = ["door"]
    cam_dir = "wide"
    new_prompt = "REVISED prompt text"
    stale_hash = compute_t2i_prompt_hash("STALE prompt — different from new")

    # 1) pre-refresh: drift detected
    result_before = _make_scene_detail_result(
        t2i_prompt=new_prompt,
        sentinel_t2i_hash=stale_hash,
        owned=owned,
        cam_dir=cam_dir,
    )
    report_before = _exercise_scene_detail_verify(
        result_before,
        owned_by_shot={(1, 0): owned},
        staging_map={"1_0": {"camera_direction": cam_dir, "framing_scale": "medium"}},
    )
    assert report_before.is_complete is False
    drifted_before = report_before.metadata.get("sentinel_drifted", [])
    assert any(d[2] == "t2i_prompt_hash" for d in drifted_before), (
        f"verify_completion 가 t2i_prompt_hash drift 를 검출해야: {drifted_before}"
    )

    # 2) refresh — t2i_prompt 와 sentinel hash align (T6 caller path).
    refreshed = _refresh_scene_detail_sentinels(
        result_before["data"], applied_indices=[(0, 0)],
    )
    assert refreshed == 1

    # 3) post-refresh: drift 0 → is_complete=True
    report_after = _exercise_scene_detail_verify(
        result_before,  # 같은 dict — refresh 가 in-place
        owned_by_shot={(1, 0): owned},
        staging_map={"1_0": {"camera_direction": cam_dir, "framing_scale": "medium"}},
    )
    assert report_after.is_complete is True, (
        f"refresh 후 verify_completion 통과해야: {report_after.missing}"
    )
