"""
=============================================================================
THẬP NGŨ NIÊN (THE FIFTEEN SPRINGS) - CRITIC QUALITY GATE TEST SUITE
=============================================================================
Authoritative Test Suite for Milestone M1: 2-Tier Quality Gate & Classifier.
Tests:
1. VideoCriticVerdict & Sub-models Pydantic Schema Validation:
   - ShotEvaluation, SceneEvaluation, VideoCriticVerdict fields and constraints.
   - Threshold synchronization: approved == (overall_score >= 0.8) and actions.
   - Serialization & JSON roundtrip validation.
2. evaluate_shot_gate (Tầng 1 - Shot Gate):
   - Valid input evaluation (score >= 0.8, approved == True, suggested_action == 'APPROVE').
   - Defect detection: missing file, corrupted stream, black frames, frozen frames.
   - Character cross-contamination detection (Thúy Kiều in Kim Trọng shot).
3. evaluate_scene_gate (Tầng 2 - Scene Gate):
   - Smooth junction between sequential shots.
   - Color continuity and trigger_color_match when color delta exceeds threshold.
   - Pacing & static freeze detection (trigger_trim_static).
   - Missing/invalid input resilience.
4. Shot Take Classification & resolve_start_frame:
   - shot_num == 1 -> CINEMATIC_CUT (independent Start Frame, NEVER prev_tail).
   - Changing character_anchor -> CINEMATIC_CUT.
   - Identical character_anchor -> CONTINUOUS_TAKE (loads prev_tail).
   - resolve_start_frame_v2 returns (frame_path, take_type).
5. Character Invariant Safeguard:
   - Resolving Kim Trọng shots NEVER returns Thúy Kiều image paths under any condition,
     even with corrupted/adversarial prompt data.
=============================================================================
"""

import os
import sys
import json
import tempfile
import shutil
from pathlib import Path
import pytest
import numpy as np
import cv2
from pydantic import ValidationError

PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT / "05_Production_Pipeline"))

from antigravity_critic_gate import (
    VideoCriticVerdict,
    ShotEvaluation,
    SceneEvaluation,
    evaluate_shot_gate,
    evaluate_scene_gate,
)
import production_orchestrator as po


# =============================================================================
# FIXTURES & SYNTHETIC MEDIA GENERATION
# =============================================================================

@pytest.fixture(scope="session")
def media_sandbox(tmp_path_factory):
    """Tạo thư mục tạm thời để sinh các video thử nghiệm."""
    temp_dir = tmp_path_factory.mktemp("critic_test_media")
    yield temp_dir
    # Cleanup sau session
    shutil.rmtree(str(temp_dir), ignore_errors=True)


def _create_synthetic_video(
    output_path: Path,
    num_frames: int = 48,
    fps: int = 24,
    width: int = 1280,
    height: int = 720,
    mode: str = "gradient",
    base_color: tuple = (100, 150, 200),
    start_frame_idx: int = 0
) -> Path:
    """Sinh video MP4 hợp lệ với các đặc tính thị giác cụ thể."""
    fourcc = cv2.VideoWriter_fourcc(*"mp4v")
    out = cv2.VideoWriter(str(output_path), fourcc, fps, (width, height))
    try:
        for i in range(num_frames):
            frame_idx = start_frame_idx + i
            if mode == "black":
                frame = np.zeros((height, width, 3), dtype=np.uint8)
            elif mode == "frozen":
                # Toàn bộ các frame giống hệt nhau
                frame = np.full((height, width, 3), base_color, dtype=np.uint8)
            elif mode == "solid":
                frame = np.full((height, width, 3), base_color, dtype=np.uint8)
            elif mode == "gradient":
                # Chuyển động vi mô tự nhiên để tránh bị nhận diện là đơ hình
                frame = np.zeros((height, width, 3), dtype=np.uint8)
                b, g, r = base_color
                offset = int(np.sin(frame_idx / 24.0) * 5)
                frame[:, :, 0] = np.clip(b + offset, 0, 255)
                frame[:, :, 1] = np.clip(g + offset, 0, 255)
                frame[:, :, 2] = np.clip(r + offset, 0, 255)
                # Thêm họa tiết chuyển động nhẹ liên tục
                circle_x = int(100 + (frame_idx % 200) * 2)
                cv2.circle(frame, (circle_x, 200), 40, (255, 255, 255), -1)
            else:
                frame = np.random.randint(0, 255, (height, width, 3), dtype=np.uint8)

            out.write(frame)
    finally:
        out.release()
    return output_path


# =============================================================================
# 1. TEST PYDANTIC SCHEMA VALIDATION
# =============================================================================

class TestVideoCriticVerdictSchema:
    def test_schema_valid_approved_verdict(self):
        """Kiểm tra schema VideoCriticVerdict với điểm đạt chuẩn >= 0.8."""
        shot_eval = ShotEvaluation(
            character_match=True,
            character_confidence=0.95,
            visual_defects=[],
            squint_or_extra_limbs=False,
            audio_guard_ok=True,
            score=0.92
        )
        verdict = VideoCriticVerdict(
            overall_score=0.92,
            approved=True,
            shot_eval=shot_eval,
            suggested_action="APPROVE",
            critique_notes="Cú máy đạt chuẩn điện ảnh hoàn hảo."
        )
        assert verdict.approved is True
        assert verdict.overall_score == 0.92
        assert verdict.suggested_action == "APPROVE"
        assert verdict.shot_eval.character_match is True

    def test_schema_rejected_score_below_threshold(self):
        """Điểm dưới 0.8 bắt buộc phải có approved == False."""
        shot_eval = ShotEvaluation(
            character_match=True,
            character_confidence=0.70,
            visual_defects=["Micro-blur"],
            score=0.75
        )
        verdict = VideoCriticVerdict(
            overall_score=0.75,
            approved=False,
            shot_eval=shot_eval,
            suggested_action="RETAKE_SHOT",
            critique_notes="Điểm không đạt ngưỡng 0.8"
        )
        assert verdict.approved is False
        assert verdict.overall_score == 0.75

    def test_schema_score_boundary_80(self):
        """Kiểm tra chính xác ngưỡng biên 0.8."""
        v_80 = VideoCriticVerdict(overall_score=0.80, suggested_action="APPROVE")
        assert v_80.approved is True

        v_799 = VideoCriticVerdict(overall_score=0.799, suggested_action="APPROVE")
        assert v_799.approved is False

    def test_schema_retake_action_overrides_approval(self):
        """Nếu suggested_action là RETAKE_SHOT thì approved luôn là False kể cả score >= 0.8."""
        verdict = VideoCriticVerdict(
            overall_score=0.90,
            suggested_action="RETAKE_SHOT",
            critique_notes="Nhân vật sai lệch nghiêm trọng yêu cầu render lại"
        )
        assert verdict.approved is False

    def test_schema_submodel_constraints(self):
        """Kiểm tra ràng buộc miền giá trị của ShotEvaluation và SceneEvaluation."""
        with pytest.raises(ValidationError):
            ShotEvaluation(character_confidence=1.5, score=0.5)

        with pytest.raises(ValidationError):
            ShotEvaluation(character_confidence=0.5, score=-0.1)

        with pytest.raises(ValidationError):
            SceneEvaluation(junction_smoothness=2.0, score=0.5)

    def test_schema_json_roundtrip(self):
        """Kiểm tra khả năng serialize/deserialize JSON trọn vẹn."""
        original = VideoCriticVerdict(
            overall_score=0.88,
            approved=True,
            shot_eval=ShotEvaluation(character_match=True, score=0.88),
            suggested_action="APPROVE",
            critique_notes="JSON serialization check"
        )
        json_str = original.model_dump_json()
        restored = VideoCriticVerdict.model_validate_json(json_str)
        assert restored.overall_score == original.overall_score
        assert restored.approved == original.approved
        assert restored.shot_eval.score == original.shot_eval.score


# =============================================================================
# 2. TEST EVALUATE_SHOT_GATE (TẦNG 1 - SHOT LEVEL)
# =============================================================================

class TestEvaluateShotGate:
    def test_evaluate_shot_gate_valid_video(self, media_sandbox):
        """Thẩm định video hợp lệ, không dị tật: kết quả >= 0.8 và APPROVE."""
        vid_path = media_sandbox / "valid_shot_10s.mp4"
        _create_synthetic_video(vid_path, num_frames=120, mode="gradient", base_color=(120, 160, 200))

        verdict = evaluate_shot_gate(
            shot_id="ep01_scene02_shot04",
            video_path=str(vid_path),
            expected_character="vuong_ong",
            prompt="Trung cận Vương Ông 55 tuổi đĩnh đạc",
            force_engine="heuristic"
        )
        assert verdict.overall_score >= 0.8
        assert verdict.approved is True
        assert verdict.suggested_action == "APPROVE"
        assert verdict.shot_eval is not None
        assert verdict.shot_eval.character_match is True
        assert len(verdict.shot_eval.visual_defects) == 0

    def test_evaluate_shot_gate_missing_file(self):
        """File không tồn tại phải trả về score 0.0 và RETAKE_SHOT."""
        verdict = evaluate_shot_gate(
            shot_id="ep01_scene02_shot99",
            video_path="nonexistent_fake_video.mp4",
            expected_character="vuong_ong",
            force_engine="heuristic"
        )
        assert verdict.overall_score == 0.0
        assert verdict.approved is False
        assert verdict.suggested_action == "RETAKE_SHOT"
        assert "does not exist" in verdict.shot_eval.visual_defects[0]

    def test_evaluate_shot_gate_corrupt_file(self, media_sandbox):
        """File rỗng hoặc bị hỏng không đọc được phải trả về RETAKE_SHOT."""
        corrupt_path = media_sandbox / "corrupted_shot.mp4"
        corrupt_path.write_bytes(b"THIS_IS_NOT_A_VALID_MP4_HEADER")

        verdict = evaluate_shot_gate(
            shot_id="ep01_scene02_shot05",
            video_path=str(corrupt_path),
            force_engine="heuristic"
        )
        assert verdict.overall_score == 0.0
        assert verdict.approved is False
        assert verdict.suggested_action == "RETAKE_SHOT"

    def test_evaluate_shot_gate_black_frame_defect(self, media_sandbox):
        """Video toàn màu đen (black frames) phải bị phạt điểm và phát hiện lỗi."""
        black_path = media_sandbox / "defect_black.mp4"
        _create_synthetic_video(black_path, num_frames=48, mode="black")

        verdict = evaluate_shot_gate(
            shot_id="ep01_scene02_shot06",
            video_path=str(black_path),
            force_engine="heuristic"
        )
        assert verdict.overall_score < 0.8
        assert verdict.approved is False
        assert any("Black frame" in d for d in verdict.shot_eval.visual_defects)
        assert verdict.suggested_action == "RETAKE_SHOT"

    def test_evaluate_shot_gate_frozen_video_defect(self, media_sandbox):
        """Video bị đơ cứng (frozen frames, không có chuyển động vi mô) phải bị ghi nhận."""
        frozen_path = media_sandbox / "defect_frozen.mp4"
        _create_synthetic_video(frozen_path, num_frames=48, mode="frozen", base_color=(100, 100, 100))

        verdict = evaluate_shot_gate(
            shot_id="ep01_scene02_shot07",
            video_path=str(frozen_path),
            force_engine="heuristic"
        )
        assert any("Frozen" in d for d in verdict.shot_eval.visual_defects)
        assert verdict.overall_score <= 0.65
        assert verdict.approved is False
        assert verdict.suggested_action == "RETAKE_SHOT"

    def test_evaluate_shot_gate_frozen_video_rejected(self, media_sandbox):
        """Video bị đơ cứng phải bị từ chối với điểm <= 0.65 và RETAKE_SHOT."""
        frozen_path = media_sandbox / "defect_frozen_reject.mp4"
        _create_synthetic_video(frozen_path, num_frames=48, mode="frozen", base_color=(100, 100, 100))
        verdict = evaluate_shot_gate("ep01_scene02_shot07", str(frozen_path), force_engine="heuristic")
        assert verdict.overall_score <= 0.65
        assert verdict.approved is False
        assert verdict.suggested_action == "RETAKE_SHOT"

    def test_evaluate_shot_gate_character_contamination_vuong_ong(self, media_sandbox):
        """Video Thúy Kiều thẩm định cho Vương Ông phải bị phát hiện cross-contamination và điểm <= 0.50."""
        tk_portrait = PROJECT_ROOT / "04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo/thuy_kieu_maiden_16yo_720p.png"
        img = cv2.imread(str(tk_portrait))
        assert img is not None
        contam_path = media_sandbox / "contam_kieu_vo.mp4"
        fourcc = cv2.VideoWriter_fourcc(*"mp4v")
        out = cv2.VideoWriter(str(contam_path), fourcc, 24, (img.shape[1], img.shape[0]))
        for _ in range(48):
            out.write(img)
        out.release()
        verdict = evaluate_shot_gate("shot_vo", str(contam_path), expected_character="vuong_ong", force_engine="heuristic")
        assert verdict.overall_score <= 0.50
        assert verdict.shot_eval.character_match is False
        assert verdict.approved is False
        assert verdict.suggested_action == "RETAKE_SHOT"

    def test_evaluate_shot_gate_character_contamination_kim_trong(self, media_sandbox):
        """Video Thúy Kiều thẩm định cho Kim Trọng phải bị phát hiện cross-contamination và điểm <= 0.50."""
        tk_portrait = PROJECT_ROOT / "04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo/thuy_kieu_maiden_16yo_720p.png"
        img = cv2.imread(str(tk_portrait))
        assert img is not None
        contam_path = media_sandbox / "contam_kieu_kt.mp4"
        fourcc = cv2.VideoWriter_fourcc(*"mp4v")
        out = cv2.VideoWriter(str(contam_path), fourcc, 24, (img.shape[1], img.shape[0]))
        for _ in range(48):
            out.write(img)
        out.release()
        verdict = evaluate_shot_gate("shot_kt", str(contam_path), expected_character="kim_trong", force_engine="heuristic")
        assert verdict.overall_score <= 0.50
        assert verdict.shot_eval.character_match is False
        assert verdict.approved is False
        assert verdict.suggested_action == "RETAKE_SHOT"


# =============================================================================
# 3. TEST EVALUATE_SCENE_GATE (TẦNG 2 - SCENE LEVEL)
# =============================================================================

class TestEvaluateSceneGate:
    def test_evaluate_scene_gate_smooth_flow(self, media_sandbox):
        """Toàn cảnh có màu sắc và điểm tiếp biên mượt mà phải đạt >= 0.8."""
        s1 = media_sandbox / "scene_shot_01.mp4"
        s2 = media_sandbox / "scene_shot_02.mp4"
        _create_synthetic_video(s1, num_frames=48, mode="gradient", base_color=(120, 150, 180), start_frame_idx=0)
        _create_synthetic_video(s2, num_frames=48, mode="gradient", base_color=(120, 150, 180), start_frame_idx=48)

        verdict = evaluate_scene_gate(
            scene_id="ep01_scene02",
            shot_video_paths=[str(s1), str(s2)],
            force_engine="heuristic"
        )
        assert verdict.overall_score >= 0.8
        assert verdict.approved is True
        assert verdict.suggested_action == "APPROVE"
        assert verdict.scene_eval is not None
        assert verdict.scene_eval.trigger_color_match is False

    def test_evaluate_scene_gate_color_shift_triggers_color_match(self, media_sandbox):
        """Độ chênh màu đột ngột giữa các shot phải kích hoạt trigger_color_match và APPLY_COLOR_MATCH."""
        s1 = media_sandbox / "scene_color_green.mp4"
        s2 = media_sandbox / "scene_color_red.mp4"
        # s1 màu xanh lá cây rực
        _create_synthetic_video(s1, num_frames=48, mode="solid", base_color=(10, 220, 20))
        # s2 màu đỏ đậm
        _create_synthetic_video(s2, num_frames=48, mode="solid", base_color=(200, 20, 10))

        verdict = evaluate_scene_gate(
            scene_id="ep01_scene02",
            shot_video_paths=[str(s1), str(s2)],
            force_engine="heuristic"
        )
        assert verdict.scene_eval is not None
        assert verdict.scene_eval.trigger_color_match is True
        assert verdict.suggested_action == "APPLY_COLOR_MATCH"

    def test_evaluate_scene_gate_empty_input(self):
        """Danh sách rỗng phải trả về RETAKE_SHOT với score 0.0."""
        verdict = evaluate_scene_gate(
            scene_id="ep01_scene02",
            shot_video_paths=[],
            force_engine="heuristic"
        )
        assert verdict.overall_score == 0.0
        assert verdict.approved is False
        assert verdict.suggested_action == "RETAKE_SHOT"

    def test_evaluate_scene_gate_missing_or_corrupt_files(self):
        """Scene Gate phải từ chối các file không tồn tại với điểm 0.0 và RETAKE_SHOT."""
        verdict = evaluate_scene_gate("sc_fake", ["nonexistent_1.mp4", "nonexistent_2.mp4"], force_engine="heuristic")
        assert verdict.overall_score == 0.0
        assert verdict.approved is False
        assert verdict.suggested_action == "RETAKE_SHOT"

    def test_evaluate_scene_gate_corrupted_files(self, media_sandbox):
        """Scene Gate phải từ chối file rỗng / garbage bytes với điểm 0.0 và RETAKE_SHOT."""
        c1 = media_sandbox / "corrupt_1.mp4"
        c2 = media_sandbox / "corrupt_2.mp4"
        c1.write_bytes(b"NOT_A_VIDEO")
        c2.write_bytes(b"")
        verdict = evaluate_scene_gate("sc_corrupt", [str(c1), str(c2)], force_engine="heuristic")
        assert verdict.overall_score == 0.0
        assert verdict.approved is False
        assert verdict.suggested_action == "RETAKE_SHOT"


# =============================================================================
# 4. TEST TAKE CLASSIFIER & RESOLVE_START_FRAME
# =============================================================================

class TestTakeClassifierAndStartFrame:
    def test_classify_shot_take_scene_opener_is_cut(self):
        """Shot 01 luôn là Cinematic Cut vì mở đầu cảnh."""
        assert po.classify_shot_take("ep01_scene01_shot01") == "CINEMATIC_CUT"
        assert po.classify_shot_take("ep01_scene02_shot01") == "CINEMATIC_CUT"
        assert po.classify_shot_take("ep01_scene07_shot01") == "CINEMATIC_CUT"

    def test_classify_shot_take_actor_switch_is_cut(self):
        """Chuyển vai (Vương Ông sang Vương Quan) bắt buộc phân loại là Cinematic Cut."""
        # Scene 02 Shot 07: Vương Ông -> Shot 08: Vương Quan
        take_type = po.classify_shot_take("ep01_scene02_shot08")
        assert take_type == "CINEMATIC_CUT"

    def test_classify_shot_take_same_actor_is_continuous(self):
        """Cùng nhân vật (Kim Trọng tương tư ở Scene 07) phân loại là Continuous Take."""
        # Scene 07 Shot 01: kim_trong -> Shot 02: kim_trong
        take_type = po.classify_shot_take("ep01_scene07_shot02")
        assert take_type == "CONTINUOUS_TAKE"

    def test_classify_shot_take_scenery_is_cut(self):
        """Cảnh vật / chén trà ('none') bắt buộc phân loại là Cinematic Cut."""
        take_type = po.classify_shot_take("ep01_scene01_shot05")
        assert take_type == "CINEMATIC_CUT"

    def test_resolve_start_frame_cinematic_cut_never_returns_prev_tail(self):
        """Cinematic Cut TUYỆT ĐỐI KHÔNG trả về prev_tail kể cả khi thư mục keyframes có frame cũ."""
        resolved = po.resolve_start_frame("ep01_scene02_shot08", {})
        # Shot 08 là Vương Quan (Cinematic Cut), không được phép trỏ vào keyframes của shot 07
        if resolved:
            assert "ep01_scene02_shot07" not in resolved
            assert "clean_frame_239" not in resolved
            # Phải trỏ vào chân dung chuẩn của Vương Quan
            assert "vuong_quan" in resolved.lower()

    def test_resolve_start_frame_v2_contract(self):
        """Giao diện v2 trả về tuple (resolved_path, take_type)."""
        frame_path, take_type = po.resolve_start_frame_v2("ep01_scene07_shot01")
        assert take_type == "CINEMATIC_CUT"
        assert frame_path is not None
        assert "kim_trong" in frame_path.lower()


# =============================================================================
# 5. TEST BILATERAL & MULTI-CHARACTER SAFEGUARDS WITH LIVE DATA
# =============================================================================

class TestBilateralAndCharacterSafeguards:
    """Kiểm thử xác thực nhân vật với dữ liệu thực tế từ po.get_all_shots()."""

    @pytest.fixture(scope="class")
    @classmethod
    def all_shots(cls):
        return po.get_all_shots()

    def test_cinematic_cut_with_live_shot_data_never_returns_tail(self, all_shots):
        """Cinematic Cut với dữ liệu shot thực tế không được phép trả về tail frame của shot trước."""
        sid = "ep01_scene02_shot08"
        sdata = all_shots.get(sid, {})
        resolved = po.resolve_start_frame(sid, sdata)
        assert resolved is not None
        assert "ep01_scene02_shot07" not in resolved
        assert "clean_frame_239" not in resolved
        assert "vuong_quan" in resolved.lower()

    def test_scene10_live_shots_resolve_authentically(self, all_shots):
        """Scene 10: Kiều độc tấu tỳ bà phải ra Thúy Kiều; Kim Trọng tương tư phải ra Kim Trọng."""
        # Kiều solo & climax pipa shots:
        kieu_shots = [
            "ep01_scene10_shot03",  # Kiều qua rào sang thư phòng
            "ep01_scene10_shot06",  # Kiều giãi bày tấc lòng son
            "ep01_scene10_shot11",  # Kiều ôm đàn tỳ bà so dây dưới trăng
            "ep01_scene10_shot12",  # Tiếng đàn tỳ bà réo rắt khúc tương tư
        ]
        for sid in kieu_shots:
            sdata = all_shots.get(sid, {})
            resolved = po.resolve_start_frame(sid, sdata)
            assert resolved is not None, f"Shot {sid} phải resolve được Start Frame!"
            assert "thuy_kieu" in resolved.lower(), f"{sid} phải ra chân dung Thúy Kiều, thực tế: {resolved}"
            assert "kim_trong" not in resolved.lower(), f"{sid} (Kiều) không được phép ra Kim Trọng: {resolved}"

        # Kim Trọng shots:
        kim_shots = [
            "ep01_scene10_shot04",  # Kim Trọng mở cửa đón Kiều
            "ep01_scene10_shot05",  # Kim Trọng nâng niu bàn tay Kiều
            "ep01_scene10_shot13",  # Kim Trọng say đắm lắng nghe
        ]
        for sid in kim_shots:
            sdata = all_shots.get(sid, {})
            resolved = po.resolve_start_frame(sid, sdata)
            assert resolved is not None, f"Shot {sid} phải resolve được Start Frame!"
            assert "kim_trong" in resolved.lower(), f"{sid} phải ra chân dung Kim Trọng, thực tế: {resolved}"
            assert "thuy_kieu" not in resolved.lower(), f"{sid} (Kim Trọng) không được phép ra Thúy Kiều: {resolved}"

    def test_scene06_live_shots_resolve_authentically(self, all_shots):
        """Scene 06: Các cú máy Kim Trọng, Thúy Kiều và Vương Quan resolve chuẩn xác."""
        kim_shots = [
            "ep01_scene06_shot03", "ep01_scene06_shot04", "ep01_scene06_shot06",
            "ep01_scene06_shot10", "ep01_scene06_shot12", "ep01_scene06_shot14"
        ]
        for sid in kim_shots:
            sdata = all_shots.get(sid, {})
            resolved = po.resolve_start_frame(sid, sdata)
            assert resolved is not None, f"Shot {sid} phải resolve được Start Frame!"
            assert "kim_trong" in resolved.lower(), f"{sid} phải ra Kim Trọng, nhận: {resolved}"
            assert "thuy_kieu" not in resolved.lower(), f"Leak Thúy Kiều tại Kim Trọng shot {sid}: {resolved}"
            assert "vuong_quan" not in resolved.lower(), f"Leak Vương Quan tại Kim Trọng shot {sid}: {resolved}"

        # Vương Quan shot 07
        s7_data = all_shots.get("ep01_scene06_shot07", {})
        s7_res = po.resolve_start_frame("ep01_scene06_shot07", s7_data)
        assert s7_res is not None
        assert "vuong_quan" in s7_res.lower()

        # Thúy Kiều shots 08, 09, 11
        kieu_shots = ["ep01_scene06_shot08", "ep01_scene06_shot09", "ep01_scene06_shot11"]
        for sid in kieu_shots:
            sdata = all_shots.get(sid, {})
            res = po.resolve_start_frame(sid, sdata)
            assert res is not None
            assert "thuy_kieu" in res.lower(), f"{sid} phải ra Thúy Kiều, nhận: {res}"

    def test_scene05_live_vuong_quan_shots_resolve_authentically(self, all_shots):
        """Scene 05: Các cú máy Vương Quan thuật chuyện và can ngăn không bị biến thành Thúy Kiều."""
        for sid in ["ep01_scene05_shot13", "ep01_scene05_shot16", "ep01_scene05_shot24"]:
            sdata = all_shots.get(sid, {})
            resolved = po.resolve_start_frame(sid, sdata)
            assert resolved is not None, f"Shot {sid} phải resolve được Start Frame!"
            assert "vuong_quan" in resolved.lower(), f"{sid} phải ra Vương Quan, nhận: {resolved}"
            assert "thuy_kieu" not in resolved.lower(), f"Leak Thúy Kiều tại Vương Quan shot {sid}: {resolved}"

    def test_adversarial_bilateral_safeguards_override(self):
        """Kiểm tra phản ứng đối kháng (Adversarial Robustness): Gán nhầm asset đối nghịch phải bị ghi đè tự động."""
        # 1. Cố tình gán Thúy Kiều vào cú máy Kim Trọng -> Phải tự sửa thành Kim Trọng
        adv_kt = {
            "character_asset_ref": "04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png",
            "scene_title": "Kim Trọng tương tư dưới trăng"
        }
        res_kt = po.resolve_start_frame("ep01_scene07_shot01", adv_kt)
        assert res_kt is not None
        assert "kim_trong" in res_kt.lower()
        assert "thuy_kieu" not in res_kt.lower()

        # 2. Cố tình gán Kim Trọng vào cú máy Kiều đàn tỳ bà -> Phải tự sửa thành Thúy Kiều
        adv_tk = {
            "character_asset_ref": "04_Assets/characters/01_Main_Protagonists/kim_trong_18yo_720p.png",
            "scene_title": "Kiều gảy đàn tỳ bà réo rắt khúc tương tư"
        }
        res_tk = po.resolve_start_frame("ep01_scene10_shot11", adv_tk)
        assert res_tk is not None
        assert "thuy_kieu" in res_tk.lower()
        assert "kim_trong" not in res_tk.lower()

        # 3. Cố tình gán Thúy Kiều vào cú máy Vương Quan can ngăn -> Phải tự sửa thành Vương Quan
        adv_vq = {
            "character_asset_ref": "04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png",
            "scene_title": "Trung cận Vương Quan can ngăn"
        }
        res_vq = po.resolve_start_frame("ep01_scene05_shot24", adv_vq)
        assert res_vq is not None
        assert "vuong_quan" in res_vq.lower()
        assert "thuy_kieu" not in res_vq.lower()

        # 4. Cố tình gán Thúy Kiều vào cú máy Vương Ông đạo mạo -> Phải tự sửa thành Vương Ông
        adv_vo = {
            "character_asset_ref": "04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png",
            "scene_title": "Vương Ông đĩnh đạc gia phong"
        }
        res_vo = po.resolve_start_frame("ep01_scene02_shot04", adv_vo)
        assert res_vo is not None
        assert "vuong_ong" in res_vo.lower()
        assert "thuy_kieu" not in res_vo.lower()

        # 5a. Cố tình gán Thúy Kiều qua character_asset_ref vào cú máy Thúy Vân -> Phải tự sửa thành Thúy Vân
        adv_tv_asset = {
            "character_asset_ref": "04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png",
            "scene_title": "Thúy Vân trang trọng đoan trang",
            "motion_prompt": "Thúy Vân mỉm cười ung dung"
        }
        res_tv_asset = po.resolve_start_frame("ep01_scene01_shot05", adv_tv_asset)
        assert res_tv_asset is not None
        assert "thuy_van" in res_tv_asset.lower()
        assert "thuy_kieu" not in res_tv_asset.lower()

        # 5b. Cố tình gán Thúy Kiều qua reference_start_frame vào cú máy Thúy Vân -> Step C & D phải chặn và sửa thành Thúy Vân
        adv_tv_ref = {
            "reference_start_frame": "04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png",
            "scene_title": "Thúy Vân trang trọng đoan trang",
            "motion_prompt": "Thúy Vân mỉm cười ung dung"
        }
        res_tv_ref = po.resolve_start_frame("ep01_scene01_shot05", adv_tv_ref)
        assert res_tv_ref is not None
        assert "thuy_van" in res_tv_ref.lower()
        assert "thuy_kieu" not in res_tv_ref.lower()

        # 6a. Cố tình gán Thúy Kiều vào cú máy compound anchor (ep01_scene10_shot07: kim_trong + thuy_kieu_maiden)
        # khi scene_title/motion_prompt chỉ rõ Kim Trọng -> Safeguard không bị vô hiệu hóa bởi anchor chung
        adv_kt_compound_asset = {
            "character_asset_ref": "04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png",
            "scene_title": "Kim Trọng viết lời thề nguyền",
            "motion_prompt": "Kim Trọng cầm bút lông viết chữ"
        }
        res_kt_compound_asset = po.resolve_start_frame("ep01_scene10_shot07", adv_kt_compound_asset)
        assert res_kt_compound_asset is not None
        assert "kim_trong" in res_kt_compound_asset.lower()
        assert "thuy_kieu" not in res_kt_compound_asset.lower()

        # 6b. Cố tình gán Thúy Kiều qua reference_start_frame vào compound anchor shot ep01_scene10_shot07
        adv_kt_compound_ref = {
            "reference_start_frame": "04_Assets/characters/01_Main_Protagonists/thuy_kieu_maiden_16yo_720p.png",
            "scene_title": "Kim Trọng viết lời thề nguyền",
            "motion_prompt": "Kim Trọng cầm bút lông viết chữ"
        }
        res_kt_compound_ref = po.resolve_start_frame("ep01_scene10_shot07", adv_kt_compound_ref)
        assert res_kt_compound_ref is not None
        assert "kim_trong" in res_kt_compound_ref.lower()
        assert "thuy_kieu" not in res_kt_compound_ref.lower()

        # 7. Cú máy attendant tiểu đồng theo hầu (ep01_scene06_shot05: tieu_dong_kim_trong)
        # không bị nuốt chuỗi thành Kim Trọng mà phải ra đúng tiểu đồng
        res_td = po.resolve_start_frame("ep01_scene06_shot05", {})
        assert res_td is not None
        assert "tieu_dong" in res_td.lower()
        assert "01_main_protagonists" not in res_td.lower()


if __name__ == "__main__":
    pytest_args = [__file__, "-v"]
    sys.exit(pytest.main(pytest_args))
