use super::*;
use serial_test::serial;

#[test]
fn source_profile_stays_pass_through_without_explicit_reencode_request() {
    let request = normalize_eye_capture_request(1920, 1080, 30, 12_000);

    assert_eq!(request.width, 1920);
    assert_eq!(request.height, 1080);
    assert_eq!(request.fps, 60);
}

#[test]
fn source_mode_selection_prefers_native_modes_without_reencode() {
    let bitrate_request = normalize_eye_capture_request(1920, 1080, 30, 2_500);
    let fps_request = normalize_eye_capture_request(1920, 1080, 24, 12_000);
    let smaller_mode_request = normalize_eye_capture_request(1600, 900, 30, 12_000);

    assert_eq!(bitrate_request.fps, 60);
    assert_eq!(fps_request.fps, 60);
    assert_eq!(smaller_mode_request.width, 1280);
    assert_eq!(smaller_mode_request.height, 720);
    assert_eq!(smaller_mode_request.fps, 60);
}

#[test]
fn eye_stall_watchdog_is_configurable_without_disabling_first_frame_errors() {
    temp_env::with_var("LESAVKA_EYE_STALL_WARN_MS", None::<&str>, || {
        assert_eq!(
            eye_stall_warn_timeout(),
            Some(std::time::Duration::from_millis(5_000))
        );
    });
    temp_env::with_var("LESAVKA_EYE_STALL_WARN_MS", Some("0"), || {
        assert_eq!(eye_stall_warn_timeout(), None);
    });
    temp_env::with_var("LESAVKA_EYE_STALL_WARN_MS", Some("1"), || {
        assert_eq!(
            eye_stall_warn_timeout(),
            Some(std::time::Duration::from_millis(500))
        );
    });
}

#[test]
fn downstream_wall_clock_ms_is_monotonic_enough_for_stall_diagnostics() {
    let first = wall_clock_ms();
    std::thread::sleep(std::time::Duration::from_millis(1));
    assert!(wall_clock_ms() >= first);
}

fn marker_frame(width: i32, height: i32) -> Vec<u8> {
    let mut rgba = vec![0_u8; (width * height * 4) as usize];
    let marker = 96;
    for y in 0..height {
        for x in 0..width {
            let idx = ((y * width + x) * 4) as usize;
            let (r, g, b) = if x < marker && y < marker {
                (255, 0, 0)
            } else if x >= width - marker && y < marker {
                (0, 255, 0)
            } else if x < marker && y >= height - marker {
                (0, 0, 255)
            } else if x >= width - marker && y >= height - marker {
                (255, 255, 0)
            } else {
                (24, 24, 24)
            };
            rgba[idx..idx + 4].copy_from_slice(&[r, g, b, 255]);
        }
    }
    rgba
}

fn pull_reencoded_frame_rgba(
    width: i32,
    height: i32,
    input_fps: u32,
    output_fps: u32,
) -> anyhow::Result<(i32, i32, Vec<u8>)> {
    gst::init().context("gst init")?;
    let desc = format!(
        "appsrc name=src is-live=false format=time block=true ! \
         videoconvert ! video/x-raw,format=I420,width={width},height={height},framerate={input_fps}/1,pixel-aspect-ratio=1/1 ! \
         x264enc tune=zerolatency speed-preset=veryfast bitrate=12000 key-int-max={input_fps} option-string=sar=1/1 ! \
         h264parse disable-passthrough=true config-interval=-1 ! \
         avdec_h264 ! videoconvert ! videoscale add-borders=false ! videorate ! \
         video/x-raw,format=I420,width={width},height={height},framerate={output_fps}/1,pixel-aspect-ratio=1/1 ! \
         x264enc tune=zerolatency speed-preset=faster bitrate=12000 key-int-max=5 option-string=sar=1/1 ! \
         h264parse disable-passthrough=true config-interval=-1 ! \
         avdec_h264 ! videoconvert ! video/x-raw,format=RGBA,pixel-aspect-ratio=1/1 ! \
         appsink name=sink emit-signals=false sync=false max-buffers=1 drop=true"
    );
    let pipeline = gst::parse::launch(&desc)?
        .downcast::<gst::Pipeline>()
        .expect("pipeline");
    let appsrc = pipeline
        .by_name("src")
        .expect("appsrc")
        .downcast::<gst_app::AppSrc>()
        .expect("appsrc cast");
    appsrc.set_caps(Some(
        &gst::Caps::builder("video/x-raw")
            .field("format", "RGBA")
            .field("width", width)
            .field("height", height)
            .field("framerate", gst::Fraction::new(input_fps as i32, 1))
            .field("pixel-aspect-ratio", gst::Fraction::new(1, 1))
            .build(),
    ));
    appsrc.set_format(gst::Format::Time);
    let appsink = pipeline
        .by_name("sink")
        .expect("appsink")
        .downcast::<gst_app::AppSink>()
        .expect("appsink cast");
    appsink.set_caps(Some(
        &gst::Caps::builder("video/x-raw")
            .field("format", "RGBA")
            .field("pixel-aspect-ratio", gst::Fraction::new(1, 1))
            .build(),
    ));

    pipeline
        .set_state(gst::State::Playing)
        .context("starting reencode probe pipeline")?;

    let mut buffer = gst::Buffer::from_mut_slice(marker_frame(width, height));
    if let Some(buf) = buffer.get_mut() {
        buf.set_pts(Some(gst::ClockTime::ZERO));
        buf.set_duration(Some(gst::ClockTime::from_nseconds(
            1_000_000_000_u64 / input_fps.max(1) as u64,
        )));
    }
    appsrc
        .push_buffer(buffer)
        .map_err(|err| anyhow::anyhow!("push buffer failed: {err:?}"))?;
    appsrc
        .end_of_stream()
        .map_err(|err| anyhow::anyhow!("eos failed: {err:?}"))?;

    // ARM CI workers can take a while to complete the full 1080p double
    // encode/decode probe; the assertion below still verifies the frame.
    let sample = appsink
        .try_pull_sample(gst::ClockTime::from_seconds(30))
        .ok_or_else(|| anyhow::anyhow!("timed out pulling reencoded frame"))?;
    let caps = sample
        .caps()
        .ok_or_else(|| anyhow::anyhow!("missing sample caps"))?;
    let structure = caps
        .structure(0)
        .ok_or_else(|| anyhow::anyhow!("missing caps structure"))?;
    let out_width = structure
        .get::<i32>("width")
        .map_err(|err| anyhow::anyhow!("missing output width: {err}"))?;
    let out_height = structure
        .get::<i32>("height")
        .map_err(|err| anyhow::anyhow!("missing output height: {err}"))?;
    let buffer = sample
        .buffer()
        .ok_or_else(|| anyhow::anyhow!("missing sample buffer"))?;
    let map = buffer
        .map_readable()
        .map_err(|_| anyhow::anyhow!("sample map failed"))?;
    let rgba = map.as_slice().to_vec();
    let _ = pipeline.set_state(gst::State::Null);
    Ok((out_width, out_height, rgba))
}

fn rgba_pixel(rgba: &[u8], width: i32, x: i32, y: i32) -> (u8, u8, u8) {
    let idx = ((y * width + x) * 4) as usize;
    (rgba[idx], rgba[idx + 1], rgba[idx + 2])
}

#[test]
#[serial]
fn reencode_probe_preserves_corner_markers_on_full_frame_content() {
    let (width, height, rgba) = pull_reencoded_frame_rgba(1920, 1080, 60, 30).expect("probe frame");
    assert_eq!((width, height), (1920, 1080));

    let top_left = rgba_pixel(&rgba, width, 24, 24);
    let top_right = rgba_pixel(&rgba, width, width - 25, 24);
    let bottom_left = rgba_pixel(&rgba, width, 24, height - 25);
    let bottom_right = rgba_pixel(&rgba, width, width - 25, height - 25);

    assert!(
        top_left.0 > 180 && top_left.1 < 80 && top_left.2 < 80,
        "top-left marker drifted: {top_left:?}"
    );
    assert!(
        top_right.1 > 180 && top_right.0 < 80 && top_right.2 < 80,
        "top-right marker drifted: {top_right:?}"
    );
    assert!(
        bottom_left.2 > 180 && bottom_left.0 < 80 && bottom_left.1 < 80,
        "bottom-left marker drifted: {bottom_left:?}"
    );
    assert!(
        bottom_right.0 > 180 && bottom_right.1 > 180 && bottom_right.2 < 120,
        "bottom-right marker drifted: {bottom_right:?}"
    );
}
