"""
=============================================================================
THẬP NGŨ NIÊN (THE FIFTEEN SPRINGS) - FLOWKIT AUTONOMOUS PIPELINE TEST SUITE
=============================================================================
Authoritative Test Suite for FlowKit-inspired Autonomous Upgrades:
1. Zero-Waste Contact Sheet Tiling Engine:
   - Exact divisor math (cols_eff * rows_eff == n_frames)
   - Zero empty black cells (prevents Vision LLM hallucinating black screen defects)
   - High-contrast timestamp badges [T=X.Xs | #Frame N]
   - JPEG byte stream encoding
2. Strict 3-Tier Error Catalog (14 Known AI Video Errors) & Hard Caps:
   - 5 Critical Errors: character_drift, breed_swap, role_reversal, brand_logo_text, character_count_drift
   - 5 High Errors: camera_drift, object_morph, reverse_motion, human_hands_limbs, scale_break
   - 4 Minor Errors: lighting_jitter, micro_blur, background_warp, color_shift
   - Critical hard cap: character_consistency <= 0.30, overall_score <= 0.59, approved=False, RETAKE_SHOT
   - High hard cap: overall_score <= 0.74, approved=False, RETAKE_SHOT
   - Deterministic model validator overriding LLM benevolence
3. Diagnostic Prompt Healing (Self-Correction Loop):
   - Targeted anti-drift / locked-camera / anatomy injection
   - Critical > High > Minor priority injection
   - Preservation of Audio Guard (No-BGM) and Closed Lips Guard
   - Strict cycle bounding (MAX_HEALING_CYCLES = 2)
4. Cinematic 3-Beat Shot Formula & Camera Separation:
   - Beat 1 (0-3s), Beat 2 (3-6s), Beat 3 (6-10s)
   - Dedicated camera movement line
5. Post-Production Smart Trim & Audio Continuity:
   - Dynamic head trim (-ss 1.0) on static first frames
   - Narrator fitting to speech duration + buffer
   - Smart stitching: xfade for CONTINUOUS_TAKE vs hard cut for CINEMATIC_CUT
=============================================================================
"""

import os
import sys
import json
import shutil
import tempfile
import subprocess
from pathlib import Path
from typing import List, Dict, Any, Tuple
import pytest
import numpy as np
import cv2

PROJECT_ROOT = Path(__file__).resolve().parent.parent
PIPELINE_DIR = PROJECT_ROOT / "05_Production_Pipeline"
if str(PIPELINE_DIR) not in sys.path:
    sys.path.insert(0, str(PIPELINE_DIR))

from contact_sheet_engine import (
    compute_zero_waste_grid,
    normalize_frame_count,
    ZeroWasteContactSheet,
    generate_zero_waste_contact_sheet,
    extract_contact_sheet_bytes,
)
from prompt_healer import (
    MAX_HEALING_CYCLES,
    ERROR_THERAPY_CATALOG,
    DiagnosticPromptHealer,
    CinematicShotPacer,
)
from antigravity_critic_gate import (
    FLOWKIT_ERROR_TIERS,
    FLOWKIT_ERROR_CATALOG,
    VideoCriticVerdict,
    ShotEvaluation,
    SceneEvaluation,
)
from audio_continuity_engine import (
    AudioContinuityEngine,
    get_ffmpeg,
    get_ffprobe,
)


# =============================================================================
# FIXTURES
# =============================================================================

@pytest.fixture(scope="session")
def flowkit_sandbox(tmp_path_factory):
    """Tạo thư mục sandbox dùng cho kiểm thử video và audio."""
    temp_dir = tmp_path_factory.mktemp("flowkit_test_sandbox")
    ffmpeg = get_ffmpeg()

    # 1. Tạo video động có chuyển động (2.5 giây, 24fps = 60 frames)
    moving_video = temp_dir / "moving_clip.mp4"
    cmd_moving = [
        ffmpeg, "-y",
        "-f", "lavfi", "-i", "testsrc=duration=2.5:size=640x360:rate=24",
        "-f", "lavfi", "-i", "sine=frequency=440:duration=2.5:sample_rate=48000",
        "-c:v", "libx264", "-pix_fmt", "yuv420p",
        "-c:a", "aac", "-b:a", "128k", "-ar", "48000",
        str(moving_video)
    ]
    subprocess.run(cmd_moving, capture_output=True, check=True)

    # 2. Tạo video có 1 giây đầu tĩnh (24 frames tĩnh, sau đó chuyển động)
    static_head_video = temp_dir / "static_head_clip.mp4"
    # Dùng 1 giây testsrc2 tĩnh rồi nối với moving clip
    cmd_static = [
        ffmpeg, "-y",
        "-f", "lavfi", "-i", "color=c=navy:duration=1.2:size=640x360:rate=24",
        "-f", "lavfi", "-i", "testsrc=duration=2.0:size=640x360:rate=24",
        "-f", "lavfi", "-i", "sine=frequency=440:duration=3.0:sample_rate=48000",
        "-filter_complex", "[0:v][1:v]concat=n=2:v=1:a=0[v_out]",
        "-map", "[v_out]", "-map", "2:a",
        "-c:v", "libx264", "-pix_fmt", "yuv420p",
        "-c:a", "aac", "-b:a", "128k", "-ar", "48000",
        str(static_head_video)
    ]
    subprocess.run(cmd_static, capture_output=True, check=True)

    # 3. Tạo file audio giọng đọc mẫu (narration) 2.0s
    narration_audio = temp_dir / "narration_sample.m4a"
    cmd_audio = [
        ffmpeg, "-y",
        "-f", "lavfi", "-i", "sine=frequency=330:duration=2.0:sample_rate=48000",
        "-c:a", "aac", "-b:a", "128k", "-ar", "48000",
        str(narration_audio)
    ]
    subprocess.run(cmd_audio, capture_output=True, check=True)

    yield {
        "dir": temp_dir,
        "moving_video": moving_video,
        "static_head_video": static_head_video,
        "narration_audio": narration_audio,
        "ffmpeg": ffmpeg,
    }


# =============================================================================
# 1. ZERO-WASTE CONTACT SHEET TILING TESTS
# =============================================================================

class TestZeroWasteContactSheetTiling:
    """Kiểm chứng toán học ước số chính xác và triệt tiêu 100% ô đen rác."""

    def test_exact_divisor_grid_math(self):
        """cols_eff * rows_eff BẮT BUỘC bằng chính xác n_frames."""
        golden_counts = [1, 2, 3, 4, 6, 8, 9, 10, 12, 16]
        for n in golden_counts:
            cols, rows = compute_zero_waste_grid(n)
            assert cols * rows == n, f"Lỗi với n={n}: {cols}x{rows} != {n}"
            assert cols >= rows, f"Cols phải >= Rows cho màn hình ngang 16:9: {cols} < {rows} (n={n})"

    def test_normalize_frame_count(self):
        """Chuẩn hóa các số nguyên tố/số xấu về các số chia vàng gần nhất."""
        assert normalize_frame_count(1) == 1
        assert normalize_frame_count(2) == 2
        assert normalize_frame_count(3) == 3
        assert normalize_frame_count(4) == 4
        assert normalize_frame_count(5) in (4, 6)
        assert normalize_frame_count(6) == 6
        assert normalize_frame_count(7) in (6, 8)
        assert normalize_frame_count(8) == 8
        assert normalize_frame_count(11) in (10, 12)

    def test_create_contact_sheet_dimensions_and_zero_black_cells(self, flowkit_sandbox):
        """Tạo contact sheet thực tế từ video và đảm bảo kích thước chính xác tuyệt đối."""
        video_path = flowkit_sandbox["moving_video"]
        sheet_path = flowkit_sandbox["dir"] / "test_sheet_6frames.jpg"

        sheet_img, out_path = generate_zero_waste_contact_sheet(
            video_path=video_path,
            num_frames=6,
            output_path=sheet_path,
            target_size=(1920, 1080)
        )

        assert sheet_img is not None
        assert out_path is not None and out_path.exists()
        assert out_path.stat().st_size > 5000

        # Kiểm tra kích thước hình ảnh
        h, w, c = sheet_img.shape
        cols, rows = compute_zero_waste_grid(6)  # cols=3, rows=2
        assert cols == 3 and rows == 2
        assert w == (1920 // cols) * cols
        assert h == (1080 // rows) * rows

        # Kiểm tra không có ô nào là đen rác hoàn toàn (mean > 0)
        cell_w = w // cols
        cell_h = h // rows
        for r in range(rows):
            for col in range(cols):
                cell = sheet_img[r * cell_h:(r + 1) * cell_h, col * cell_w:(col + 1) * cell_w]
                # Mỗi ô phải chứa nội dung video, không được là đen rỗng (zeros)
                assert np.mean(cell) > 5.0, f"Ô ({r}, {col}) bị rỗng đen (unfilled black cell)!"

    def test_extract_contact_sheet_bytes(self, flowkit_sandbox):
        """Trích xuất JPEG byte stream trực tiếp cho Multimodal Vision LLM."""
        video_path = flowkit_sandbox["moving_video"]
        raw_bytes = extract_contact_sheet_bytes(video_path, num_frames=6)
        assert raw_bytes is not None
        assert len(raw_bytes) > 5000
        # Kiểm tra JPEG magic bytes: \xff\xd8\xff
        assert raw_bytes.startswith(b"\xff\xd8\xff")


# =============================================================================
# 2. STRICT 3-TIER ERROR CATALOG & HARD CAPS TESTS
# =============================================================================

class TestFlowKitStrictErrorCatalogAndHardCaps:
    """Kiểm chứng danh mục 14 lỗi và cơ chế Hard Caps tuyệt đối."""

    def test_catalog_14_errors_complete(self):
        """Xác nhận đủ 14 lỗi theo 3 tầng nghiêm ngặt (5 Critical, 5 High, 4 Minor)."""
        assert len(FLOWKIT_ERROR_TIERS) == 14
        assert len(FLOWKIT_ERROR_CATALOG) == 14

        critical_errors = [k for k, v in FLOWKIT_ERROR_TIERS.items() if v == "critical"]
        high_errors = [k for k, v in FLOWKIT_ERROR_TIERS.items() if v == "high"]
        minor_errors = [k for k, v in FLOWKIT_ERROR_TIERS.items() if v == "minor"]

        assert len(critical_errors) == 5
        assert set(critical_errors) == {
            "character_drift", "breed_swap", "role_reversal",
            "brand_logo_text", "character_count_drift"
        }

        assert len(high_errors) == 5
        assert set(high_errors) == {
            "camera_drift", "object_morph", "reverse_motion",
            "human_hands_limbs", "scale_break"
        }

        assert len(minor_errors) == 4
        assert set(minor_errors) == {
            "lighting_jitter", "micro_blur", "background_warp", "color_shift"
        }

    def test_critical_error_forces_hard_caps(self):
        """Critical Error BẮT BUỘC ép character_consistency <= 0.30, overall_score <= 0.59, approved=False."""
        # Giả lập LLM gửi về kết quả khoan dung (score cao) dù có character_drift
        verdict = VideoCriticVerdict(
            overall_score=0.98,
            approved=True,
            suggested_action="APPROVE",
            detected_errors=["character_drift"],
            shot_eval=ShotEvaluation(
                character_consistency=0.95,
                score=0.95,
                character_match=True
            )
        )

        # Hard cap phải tự động kích hoạt đè bẹp sự khoan dung của LLM
        assert verdict.shot_eval.character_consistency <= 0.30
        assert verdict.overall_score <= 0.59
        assert verdict.approved is False
        assert verdict.suggested_action == "RETAKE_SHOT"
        assert verdict.shot_eval.character_match is False
        assert verdict.shot_eval.score <= 0.59

    def test_all_5_critical_errors_trigger_hard_caps(self):
        """Tất cả 5 lỗi Critical đều kích hoạt cùng một cơ chế chặn cứng."""
        critical_list = [
            "character_drift", "breed_swap", "role_reversal",
            "brand_logo_text", "character_count_drift"
        ]
        for crit_err in critical_list:
            v = VideoCriticVerdict(
                overall_score=0.90,
                approved=True,
                detected_errors=[crit_err],
                shot_eval=ShotEvaluation(character_consistency=0.90, score=0.90)
            )
            assert v.overall_score <= 0.59, f"Lỗi {crit_err} không hạ overall_score <= 0.59"
            assert v.approved is False, f"Lỗi {crit_err} không từ chối phê duyệt"
            assert v.suggested_action == "RETAKE_SHOT"
            assert v.shot_eval.character_consistency <= 0.30

    def test_high_error_forces_hard_caps(self):
        """High Error BẮT BUỘC ép overall_score <= 0.74, approved=False, suggested_action=RETAKE_SHOT."""
        high_list = [
            "camera_drift", "object_morph", "reverse_motion",
            "human_hands_limbs", "scale_break"
        ]
        for high_err in high_list:
            v = VideoCriticVerdict(
                overall_score=0.92,
                approved=True,
                detected_errors=[high_err],
                shot_eval=ShotEvaluation(score=0.92)
            )
            assert v.overall_score <= 0.74, f"Lỗi {high_err} không hạ overall_score <= 0.74"
            assert v.approved is False, f"Lỗi {high_err} không hủy phê duyệt"
            assert v.suggested_action == "RETAKE_SHOT"

    def test_minor_errors_allow_approval_if_score_passes(self):
        """Minor Errors không chặn cứng, cho phép duyệt nếu overall_score >= 0.8."""
        v = VideoCriticVerdict(
            overall_score=0.85,
            approved=True,
            suggested_action="APPROVE",
            detected_errors=["lighting_jitter", "micro_blur"],
            shot_eval=ShotEvaluation(score=0.85)
        )
        assert v.overall_score == 0.85
        assert v.approved is True
        assert v.suggested_action == "APPROVE"


# =============================================================================
# 3. DIAGNOSTIC PROMPT HEALING TESTS
# =============================================================================

class TestDiagnosticPromptHealing:
    """Kiểm chứng phác đồ điều trị prompt tự hành (Self-Correction Loop)."""

    def test_therapy_catalog_covers_all_14_errors(self):
        """Bản đồ điều trị ERROR_THERAPY_CATALOG phải hỗ trợ đủ 14 lỗi."""
        assert len(ERROR_THERAPY_CATALOG) == 14
        for err_key in FLOWKIT_ERROR_TIERS:
            assert err_key in ERROR_THERAPY_CATALOG, f"Thiếu liệu pháp điều trị cho: {err_key}"
            assert "therapy" in ERROR_THERAPY_CATALOG[err_key]
            assert "vi_therapy" in ERROR_THERAPY_CATALOG[err_key]
            assert "tier" in ERROR_THERAPY_CATALOG[err_key]

    def test_heal_prompt_injects_therapies_and_preserves_guards(self):
        """Bơm chỉ thị kháng lỗi và bảo toàn nguyên vẹn Audio Guard & Closed Lips Guard ở cuối."""
        raw_prompt = (
            "Thúy Kiều ngắm trăng bên hồ sen."
            " [KHÓA KHẨU HÌNH BẮT BUỘC: Môi khép kín, không mấp máy thoại]"
            " Quy tắc âm thanh: Tuyệt đối không nhạc nền BGM, chỉ giữ tiếng foley tiếng gió thổi."
        )

        healed, applied = DiagnosticPromptHealer.heal_prompt(
            original_prompt=raw_prompt,
            detected_errors=["character_drift", "camera_drift"],
            character_name="Thúy Kiều",
            cycle=1
        )

        assert len(applied) == 2
        assert "character_drift (critical)" in applied
        assert "camera_drift (high)" in applied

        # Kiểm tra nội dung trị liệu xuất hiện trong prompt
        assert "HEALING-CRITICAL" in healed
        assert "HEALING-HIGH" in healed
        assert "Thúy Kiều" in healed

        # Kiểm tra Audio Guard và Closed Lips Guard nằm ở cuối cùng
        assert healed.endswith("chỉ giữ tiếng foley tiếng gió thổi.")
        assert "Quy tắc âm thanh:" in healed
        assert "[KHÓA KHẨU HÌNH BẮT BUỘC" in healed

    def test_heal_prompt_cycle_bounding(self):
        """Vòng lặp tự sửa BẮT BUỘC dừng sau tối đa 2 chu trình (MAX_HEALING_CYCLES = 2)."""
        raw_prompt = "Kim Trọng dừng ngựa trước ngõ đào."
        
        # Chu trình 1: Cho phép sửa
        h1, a1 = DiagnosticPromptHealer.heal_prompt(raw_prompt, ["object_morph"], cycle=1)
        assert len(a1) == 1
        assert "HEALING-HIGH" in h1

        # Chu trình 2: Cho phép sửa
        h2, a2 = DiagnosticPromptHealer.heal_prompt(raw_prompt, ["object_morph"], cycle=2)
        assert len(a2) == 1

        # Chu trình 3 (> MAX_HEALING_CYCLES): Dừng triệt để, không sửa tiếp
        h3, a3 = DiagnosticPromptHealer.heal_prompt(raw_prompt, ["object_morph"], cycle=3)
        assert a3 == []
        assert h3 == raw_prompt

    def test_heal_prompt_critical_first_priority(self):
        """Chỉ thị Critical luôn được ưu tiên bơm trước chỉ thị High và Minor."""
        raw_prompt = "Vương Quan đứng nhìn chị gái."
        healed, applied = DiagnosticPromptHealer.heal_prompt(
            raw_prompt,
            detected_errors=["lighting_jitter", "breed_swap", "camera_drift"],
            character_name="Vương Quan",
            cycle=1
        )
        assert applied[0].startswith("breed_swap")  # Critical
        assert applied[1].startswith("camera_drift")  # High
        assert applied[2].startswith("lighting_jitter")  # Minor


# =============================================================================
# 4. CINEMATIC 3-BEAT SHOT FORMULA TESTS
# =============================================================================

class TestCinematic3BeatShotPacer:
    """Kiểm chứng cấu trúc nhịp điệu 3 hồi điện ảnh và tách biệt camera."""

    def test_format_3beat_prompt_auto_parsing(self):
        """Tự động phân rã văn xuôi thành 3 Beat và câu lệnh Camera độc lập."""
        prompt = (
            "Nàng Kiều tựa cửa sổ nhìn cành đào phai. "
            "Gió xuân thoảng qua lay động tà áo xanh ngọc. "
            "Một giọt nước mắt khẽ lăn trên má, lắng lại trầm ngâm."
        )
        paced = CinematicShotPacer.format_3beat_prompt(
            raw_prompt=prompt,
            camera_motion="Slow push-in 50mm anamorphic"
        )

        assert "[Beat 1 (0-3s) - Thiết lập]:" in paced
        assert "[Beat 2 (3-6s) - Kịch tính & Cảm xúc]:" in paced
        assert "[Beat 3 (6-10s) - Lắng đọng & Nối tiếp]:" in paced
        assert "[Chuyển động Camera]: Slow push-in 50mm anamorphic" in paced

    def test_format_3beat_prompt_explicit_beats(self):
        """Sử dụng 3 beat tường minh do đạo diễn chỉ định."""
        paced = CinematicShotPacer.format_3beat_prompt(
            raw_prompt="Shot tổng",
            camera_motion="Static lock-off",
            beat_1_desc="Kiều bước vào phòng",
            beat_2_desc="Kiều chạm tay vào cây đàn đáy",
            beat_3_desc="Kiều khép mắt nhớ người xưa"
        )
        assert "Kiều bước vào phòng" in paced
        assert "Kiều chạm tay vào cây đàn đáy" in paced
        assert "Kiều khép mắt nhớ người xưa" in paced
        assert "[Chuyển động Camera]: Static lock-off" in paced


# =============================================================================
# 5. SMART TRIM, NARRATOR FITTING & SMART STITCH TESTS
# =============================================================================

class TestSmartTrimAndPostProduction:
    """Kiểm chứng Dynamic Head Trim, Narrator Fitting và Smart Stitching."""

    def test_smart_trim_detects_static_head(self, flowkit_sandbox):
        """Phát hiện 1 giây đầu tĩnh và tự động cắt tỉa -ss 1.0."""
        engine = AudioContinuityEngine()
        static_video = flowkit_sandbox["static_head_video"]
        trimmed_out = flowkit_sandbox["dir"] / "trimmed_static_head.mp4"

        out = engine.smart_trim_shot_head(
            video_path=str(static_video),
            output_path=str(trimmed_out),
            trim_dur=1.0,
            detect_static=True
        )

        assert Path(out).exists()
        assert out == str(trimmed_out)

        # Kiểm tra thời lượng bị rút ngắn khoảng 1.0s (từ 3.0s xuống ~2.0s)
        rep_orig = engine.inspect_shot_audio(str(static_video))
        rep_trim = engine.inspect_shot_audio(out)
        assert rep_trim["duration"] < rep_orig["duration"]
        assert abs(rep_orig["duration"] - rep_trim["duration"] - 1.0) < 0.25

    def test_smart_trim_skips_dynamic_head(self, flowkit_sandbox):
        """Video có chuyển động ngay từ đầu thì không cắt tỉa."""
        engine = AudioContinuityEngine()
        moving_video = flowkit_sandbox["moving_video"]
        trimmed_out = flowkit_sandbox["dir"] / "skip_moving_head.mp4"

        out = engine.smart_trim_shot_head(
            video_path=str(moving_video),
            output_path=str(trimmed_out),
            trim_dur=1.0,
            detect_static=True
        )

        # Trả về nguyên bản không cắt
        assert out == str(moving_video)
        assert not trimmed_out.exists()

    def test_fit_footage_to_narration(self, flowkit_sandbox):
        """Khớp thời lượng video với giọng đọc + 0.3s buffer và chuẩn hóa EBU R128."""
        engine = AudioContinuityEngine()
        moving_video = flowkit_sandbox["moving_video"]  # 2.5s
        narration_audio = flowkit_sandbox["narration_audio"]  # 2.0s
        fitted_out = flowkit_sandbox["dir"] / "fitted_narration.mp4"

        ok = engine.fit_footage_to_narration(
            video_path=str(moving_video),
            narration_audio_path=str(narration_audio),
            output_path=str(fitted_out),
            buffer_sec=0.3
        )

        assert ok is True
        assert fitted_out.exists()
        rep = engine.inspect_shot_audio(str(fitted_out))
        assert rep["has_audio"] is True
        # Thời lượng phải là 2.0s + 0.3s = 2.3s (+- 0.15s)
        assert abs(rep["duration"] - 2.3) < 0.20
        # Âm lượng phải nằm trong ngưỡng EBU R128 (-14 LUFS +- 2.5)
        meas = engine.measure_loudness(str(fitted_out))
        assert -16.5 <= float(meas["input_i"]) <= -11.5

    def test_stitch_flowkit_smart_continuous_vs_cinematic_cut(self, flowkit_sandbox):
        """Ghép nối thông minh: xfade cho CONTINUOUS_TAKE và hard cut cho CINEMATIC_CUT."""
        engine = AudioContinuityEngine()
        v1 = flowkit_sandbox["moving_video"]
        v2 = flowkit_sandbox["moving_video"]

        # Kịch bản 1: CONTINUOUS_TAKE -> xfade 0.5s + acrossfade 0.5s
        entries_continuous = [
            {"video_path": str(v1), "take_type": "CINEMATIC_CUT"},
            {"video_path": str(v2), "take_type": "CONTINUOUS_TAKE"}
        ]
        out_cont = flowkit_sandbox["dir"] / "stitch_continuous.mp4"
        ok_cont = engine.stitch_flowkit_smart(entries_continuous, str(out_cont), xfade_dur=0.5)
        assert ok_cont is True
        assert out_cont.exists()
        rep_cont = engine.inspect_shot_audio(str(out_cont))
        assert rep_cont["has_audio"] is True

        # Kiểm chứng triệt tiêu lệch đồng bộ A/V: Video duration và Audio duration phải khớp chính xác
        ffprobe_exe = flowkit_sandbox.get("ffprobe") or engine.ffprobe
        probe_res = subprocess.run(
            [ffprobe_exe, "-v", "error", "-show_entries", "stream=codec_type,duration:format=duration", "-of", "json", str(out_cont)],
            capture_output=True, text=True, check=True
        )
        p_data = json.loads(probe_res.stdout)
        v_dur = float([s["duration"] for s in p_data["streams"] if s["codec_type"] == "video"][0])
        a_dur = float([s["duration"] for s in p_data["streams"] if s["codec_type"] == "audio"][0])
        assert abs(v_dur - a_dur) < 0.05, f"Lệch đồng bộ A/V: Video={v_dur:.3f}s, Audio={a_dur:.3f}s (delta={abs(v_dur-a_dur):.3f}s)"

        # Kịch bản 2: CINEMATIC_CUT -> hard cut + 30ms micro-fade
        entries_cut = [
            {"video_path": str(v1), "take_type": "CINEMATIC_CUT"},
            {"video_path": str(v2), "take_type": "CINEMATIC_CUT"}
        ]
        out_cut = flowkit_sandbox["dir"] / "stitch_cut.mp4"
        ok_cut = engine.stitch_flowkit_smart(entries_cut, str(out_cut))
        assert ok_cut is True
        assert out_cut.exists()
        rep_cut = engine.inspect_shot_audio(str(out_cut))
        assert rep_cut["has_audio"] is True

    def test_stitch_flowkit_smart_3shot_mixed_chaining(self, flowkit_sandbox):
        """Kiểm chứng ghép nối đa shot (3+ shots) với tổ hợp xen kẽ CONTINUOUS_TAKE và CINEMATIC_CUT."""
        engine = AudioContinuityEngine()
        v = flowkit_sandbox["moving_video"]
        out_mixed = flowkit_sandbox["dir"] / "stitch_3shot_mixed.mp4"

        entries = [
            {"video_path": str(v), "take_type": "CINEMATIC_CUT"},
            {"video_path": str(v), "take_type": "CONTINUOUS_TAKE"},
            {"video_path": str(v), "take_type": "CINEMATIC_CUT"}
        ]
        ok = engine.stitch_flowkit_smart(entries, str(out_mixed), xfade_dur=0.5)
        assert ok is True
        assert out_mixed.exists()

        ffprobe_exe = flowkit_sandbox.get("ffprobe") or engine.ffprobe
        probe_res = subprocess.run(
            [ffprobe_exe, "-v", "error", "-show_entries", "stream=codec_type,duration:format=duration", "-of", "json", str(out_mixed)],
            capture_output=True, text=True, check=True
        )
        p_data = json.loads(probe_res.stdout)
        v_dur = float([s["duration"] for s in p_data["streams"] if s["codec_type"] == "video"][0])
        a_dur = float([s["duration"] for s in p_data["streams"] if s["codec_type"] == "audio"][0])
        assert abs(v_dur - a_dur) < 0.05, f"Lệch đồng bộ 3-shot: Video={v_dur:.3f}s, Audio={a_dur:.3f}s"

    def test_smart_trim_short_clip_safeguard(self, flowkit_sandbox):
        """Video quá ngắn (<= 1.5s) được bảo vệ an toàn, không bị cắt trụi (-ss 1.0)."""
        engine = AudioContinuityEngine()
        temp_dir = flowkit_sandbox["dir"]
        short_video = temp_dir / "ultra_short_clip.mp4"

        # Tạo clip 1.2s
        cmd = [
            engine.ffmpeg, "-y",
            "-f", "lavfi", "-i", "testsrc=duration=1.2:size=640x360:rate=24",
            "-f", "lavfi", "-i", "sine=frequency=440:duration=1.2:sample_rate=48000",
            "-c:v", "libx264", "-c:a", "aac", str(short_video)
        ]
        subprocess.run(cmd, capture_output=True, check=True)

        out = engine.smart_trim_shot_head(str(short_video), detect_static=False, trim_dur=1.0)
        # Safeguard phải giữ nguyên file gốc thay vì tạo file rỗng
        assert out == str(short_video)

    def test_heal_prompt_newline_guard_and_canonical_order(self):
        """Prompt Healer xử lý chuẩn xác xuống dòng, Closed Lips variant và bảo lưu Audio Guard ở cuối."""
        p = "Nàng Kiều nhìn xa xăm.\n[KHÓA KHẨU HÌNH: Đôi môi khép chặt]\nQuy tắc âm thanh: Tuyệt đối KHÔNG sinh nhạc nền."
        healed, therapies = DiagnosticPromptHealer.heal_prompt(p, ["character_drift", "camera_drift"], "Thúy Kiều")
        assert len(therapies) == 2
        # Audio Guard phải luôn ở cuối cùng
        assert healed.endswith("Quy tắc âm thanh: Tuyệt đối KHÔNG sinh nhạc nền.")
        # Closed Lips Guard phải đứng trước Audio Guard
        assert "[KHÓA KHẨU HÌNH" in healed
        assert healed.index("[KHÓA KHẨU HÌNH") < healed.index("Quy tắc âm thanh:")
        # Therapy injections phải nằm trong thân prompt trước cả 2 guard
        assert "[HEALING-CRITICAL:" in healed
        assert healed.index("[HEALING-CRITICAL:") < healed.index("[KHÓA KHẨU HÌNH")

    def test_contact_sheet_preserve_aspect_ratio(self, flowkit_sandbox):
        """Contact sheet với preserve_aspect_ratio=True duy trì tỉ lệ pixel 16:9 không méo hình."""
        video_path = flowkit_sandbox["moving_video"]
        out_sheet = flowkit_sandbox["dir"] / "ar_preserved_sheet.jpg"
        sheet_img, p = generate_zero_waste_contact_sheet(video_path, num_frames=6, output_path=out_sheet)
        assert sheet_img is not None
        assert p.exists()

    def test_orchestrator_cli_flowkit_smart_mode(self):
        """CLI của production_orchestrator hỗ trợ đầy đủ --mode flowkit_smart và --pacing-3beat."""
        import subprocess
        import sys
        res = subprocess.run(
            [sys.executable, "05_Production_Pipeline/production_orchestrator.py", "--help"],
            capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
        )
        assert res.stdout is not None
        assert "flowkit_smart" in res.stdout
        assert "--pacing-3beat" in res.stdout
