#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
=============================================================================
CHALLENGER 1 EMPIRICAL HARNESS: FALLBACK CONCAT WITH SILENT CLIPS (M4 ITER 2)
=============================================================================
Milestone: M4 Iteration 2 Gate Verification
Author: Challenger 1 (critic, specialist)

Empirically tests:
1. Canonical resolution of ffprobe executable without FileNotFoundError.
2. Fallback Concat with production-standard 10.0s clips (mixed audio & silent -an).
3. Fallback Concat with production-standard 10.0s clips (100% silent -an).
4. Stress analysis: Audio duration behavior with non-10.0s clips in fallback path.
5. Verification of output container duration, AAC codec, 48000Hz, stereo (2ch).
=============================================================================
"""

import os
import sys
import json
import shutil
import subprocess
import tempfile
from pathlib import Path
from unittest.mock import patch
import pytest

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))

import production_orchestrator as po
from audio_continuity_engine import AudioContinuityEngine, get_ffmpeg, get_ffprobe


def create_synthetic_clip(path: Path, duration: float = 10.0, freq: int = 440, has_audio: bool = True, color: str = "black") -> Path:
    """Tạo video synthetic nhẹ (160x120, ultrafast) có hoặc không có audio track."""
    ffmpeg = get_ffmpeg()
    path.parent.mkdir(parents=True, exist_ok=True)
    cmd = [
        ffmpeg, "-y",
        "-f", "lavfi", "-i", f"color=c={color}:s=160x120:d={duration}"
    ]
    if has_audio:
        cmd.extend([
            "-f", "lavfi", "-i", f"sine=frequency={freq}:sample_rate=48000:duration={duration}",
            "-map", "0:v", "-map", "1:a",
            "-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p",
            "-c:a", "aac", "-b:a", "128k", "-ar", "48000", "-ac", "2"
        ])
    else:
        cmd.extend([
            "-an", "-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p"
        ])
    cmd.extend(["-t", str(duration), str(path)])
    subprocess.run(cmd, capture_output=True, check=True)
    return path


def probe_media_file(path: Path) -> dict:
    """Phân tích container và stream bằng ffprobe."""
    ffprobe = get_ffprobe()
    cmd = [
        ffprobe, "-v", "error",
        "-show_entries", "stream=index,codec_type,codec_name,sample_rate,channels,duration:format=duration,size",
        "-of", "json", str(path)
    ]
    res = subprocess.run(cmd, capture_output=True, text=True, check=True)
    data = json.loads(res.stdout)
    streams = data.get("streams", [])
    fmt = data.get("format", {})

    v_stream = next((s for s in streams if s.get("codec_type") == "video"), None)
    a_stream = next((s for s in streams if s.get("codec_type") == "audio"), None)

    return {
        "format_duration": float(fmt.get("duration", 0.0)),
        "size_bytes": int(fmt.get("size", 0)),
        "has_video": v_stream is not None,
        "video_codec": v_stream.get("codec_name") if v_stream else None,
        "video_duration": float(v_stream.get("duration", fmt.get("duration", 0.0))) if v_stream else 0.0,
        "has_audio": a_stream is not None,
        "audio_codec": a_stream.get("codec_name") if a_stream else None,
        "sample_rate": int(a_stream.get("sample_rate", 0)) if a_stream else 0,
        "channels": int(a_stream.get("channels", 0)) if a_stream else 0,
        "audio_duration": float(a_stream.get("duration", fmt.get("duration", 0.0))) if a_stream else 0.0,
    }


class TestChallengerM4FallbackConcatProbe:
    """Empirical probe suite for Milestone M4 Iteration 2 Gate Verification."""

    def test_01_verify_ffprobe_executable_resolution(self):
        """Xác nhận get_ffprobe() giải quyết đúng đường dẫn thực thi và không văng FileNotFoundError."""
        ffprobe_exe = get_ffprobe()
        ffmpeg_exe = get_ffmpeg()
        assert os.path.exists(ffprobe_exe), f"FFprobe path does not exist on disk: {ffprobe_exe}"
        assert os.path.exists(ffmpeg_exe), f"FFmpeg path does not exist on disk: {ffmpeg_exe}"

        res = subprocess.run([ffprobe_exe, "-version"], capture_output=True, text=True)
        assert res.returncode == 0, f"ffprobe execution failed with returncode {res.returncode}"
        assert "ffprobe version" in res.stdout

    def test_02_production_standard_10s_fallback_concat_mixed(self, tmp_path):
        """Xác nhận Fallback Concat trên các shot chuẩn 10s (có và không có audio) ghép nối hoàn hảo và đồng bộ A/V."""
        c1 = create_synthetic_clip(tmp_path / "shot_01.mp4", duration=10.0, freq=440, has_audio=True, color="red")
        c2 = create_synthetic_clip(tmp_path / "shot_02.mp4", duration=10.0, has_audio=False, color="blue")
        c3 = create_synthetic_clip(tmp_path / "shot_03.mp4", duration=10.0, freq=880, has_audio=True, color="green")

        meta_c1 = probe_media_file(c1)
        meta_c2 = probe_media_file(c2)
        meta_c3 = probe_media_file(c3)
        assert meta_c1["has_audio"] is True
        assert meta_c2["has_audio"] is False
        assert meta_c3["has_audio"] is True

        out_mixed = tmp_path / "mixed_master_10s_v1.mp4"
        mock_shots_mixed = [
            ("ep01_test_shot01", {}),
            ("ep01_test_shot02", {}),
            ("ep01_test_shot03", {})
        ]
        shot_map_mixed = {
            "ep01_test_shot01": c1,
            "ep01_test_shot02": c2,
            "ep01_test_shot03": c3
        }

        with patch("production_orchestrator.get_shots_for_scene", return_value=mock_shots_mixed), \
             patch("production_orchestrator.find_rendered_video", side_effect=lambda sid: shot_map_mixed.get(sid)), \
             patch.object(AudioContinuityEngine, "stitch_with_audio_crossfade", return_value=False):
            result = po.concat_scene_shots("ep01_test_mixed_scene", output_path=str(out_mixed), enable_critic=False)

        assert result is not None, "Fallback concat returned None!"
        assert out_mixed.exists(), "Output master file was not created!"
        meta_out = probe_media_file(out_mixed)
        assert meta_out["has_audio"] is True, "Output file missing audio stream!"
        assert meta_out["audio_codec"] == "aac", f"Expected aac, got {meta_out['audio_codec']}"
        assert meta_out["sample_rate"] == 48000, f"Expected 48000Hz, got {meta_out['sample_rate']}"
        assert meta_out["channels"] == 2, f"Expected stereo (2 channels), got {meta_out['channels']}"
        assert abs(meta_out["format_duration"] - 30.0) < 0.25, f"Expected duration ~30.0s, got {meta_out['format_duration']}"
        assert abs(meta_out["video_duration"] - 30.0) < 0.25, f"Expected video duration ~30.0s, got {meta_out['video_duration']}"
        assert abs(meta_out["audio_duration"] - 30.0) < 0.25, f"Expected audio duration ~30.0s, got {meta_out['audio_duration']}"

    def test_03_production_standard_10s_fallback_concat_all_silent(self, tmp_path):
        """Xác nhận Fallback Concat trên 100% clip câm (-an) tự động sinh luồng AAC 48kHz stereo không crash."""
        s1 = create_synthetic_clip(tmp_path / "silent_01.mp4", duration=10.0, has_audio=False, color="black")
        s2 = create_synthetic_clip(tmp_path / "silent_02.mp4", duration=10.0, has_audio=False, color="white")

        out_all_silent = tmp_path / "all_silent_master_10s_v1.mp4"
        mock_shots_silent = [
            ("ep01_silent_shot01", {}),
            ("ep01_silent_shot02", {})
        ]
        shot_map_silent = {
            "ep01_silent_shot01": s1,
            "ep01_silent_shot02": s2
        }

        with patch("production_orchestrator.get_shots_for_scene", return_value=mock_shots_silent), \
             patch("production_orchestrator.find_rendered_video", side_effect=lambda sid: shot_map_silent.get(sid)), \
             patch.object(AudioContinuityEngine, "stitch_with_audio_crossfade", return_value=False):
            result_silent = po.concat_scene_shots("ep01_test_silent_scene", output_path=str(out_all_silent), enable_critic=False)

        assert result_silent is not None
        assert out_all_silent.exists()
        meta_silent = probe_media_file(out_all_silent)
        assert meta_silent["has_audio"] is True
        assert meta_silent["audio_codec"] == "aac"
        assert meta_silent["sample_rate"] == 48000
        assert meta_silent["channels"] == 2
        assert abs(meta_silent["format_duration"] - 20.0) < 0.25
        assert abs(meta_silent["video_duration"] - 20.0) < 0.25
        assert abs(meta_silent["audio_duration"] - 20.0) < 0.25

    def test_04_adversarial_non_10s_clip_fallback_behavior(self, tmp_path):
        """Đặc tả thực nghiệm: Fallback concat khi clip không đủ 10s (aevalsrc d=10.0 cố định cho shot chuẩn Muse)."""
        t1 = create_synthetic_clip(tmp_path / "trim_01.mp4", duration=2.0, has_audio=False, color="cyan")
        t2 = create_synthetic_clip(tmp_path / "trim_02.mp4", duration=2.0, freq=500, has_audio=True, color="magenta")
        out_trim = tmp_path / "trim_master_v1.mp4"

        mock_shots_trim = [("ep01_trim_shot01", {}), ("ep01_trim_shot02", {})]
        shot_map_trim = {"ep01_trim_shot01": t1, "ep01_trim_shot02": t2}

        with patch("production_orchestrator.get_shots_for_scene", return_value=mock_shots_trim), \
             patch("production_orchestrator.find_rendered_video", side_effect=lambda sid: shot_map_trim.get(sid)), \
             patch.object(AudioContinuityEngine, "stitch_with_audio_crossfade", return_value=False):
            result_trim = po.concat_scene_shots("ep01_test_trim_scene", output_path=str(out_trim), enable_critic=False)

        assert result_trim is not None
        assert out_trim.exists()
        meta_trim = probe_media_file(out_trim)
        assert meta_trim["has_audio"] is True
        assert meta_trim["audio_codec"] == "aac"
        assert meta_trim["channels"] == 2
        assert meta_trim["sample_rate"] == 48000
        # Characterize: Fallback generates 10s for the silent clip + 2s for audio clip = 12s audio vs 4s video
        assert abs(meta_trim["audio_duration"] - 12.0) < 0.25
        assert abs(meta_trim["video_duration"] - 4.0) < 0.25


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