use super::{
    adaptive_gray_roi_mask, adaptive_rgb_roi_mask, dark_roi_factor, extract_audio_samples,
    extract_video_brightness, extract_video_colors, extract_video_timestamps, palette_match_score,
    retain_largest_connected_roi, run_command, summarize_frame_brightness, summarize_frame_color,
};
use crate::sync_probe::analyze::test_support::{
    audio_samples_to_bytes, frame_json, thumbnail_rgb_video_bytes, thumbnail_video_bytes,
    with_fake_media_tools,
};
use std::process::Command;

#[test]
/// Keeps `extract_video_timestamps_reads_fake_ffprobe_output` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
fn extract_video_timestamps_reads_fake_ffprobe_output() {
    let timestamps = vec![0.0, 0.5, 1.0];
    with_fake_media_tools(
        &frame_json(&timestamps),
        &[1, 2, 3],
        &[1, 0],
        |capture_path| {
            let parsed = extract_video_timestamps(capture_path).expect("video timestamps");
            assert_eq!(parsed, timestamps);
        },
    );
}

#[test]
/// Keeps `extract_video_timestamps_rejects_empty_and_invalid_outputs` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
fn extract_video_timestamps_rejects_empty_and_invalid_outputs() {
    with_fake_media_tools(br#"{"frames":[]}"#, &[1], &[1, 0], |capture_path| {
        let error = extract_video_timestamps(capture_path).expect_err("empty frames fail");
        assert!(
            error
                .to_string()
                .contains("did not return any video frame timestamps")
        );
    });

    with_fake_media_tools(
        br#"{"frames":[{"best_effort_timestamp_time":"bad"}]}"#,
        &[1],
        &[1, 0],
        |capture_path| {
            let error =
                extract_video_timestamps(capture_path).expect_err("invalid timestamp fails");
            assert!(error.to_string().contains("parsing frame timestamp"));
        },
    );
}

#[test]
/// Keeps `extract_video_brightness_reads_fake_ffmpeg_output` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
fn extract_video_brightness_reads_fake_ffmpeg_output() {
    let brightness = vec![5u8, 100, 250];
    with_fake_media_tools(
        br#"{"frames":[{"best_effort_timestamp_time":"0.0"}]}"#,
        &thumbnail_video_bytes(&brightness),
        &[1, 0],
        |capture_path| {
            let parsed = extract_video_brightness(capture_path).expect("video brightness");
            assert_eq!(parsed, brightness);
        },
    );
}

#[test]
/// Keeps `extract_video_brightness_rejects_empty_output` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
fn extract_video_brightness_rejects_empty_output() {
    with_fake_media_tools(
        br#"{"frames":[{"best_effort_timestamp_time":"0.0"}]}"#,
        &[],
        &[1, 0],
        |capture_path| {
            let error = extract_video_brightness(capture_path).expect_err("empty brightness");
            assert!(
                error
                    .to_string()
                    .contains("did not emit any video brightness data")
            );
        },
    );
}

#[test]
/// Keeps `extract_video_brightness_uses_full_frame_thumbnail_average` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
fn extract_video_brightness_uses_full_frame_thumbnail_average() {
    let brightness = vec![20u8, 45, 20];
    with_fake_media_tools(
        &frame_json(&[0.0, 0.1, 0.2]),
        &thumbnail_video_bytes(&brightness),
        &[1, 0],
        |capture_path| {
            let parsed = extract_video_brightness(capture_path).expect("video brightness");
            assert_eq!(parsed, brightness);
        },
    );
}

#[test]
fn extract_video_brightness_rejects_truncated_frame_data() {
    with_fake_media_tools(&frame_json(&[0.0]), &[1, 2, 3], &[1, 0], |capture_path| {
        let error = extract_video_brightness(capture_path).expect_err("truncated frame bytes");
        assert!(error.to_string().contains("not divisible"));
    });
}

#[test]
/// Keeps `extract_video_colors_reads_fake_ffmpeg_output` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
fn extract_video_colors_reads_fake_ffmpeg_output() {
    let colors = vec![(255, 45, 45), (0, 230, 118), (41, 121, 255)];
    with_fake_media_tools(
        &frame_json(&[0.0, 0.1, 0.2]),
        &thumbnail_rgb_video_bytes(&colors),
        &[1, 0],
        |capture_path| {
            let parsed = extract_video_colors(capture_path).expect("video colors");
            assert_eq!(parsed[0].r, 255);
            assert_eq!(parsed[1].g, 230);
            assert_eq!(parsed[2].b, 255);
        },
    );
}

#[test]
fn extract_video_colors_rejects_empty_and_truncated_frame_data() {
    with_fake_media_tools(
        br#"{"frames":[{"best_effort_timestamp_time":"0.0"}]}"#,
        &[],
        &[1, 0],
        |capture_path| {
            let error = extract_video_colors(capture_path).expect_err("empty colors");
            assert!(
                error
                    .to_string()
                    .contains("did not emit any video color data")
            );
        },
    );

    with_fake_media_tools(&frame_json(&[0.0]), &[1, 2, 3], &[1, 0], |capture_path| {
        let error = extract_video_colors(capture_path).expect_err("truncated color bytes");
        assert!(error.to_string().contains("not divisible"));
    });
}

#[test]
/// Keeps `extract_video_colors_tracks_small_flashing_screen_region` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
fn extract_video_colors_tracks_small_flashing_screen_region() {
    const SIDE: usize = 64;
    let mut bytes = Vec::new();
    for color in [(24, 28, 32), (255, 45, 45), (24, 28, 32), (0, 230, 118)] {
        let mut frame = vec![34u8; SIDE * SIDE * 3];
        for y in 6..18 {
            for x in 40..54 {
                let offset = (y * SIDE + x) * 3;
                frame[offset] = color.0;
                frame[offset + 1] = color.1;
                frame[offset + 2] = color.2;
            }
        }
        bytes.extend_from_slice(&frame);
    }

    with_fake_media_tools(
        &frame_json(&[0.0, 0.1, 0.2, 0.3]),
        &bytes,
        &[1, 0],
        |capture_path| {
            let parsed = extract_video_colors(capture_path).expect("video colors");
            assert!(
                parsed[1].r > 220 && parsed[1].g < 80,
                "red pulse should dominate selected ROI: {:?}",
                parsed[1]
            );
            assert!(
                parsed[3].g > 190 && parsed[3].r < 60,
                "green pulse should dominate selected ROI: {:?}",
                parsed[3]
            );
        },
    );
}

#[test]
/// Keeps `extract_audio_samples_reads_fake_ffmpeg_output` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
fn extract_audio_samples_reads_fake_ffmpeg_output() {
    let samples = vec![1i16, -2, 32_000];
    with_fake_media_tools(
        br#"{"frames":[{"best_effort_timestamp_time":"0.0"}]}"#,
        &[1],
        &audio_samples_to_bytes(&samples),
        |capture_path| {
            let parsed = extract_audio_samples(capture_path).expect("audio samples");
            assert_eq!(parsed, samples);
        },
    );
}

#[test]
/// Keeps `extract_audio_samples_rejects_too_short_output` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
fn extract_audio_samples_rejects_too_short_output() {
    with_fake_media_tools(
        br#"{"frames":[{"best_effort_timestamp_time":"0.0"}]}"#,
        &[1],
        &[7],
        |capture_path| {
            let error = extract_audio_samples(capture_path).expect_err("short audio");
            assert!(
                error
                    .to_string()
                    .contains("did not emit enough audio data to analyze")
            );
        },
    );
}

#[test]
/// Keeps `run_command_reports_success_and_failure` explicit because it sits on sync-probe analysis, where small timestamp or pairing mistakes can hide real A/V skew.
/// Inputs are the typed parameters; output is the return value or side effect.
fn run_command_reports_success_and_failure() {
    let output = run_command(
        Command::new("sh").arg("-c").arg("printf 'ok'"),
        "success command",
    )
    .expect("success output");
    assert_eq!(output, b"ok");

    let error = run_command(
        Command::new("sh")
            .arg("-c")
            .arg("printf 'boom' >&2; exit 7"),
        "failing command",
    )
    .expect_err("failing command should error");
    assert!(error.to_string().contains("failing command failed: boom"));
}

#[test]
/// Verifies adaptive ROI helpers have explicit fallback behavior.
///
/// Inputs: tiny masks and frames that cannot produce a stable ROI plus one
/// connected flashing region. Outputs: helper-level assertions. Why: analyzer
/// robustness depends on falling back to whole-frame summaries when the RCT
/// capture has too little color/brightness evidence for a reliable mask.
fn adaptive_roi_helpers_cover_fallbacks_and_connected_region_retention() {
    assert!(adaptive_gray_roi_mask(&[], 4).is_none());
    assert!(adaptive_rgb_roi_mask(&[], 4).is_none());
    assert!(adaptive_gray_roi_mask(&[&[1, 2, 3, 4]], 4).is_none());
    assert!(adaptive_rgb_roi_mask(&[&[1, 2, 3, 4, 5, 6]], 2).is_none());

    assert_eq!(
        summarize_frame_brightness(&[10, 30], Some(&[false, false])),
        20
    );

    let color = summarize_frame_color(&[10, 20, 30, 40, 50, 60], Some(&[false, false]));
    assert_eq!((color.r, color.g, color.b), (25, 35, 45));

    assert_eq!(dark_roi_factor(130), 0.25);
    assert_eq!(dark_roi_factor(200), 0.10);
    assert_eq!(palette_match_score(10, 10, 10), 0.0);
    assert!(palette_match_score(255, 45, 45) > 0.95);

    let non_square = vec![true, false, true];
    assert_eq!(retain_largest_connected_roi(non_square.clone()), non_square);

    let mut mask = vec![false; 36];
    for selected in mask.iter_mut().take(20) {
        *selected = true;
    }
    mask[35] = true;
    let retained = retain_largest_connected_roi(mask);
    assert_eq!(retained.iter().filter(|selected| **selected).count(), 20);
    assert!(!retained[35]);
}
