#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
=============================================================================
THẬP NGŨ NIÊN (THE FIFTEEN SPRINGS) - ADVERSARIAL AUDIO CONCAT PROBE (M4)
=============================================================================
Milestone: M4 Gate Verification - Empirical Challenger 1
Focus:
  1. Multi-Shot Mode A vs Mode B Audio Duration Drift & A/V Cut Sync
  2. Silent Clip & Missing Audio Track Resilience (Engine vs Orchestrator Fallback)
  3. Boundary Smoothing 0.000s Duration Loss Invariant
=============================================================================
"""

import os
import sys
import json
import math
import shutil
import subprocess
from pathlib import Path
from typing import Dict, List, Any
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))

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


def make_test_clip(
    output_path: Path,
    duration: float = 2.0,
    freq: int = 440,
    color: str = "black",
    has_audio: bool = True,
    mute_audio: bool = False,
    sample_rate: int = 48000,
    channels: int = 2
) -> Path:
    """Tạo video synthetic siêu nhẹ (160x120, ultrafast) để chạy probe cực nhanh."""
    ffmpeg = get_ffmpeg()
    output_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:
        if mute_audio:
            cmd.extend([
                "-f", "lavfi", "-i", f"aevalsrc=0:s={sample_rate}:d={duration}",
                "-map", "0:v", "-map", "1:a",
                "-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p",
                "-c:a", "aac", "-b:a", "128k", "-ar", str(sample_rate), "-ac", str(channels)
            ])
        else:
            cmd.extend([
                "-f", "lavfi", "-i", f"sine=frequency={freq}:sample_rate={sample_rate}:duration={duration}",
                "-map", "0:v", "-map", "1:a",
                "-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p",
                "-c:a", "aac", "-b:a", "128k", "-ar", str(sample_rate), "-ac", str(channels)
            ])
    else:
        # Hoàn toàn không có stream audio (-an)
        cmd.extend([
            "-an", "-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p"
        ])
    cmd.extend(["-t", str(duration), str(output_path)])
    subprocess.run(cmd, capture_output=True, check=True)
    return output_path


def probe_file(media_path: Path) -> Dict[str, Any]:
    """Phân tích chi tiết container, video và audio streams qua ffprobe."""
    ffprobe = get_ffprobe()
    cmd = [
        ffprobe, "-v", "error",
        "-show_entries", "stream=codec_type,codec_name,sample_rate,channels,duration,nb_frames:format=duration,size",
        "-of", "json", str(media_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,
    }


def detect_tail_silence(media_path: Path, min_silence_dur: float = 0.5, noise_threshold: str = "-50dB") -> float:
    """Đo thời lượng im lặng ở cuối file âm thanh qua FFmpeg silencedetect."""
    ffmpeg = get_ffmpeg()
    cmd = [
        ffmpeg, "-i", str(media_path),
        "-af", f"silencedetect=noise={noise_threshold}:d={min_silence_dur}",
        "-f", "null", "-"
    ]
    res = subprocess.run(cmd, capture_output=True, text=True)
    stderr = res.stderr

    silence_starts = []
    for line in stderr.splitlines():
        if "silence_start:" in line:
            parts = line.split("silence_start:")
            try:
                silence_starts.append(float(parts[1].split()[0]))
            except ValueError:
                pass
    if silence_starts:
        last_start = silence_starts[-1]
        meta = probe_file(media_path)
        total_dur = meta["format_duration"]
        if total_dur > last_start:
            return total_dur - last_start
    return 0.0


class TestAdversarialM4AudioConcatProbe:
    """Tập hợp các probe thực nghiệm đối kháng kiểm chứng Milestone M4."""

    def test_probe_01_mode_a_vs_mode_b_multi_shot_timeline_drift(self, tmp_path):
        """
        PROBE 1: Đo độ co rút thời lượng (duration shrinkage) và lệch pha A/V giữa Mode A và Mode B.
        - Tạo 6 shots x 2.0s = 12.0s tổng.
        - Mode B (acrossfade, d=0.5s):
          * Độ co rút âm thanh lý thuyết = (6 - 1) x 0.5s = 2.5s!
          * Audio bị apad đệm 2.5s câm ở đuôi.
          * Nội dung âm thanh của shot 6 bị đẩy sớm 2.5s so với hình ảnh (phá vỡ cut sync).
        - Mode A (boundary_smoothing, 30ms micro-fade):
          * Độ co rút âm thanh = 0.000s!
          * Âm thanh shot 6 bắt đầu chính xác tại timestamp 10.0s (khớp 100% video cut).
          * Không có khoảng câm nhân tạo ở đuôi.
        """
        num_shots = 6
        shot_dur = 2.0
        total_expected_dur = num_shots * shot_dur  # 12.0s
        crossfade_dur = 0.5

        clips = []
        for i in range(num_shots):
            p = tmp_path / f"shot_{i+1}.mp4"
            make_test_clip(p, duration=shot_dur, freq=300 + i * 100)
            clips.append(str(p))

        engine = AudioContinuityEngine()

        # 1. Chạy Mode B: acrossfade
        out_mode_b = tmp_path / "probe_mode_b_acrossfade.mp4"
        ok_b = engine.stitch_with_audio_crossfade(
            clips,
            str(out_mode_b),
            crossfade_dur=crossfade_dur,
            normalize_lufs=False,
            mode="acrossfade"
        )
        assert ok_b is True
        assert out_mode_b.exists()

        meta_b = probe_file(out_mode_b)
        assert meta_b["has_audio"] is True
        tail_silence_b = detect_tail_silence(out_mode_b, min_silence_dur=1.0)
        expected_shrinkage = (num_shots - 1) * crossfade_dur  # 2.5s

        print(f"\n[PROBE 1 - Mode B Results]:")
        print(f"  Total Video Duration: {meta_b['video_duration']:.3f}s")
        print(f"  Container Duration:   {meta_b['format_duration']:.3f}s")
        print(f"  Detected Tail Silence: {tail_silence_b:.3f}s (Theoretical shrinkage: {expected_shrinkage:.3f}s)")
        assert tail_silence_b >= expected_shrinkage - 0.5, (
            f"Mode B must have tail silence caused by acrossfade shrinkage! Detected: {tail_silence_b}s"
        )

        # 2. Chạy Mode A: boundary_smoothing
        out_mode_a = tmp_path / "probe_mode_a_boundary_smoothing.mp4"
        ok_a = engine.stitch_with_audio_crossfade(
            clips,
            str(out_mode_a),
            crossfade_dur=crossfade_dur,
            normalize_lufs=False,
            mode="boundary_smoothing"
        )
        assert ok_a is True
        assert out_mode_a.exists()

        meta_a = probe_file(out_mode_a)
        assert meta_a["has_audio"] is True
        tail_silence_a = detect_tail_silence(out_mode_a, min_silence_dur=1.0)

        duration_diff = abs(meta_a["audio_duration"] - total_expected_dur)
        av_diff = abs(meta_a["audio_duration"] - meta_a["video_duration"])

        print(f"\n[PROBE 1 - Mode A Results]:")
        print(f"  Total Video Duration: {meta_a['video_duration']:.3f}s")
        print(f"  Total Audio Duration: {meta_a['audio_duration']:.3f}s")
        print(f"  Duration Difference:  {duration_diff:.4f}s")
        print(f"  A/V Drift:            {av_diff:.4f}s")
        print(f"  Detected Tail Silence: {tail_silence_a:.3f}s")

        assert duration_diff < 0.1, f"Mode A duration difference too large: {duration_diff}s"
        assert av_diff < 0.05, f"Mode A A/V drift too large: {av_diff}s"
        assert tail_silence_a < 0.5, f"Mode A should NOT have tail silence! Detected: {tail_silence_a}s"

    def test_probe_02_ten_shot_scale_mode_a_zero_drift_verification(self, tmp_path):
        """
        PROBE 2: Kiểm chứng quy mô lớn 10 shots (chuẩn 10 shots x 1.0s).
        Xác nhận với 10 shots, Mode A vẫn giữ vững 0.000s duration loss,
        trong khi Mode B sẽ gây co rút nghiêm trọng (9 x crossfade_dur).
        """
        num_shots = 10
        shot_dur = 1.0
        expected_total = num_shots * shot_dur  # 10.0s

        clips = []
        for i in range(num_shots):
            p = tmp_path / f"scale_shot_{i+1:02d}.mp4"
            make_test_clip(p, duration=shot_dur, freq=400 + i * 50)
            clips.append(str(p))

        engine = AudioContinuityEngine()
        out_scale_a = tmp_path / "probe_10_shots_mode_a.mp4"

        ok = engine.stitch_with_audio_crossfade(
            clips,
            str(out_scale_a),
            crossfade_dur=0.3,
            normalize_lufs=False,
            mode="boundary_smoothing"
        )
        assert ok is True
        assert out_scale_a.exists()

        meta = probe_file(out_scale_a)
        assert meta["has_audio"] is True
        assert meta["audio_codec"] == "aac"
        assert meta["sample_rate"] == 48000
        assert meta["channels"] == 2

        drift = abs(meta["audio_duration"] - expected_total)
        print(f"\n[PROBE 2 - 10 Shots Scale Mode A]: Expected {expected_total}s | Actual {meta['audio_duration']:.3f}s | Drift = {drift:.4f}s")
        assert drift < 0.1, f"10-shot Mode A drift must be < 0.1s, got {drift}s"

    def test_probe_03a_engine_silent_and_missing_stream_resilience(self, tmp_path):
        """
        PROBE 3A: Xác nhận AudioContinuityEngine chống chịu hoàn hảo với clip thiếu stream audio (-an) và clip câm.
        """
        c1 = make_test_clip(tmp_path / "c1_normal.mp4", duration=0.8, freq=440, has_audio=True)
        c2 = make_test_clip(tmp_path / "c2_no_audio.mp4", duration=0.8, has_audio=False)
        c3 = make_test_clip(tmp_path / "c3_mute.mp4", duration=0.8, has_audio=True, mute_audio=True)
        c4 = make_test_clip(tmp_path / "c4_normal.mp4", duration=0.8, freq=880, has_audio=True)

        clips = [str(c1), str(c2), str(c3), str(c4)]
        expected_total = 4 * 0.8  # 3.2s

        engine = AudioContinuityEngine()
        out_engine = tmp_path / "probe_silent_engine.mp4"
        ok_eng = engine.stitch_with_audio_crossfade(
            clips,
            str(out_engine),
            crossfade_dur=0.1,
            normalize_lufs=True,
            mode="boundary_smoothing"
        )
        assert ok_eng is True, "AudioContinuityEngine must handle missing audio streams without crash!"
        assert out_engine.exists()

        meta_eng = probe_file(out_engine)
        assert meta_eng["has_audio"] is True
        assert meta_eng["audio_codec"] == "aac"
        assert meta_eng["sample_rate"] == 48000
        assert meta_eng["channels"] == 2
        assert abs(meta_eng["format_duration"] - expected_total) < 0.25
        print(f"\n[PROBE 3A - Engine Concat Resilience]: {meta_eng['audio_codec']} {meta_eng['sample_rate']}Hz stereo, dur={meta_eng['format_duration']:.2f}s [PASSED]")

    def test_probe_03b_orchestrator_fallback_ffprobe_path_bug(self, tmp_path):
        """
        PROBE 3B [CRITICAL VULNERABILITY]:
        Kiểm chứng lỗi đường dẫn ffprobe tại production_orchestrator.py:1170.
        Biểu thức `ffmpeg_exe.replace('ffmpeg', 'ffprobe')` đã biến đổi:
        `ffmpeg-9.0.2-full_build\bin\ffmpeg.exe` thành `ffprobe-9.0.2-full_build\bin\ffprobe.exe` (không tồn tại!).
        Hậu quả: Lỗi FileNotFoundError bị nuốt chửng bởi `except Exception: has_a = True`,
        khiến Fallback Concat luôn phán đoán clip có audio, dẫn tới crash `:a matches no streams` trên FFmpeg.
        """
        ffmpeg_exe = get_ffmpeg()
        corrupted_ffprobe = ffmpeg_exe.replace("ffmpeg", "ffprobe")
        is_corrupted_path_existing = os.path.exists(corrupted_ffprobe)

        print(f"\n[PROBE 3B - FFprobe Path Vulnerability Analysis]:")
        print(f"  ffmpeg_exe:        {ffmpeg_exe}")
        print(f"  corrupted_ffprobe: {corrupted_ffprobe}")
        print(f"  Path exists?:      {is_corrupted_path_existing}")

        # Thao tác này chứng minh đường dẫn sinh ra bị lỗi trên Windows WinGet package
        if "ffmpeg-9.0.2-full_build" in ffmpeg_exe:
            assert is_corrupted_path_existing is False, (
                "Empirical proof: corrupted_ffprobe points to a non-existent directory!"
            )

        # Kiểm chứng khi dùng get_ffprobe() chính tắc
        canonical_ffprobe = get_ffprobe()
        print(f"  canonical_ffprobe: {canonical_ffprobe}")
        assert os.path.exists(canonical_ffprobe), "get_ffprobe() points to a valid executable"

    def test_probe_04_dialogue_cut_alignment_at_timeline_tail(self, tmp_path):
        """
        PROBE 4: Kiểm chứng tính đồng bộ tuyệt đối của lời thoại / âm thanh ở cuối timeline.
        - Shot 1 (0.0s - 2.0s): Tần số thấp 220Hz.
        - Shot 2 (2.0s - 4.0s): Tần số cao 1000Hz (đại diện cho thoại quan trọng ở shot 2).
        - Trong Mode A: Âm thanh 1000Hz của Shot 2 BẮT BUỘC bắt đầu chính xác tại timestamp 2.0s (±0.03s do micro-fade).
        - Trong Mode B (crossfade=1.0s): Âm thanh 1000Hz bị kéo về bắt đầu tại timestamp 1.0s (sớm 1.0s trước khi chuyển cảnh video!).
        """
        s1 = make_test_clip(tmp_path / "dialogue_s1.mp4", duration=2.0, freq=220)
        s2 = make_test_clip(tmp_path / "dialogue_s2.mp4", duration=2.0, freq=1000)

        clips = [str(s1), str(s2)]
        engine = AudioContinuityEngine()

        out_a = tmp_path / "probe_sync_mode_a.mp4"
        engine.stitch_with_audio_crossfade(clips, str(out_a), crossfade_dur=1.0, normalize_lufs=False, mode="boundary_smoothing")

        out_b = tmp_path / "probe_sync_mode_b.mp4"
        engine.stitch_with_audio_crossfade(clips, str(out_b), crossfade_dur=1.0, normalize_lufs=False, mode="acrossfade")

        meta_a = probe_file(out_a)
        meta_b = probe_file(out_b)

        assert abs(meta_a["format_duration"] - 4.0) < 0.1
        assert abs(meta_b["format_duration"] - 4.0) < 0.1

        ffmpeg = get_ffmpeg()
        def extract_sample_volume(video_file: Path, start: float, duration: float, freq: int) -> float:
            cmd = [
                ffmpeg, "-ss", str(start), "-t", str(duration), "-i", str(video_file),
                "-af", f"bandpass=f={freq}:width_type=h:w=50,volumedetect",
                "-f", "null", "-"
            ]
            res = subprocess.run(cmd, capture_output=True, text=True)
            for line in res.stderr.splitlines():
                if "mean_volume:" in line:
                    try:
                        return float(line.split("mean_volume:")[1].split()[0])
                    except Exception:
                        pass
            return -99.0

        vol_a_1000 = extract_sample_volume(out_a, start=1.2, duration=0.4, freq=1000)
        vol_b_1000 = extract_sample_volume(out_b, start=1.2, duration=0.4, freq=1000)

        print(f"\n[PROBE 4 - Dialogue Cut Alignment at t=1.2s..1.6s]:")
        print(f"  Mode A (Boundary Smoothing) 1000Hz Volume at t=1.4s: {vol_a_1000:.1f} dB (Clean separation)")
        print(f"  Mode B (Acrossfade)         1000Hz Volume at t=1.4s: {vol_b_1000:.1f} dB (Audio leaked prematurely!)")

        assert vol_a_1000 < -35.0 or vol_a_1000 < vol_b_1000 - 10.0, (
            f"Mode A must prevent premature audio leak before cut! Vol A: {vol_a_1000}dB vs Vol B: {vol_b_1000}dB"
        )


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