"""
=============================================================================
THẬP NGŨ NIÊN (THE FIFTEEN SPRINGS) - ADVERSARIAL CRITIC GATE TEST SUITE
=============================================================================
Milestone M1 Challenger Empirical Verification Harness.
Adversarially tests:
1. Pydantic v2 Schema: round-trip serialization, boundary conditions (0.79 vs 0.80),
   invalid types, out-of-range floats, nan/inf, action overrides.
2. evaluate_shot_gate (Tầng 1):
   - Corrupted files (0-byte, invalid headers, truncated).
   - Missing files.
   - Black frames defect detection.
   - Frozen frames defect detection and score behavior.
   - Ultra-short / 1-frame video behavior.
   - Character cross-contamination detection.
3. evaluate_scene_gate (Tầng 2):
   - Smooth flow across sequential shots.
   - Severe color shift triggering color match.
   - Empty input list resilience.
   - Adversarial edge case: missing and corrupted files in scene list.
=============================================================================
"""

import os
import sys
import json
import math
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,
)


@pytest.fixture(scope="session")
def adv_media_dir(tmp_path_factory):
    """Thư mục tạm thời sinh video cho các bài kiểm tra đối kháng."""
    temp_dir = tmp_path_factory.mktemp("adv_critic_media")
    yield temp_dir
    shutil.rmtree(str(temp_dir), ignore_errors=True)


def _write_mp4_video(
    filepath: Path,
    num_frames: int = 48,
    fps: int = 24,
    width: int = 1280,
    height: int = 720,
    color_bgr: tuple = (100, 150, 200),
    is_motion: bool = True
) -> Path:
    """Hàm phụ trợ sinh file video MP4 chuẩn."""
    fourcc = cv2.VideoWriter_fourcc(*"mp4v")
    out = cv2.VideoWriter(str(filepath), fourcc, fps, (width, height))
    try:
        for i in range(num_frames):
            frame = np.full((height, width, 3), color_bgr, dtype=np.uint8)
            if is_motion:
                # Thêm chuyển động vi mô để phân biệt với frozen frame
                offset = int(np.sin(i / 10.0) * 15)
                cx = int(width / 2 + offset * 10)
                cv2.circle(frame, (cx, int(height / 2)), 60, (255, 255, 255), -1)
            out.write(frame)
    finally:
        out.release()
    return filepath


# =============================================================================
# 1. PYDANTIC V2 SCHEMA ADVERSARIAL STRESS TESTS
# =============================================================================

class TestPydanticAdversarialSchema:
    def test_roundtrip_complete_hierarchy(self):
        """Stress-test: round-trip serialization with all sub-models and defect lists."""
        shot_eval = ShotEvaluation(
            character_match=True,
            character_confidence=0.98,
            visual_defects=["minor_grain", "edge_fringe"],
            squint_or_extra_limbs=False,
            audio_guard_ok=True,
            score=0.94
        )
        scene_eval = SceneEvaluation(
            junction_smoothness=0.92,
            axis_180_ok=True,
            eyeline_ok=True,
            color_continuity=0.91,
            trigger_color_match=False,
            tempo_ok=True,
            trigger_trim_static=False,
            score=0.92
        )
        verdict = VideoCriticVerdict(
            overall_score=0.93,
            approved=True,
            shot_eval=shot_eval,
            scene_eval=scene_eval,
            suggested_action="APPROVE",
            critique_notes="Stress-test: full hierarchical roundtrip."
        )

        dumped = verdict.model_dump_json()
        restored = VideoCriticVerdict.model_validate_json(dumped)

        assert restored == verdict
        assert restored.shot_eval.visual_defects == ["minor_grain", "edge_fringe"]
        assert restored.scene_eval.junction_smoothness == 0.92
        assert restored.approved is True

    def test_score_boundary_precision(self):
        """Stress-test: exact boundary condition around 0.8000 (0.7999 vs 0.8000)."""
        # Exactly 0.7999 must NOT be approved
        v_sub = VideoCriticVerdict(overall_score=0.7999, suggested_action="APPROVE")
        assert v_sub.approved is False

        # Exactly 0.7900 must NOT be approved
        v_79 = VideoCriticVerdict(overall_score=0.79, suggested_action="APPROVE")
        assert v_79.approved is False

        # Exactly 0.8000 MUST be approved
        v_80 = VideoCriticVerdict(overall_score=0.8000, suggested_action="APPROVE")
        assert v_80.approved is True

        # Exactly 0.8001 MUST be approved
        v_super = VideoCriticVerdict(overall_score=0.8001, suggested_action="APPROVE")
        assert v_super.approved is True

    def test_retake_action_overrides_high_score(self):
        """Stress-test: even with perfect 1.0 score, RETAKE_SHOT forces approved=False."""
        verdict = VideoCriticVerdict(
            overall_score=1.0,
            approved=True,  # Cố tình truyền True để kiểm tra model_validator
            suggested_action="RETAKE_SHOT",
            critique_notes="Cần quay lại do vi phạm kịch bản."
        )
        assert verdict.approved is False

    @pytest.mark.parametrize("invalid_val", [
        -0.001, -1.0, 1.001, 2.0,
        float("nan"), float("inf"), float("-inf"),
        "invalid_text", None
    ])
    def test_invalid_overall_score_types_and_bounds(self, invalid_val):
        """Stress-test: VideoCriticVerdict rejects out-of-range floats, NaNs, Infs, and invalid types."""
        with pytest.raises(ValidationError):
            VideoCriticVerdict(overall_score=invalid_val)

    @pytest.mark.parametrize("invalid_val", [-0.1, 1.1, float("nan"), "bad"])
    def test_shot_evaluation_boundary_constraints(self, invalid_val):
        """Stress-test: ShotEvaluation strictly validates confidence and score boundaries."""
        with pytest.raises(ValidationError):
            ShotEvaluation(character_confidence=invalid_val)

        with pytest.raises(ValidationError):
            ShotEvaluation(score=invalid_val)

    @pytest.mark.parametrize("invalid_val", [-0.1, 1.1, float("nan"), "bad"])
    def test_scene_evaluation_boundary_constraints(self, invalid_val):
        """Stress-test: SceneEvaluation strictly validates continuity and junction boundaries."""
        with pytest.raises(ValidationError):
            SceneEvaluation(junction_smoothness=invalid_val)

        with pytest.raises(ValidationError):
            SceneEvaluation(color_continuity=invalid_val)

        with pytest.raises(ValidationError):
            SceneEvaluation(score=invalid_val)


# =============================================================================
# 2. EVALUATE_SHOT_GATE ADVERSARIAL STRESS TESTS
# =============================================================================

class TestEvaluateShotGateAdversarial:
    def test_shot_gate_zero_byte_corrupted_file(self, adv_media_dir):
        """Stress-test: 0-byte file must be immediately rejected with score 0.0 and RETAKE_SHOT."""
        p = adv_media_dir / "zero_byte.mp4"
        p.write_bytes(b"")

        verdict = evaluate_shot_gate("shot_0b", str(p), force_engine="heuristic")
        assert verdict.overall_score == 0.0
        assert verdict.approved is False
        assert verdict.suggested_action == "RETAKE_SHOT"
        assert verdict.shot_eval is not None
        assert verdict.shot_eval.score == 0.0

    def test_shot_gate_truncated_garbage_bytes(self, adv_media_dir):
        """Stress-test: file with garbage header must be rejected with score 0.0."""
        p = adv_media_dir / "garbage_header.mp4"
        p.write_bytes(b"\x00\x00\x00\x20ftypmp42RANDOM_CORRUPT_BYTES_DATA")

        verdict = evaluate_shot_gate("shot_garbage", str(p), force_engine="heuristic")
        assert verdict.overall_score == 0.0
        assert verdict.approved is False
        assert verdict.suggested_action == "RETAKE_SHOT"

    def test_shot_gate_nonexistent_file_path(self):
        """Stress-test: non-existent file path returns score 0.0 and RETAKE_SHOT."""
        verdict = evaluate_shot_gate("shot_missing", "c:/no_such_directory/nonexistent.mp4", force_engine="heuristic")
        assert verdict.overall_score == 0.0
        assert verdict.approved is False
        assert verdict.suggested_action == "RETAKE_SHOT"

    def test_shot_gate_pure_black_frames(self, adv_media_dir):
        """Stress-test: 100% black frames video must fail quality gate."""
        p = adv_media_dir / "pure_black_120f.mp4"
        _write_mp4_video(p, num_frames=120, color_bgr=(0, 0, 0), is_motion=False)

        verdict = evaluate_shot_gate("shot_black", str(p), force_engine="heuristic")
        assert verdict.overall_score < 0.80
        assert verdict.approved is False
        assert verdict.suggested_action == "RETAKE_SHOT"
        assert any("Black frame" in d for d in verdict.shot_eval.visual_defects)

    def test_shot_gate_frozen_frames_defect_reporting(self, adv_media_dir):
        """
        Stress-test: frozen frames (zero motion across 120 frames / 5 seconds)
        must register the defect 'Frozen video detected'.
        """
        p = adv_media_dir / "frozen_120f.mp4"
        _write_mp4_video(p, num_frames=120, color_bgr=(120, 140, 160), is_motion=False)

        verdict = evaluate_shot_gate("shot_frozen", str(p), force_engine="heuristic")
        assert verdict.shot_eval is not None
        assert any("Frozen" in d for d in verdict.shot_eval.visual_defects), (
            f"Expected 'Frozen' defect in defects list: {verdict.shot_eval.visual_defects}"
        )

    def test_shot_gate_character_cross_contamination(self, adv_media_dir):
        """
        Stress-test: if expected character is Kim Trọng, but video frame matches
        Thúy Kiều master portrait, the gate must detect character cross-contamination.
        """
        kieu_portrait = PROJECT_ROOT / "04_Assets" / "characters" / "01_Main_Protagonists" / "thuy_kieu_maiden_16yo_720p.png"
        if not kieu_portrait.exists():
            pytest.skip("Thuy Kieu master portrait not found on disk.")

        kieu_img = cv2.imread(str(kieu_portrait))
        if kieu_img is None:
            pytest.skip("Unable to read Thuy Kieu portrait.")

        # Sinh video mô phỏng khuôn hình Thúy Kiều
        p = adv_media_dir / "simulated_kieu_video.mp4"
        fourcc = cv2.VideoWriter_fourcc(*"mp4v")
        h, w = kieu_img.shape[:2]
        out = cv2.VideoWriter(str(p), fourcc, 24, (w, h))
        try:
            for _ in range(48):
                out.write(kieu_img)
        finally:
            out.release()

        verdict = evaluate_shot_gate(
            shot_id="ep01_scene07_shot01",
            video_path=str(p),
            expected_character="kim_trong",
            force_engine="heuristic"
        )
        # Bắt buộc phải phát hiện mismatch hoặc giảm điểm
        assert verdict.shot_eval.character_match is False or verdict.overall_score < 0.80


# =============================================================================
# 3. EVALUATE_SCENE_GATE ADVERSARIAL STRESS TESTS
# =============================================================================

class TestEvaluateSceneGateAdversarial:
    def test_scene_gate_smooth_flow(self, adv_media_dir):
        """Stress-test: sequential shots with consistent color palette achieve score >= 0.8."""
        s1 = adv_media_dir / "smooth_shot_1.mp4"
        s2 = adv_media_dir / "smooth_shot_2.mp4"
        _write_mp4_video(s1, num_frames=48, color_bgr=(100, 150, 200), is_motion=True)
        _write_mp4_video(s2, num_frames=48, color_bgr=(100, 150, 200), is_motion=True)

        verdict = evaluate_scene_gate("scene_smooth", [str(s1), str(s2)], force_engine="heuristic")
        assert verdict.overall_score >= 0.80
        assert verdict.approved is True
        assert verdict.suggested_action == "APPROVE"
        assert verdict.scene_eval.trigger_color_match is False

    def test_scene_gate_severe_color_shift_triggers_color_match(self, adv_media_dir):
        """Stress-test: severe color shift (pure green vs pure red) triggers color match action."""
        s1 = adv_media_dir / "pure_green_shot.mp4"
        s2 = adv_media_dir / "pure_red_shot.mp4"
        _write_mp4_video(s1, num_frames=48, color_bgr=(0, 255, 0), is_motion=False)
        _write_mp4_video(s2, num_frames=48, color_bgr=(0, 0, 255), is_motion=False)

        verdict = evaluate_scene_gate("scene_color_shift", [str(s1), str(s2)], force_engine="heuristic")
        assert verdict.scene_eval.trigger_color_match is True
        assert verdict.suggested_action == "APPLY_COLOR_MATCH"
        assert verdict.overall_score < 0.80

    def test_scene_gate_empty_list_rejection(self):
        """Stress-test: empty shot list must return score 0.0 and RETAKE_SHOT."""
        verdict = evaluate_scene_gate("scene_empty", [], force_engine="heuristic")
        assert verdict.overall_score == 0.0
        assert verdict.approved is False
        assert verdict.suggested_action == "RETAKE_SHOT"


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