// Downstream eye-video feed contracts.
//
// Scope: exercise the server eye stream with a deterministic GStreamer test
// source and lock down the native source-mode policy used by the launcher and
// diagnostics.
// Targets: `server/src/video.rs`, `server/src/video_support.rs`,
// `common/src/eye_source.rs`.
// Why: the live capture cards are hardware-dependent, but this catches the
// packet, timing, keyframe, and mode-selection regressions that break the
// client previews before hardware enters the loop.

use futures_util::StreamExt;
use lesavka_common::eye_source::{
    EyeSourceMode, default_eye_source_mode, display_size_for_source_mode,
    eye_source_mode_for_request, native_eye_source_modes,
};
use lesavka_server::{video, video_support::contains_idr};
use serial_test::serial;
use std::time::Duration;

const SERVER_VIDEO_STREAM_CORE: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/server/src/video/stream_core.rs"
));
const SERVER_VIDEO_EYE_CAPTURE: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/server/src/video/eye_capture.rs"
));

#[test]
fn native_downstream_eye_modes_stay_widescreen_and_square_pixel() {
    assert_eq!(
        native_eye_source_modes(),
        &[
            EyeSourceMode {
                width: 1920,
                height: 1080,
                fps: 60,
            },
            EyeSourceMode {
                width: 1280,
                height: 720,
                fps: 60,
            },
        ]
    );
    assert_eq!(
        default_eye_source_mode(),
        EyeSourceMode {
            width: 1920,
            height: 1080,
            fps: 60,
        }
    );
    for mode in native_eye_source_modes() {
        assert_eq!(
            display_size_for_source_mode(*mode),
            (mode.width, mode.height)
        );
    }
    assert_eq!(
        display_size_for_source_mode(EyeSourceMode {
            width: 720,
            height: 576,
            fps: 50,
        }),
        (1024, 576)
    );
    assert_eq!(
        display_size_for_source_mode(EyeSourceMode {
            width: 720,
            height: 480,
            fps: 60,
        }),
        (854, 480)
    );
}

#[test]
fn downstream_eye_request_chooses_best_native_capture_mode() {
    assert_eq!(
        eye_source_mode_for_request(0, 0),
        EyeSourceMode {
            width: 1920,
            height: 1080,
            fps: 60,
        }
    );
    assert_eq!(
        eye_source_mode_for_request(1920, 1080),
        EyeSourceMode {
            width: 1920,
            height: 1080,
            fps: 60,
        }
    );
    assert_eq!(
        eye_source_mode_for_request(1600, 900),
        EyeSourceMode {
            width: 1280,
            height: 720,
            fps: 60,
        }
    );
    assert_eq!(
        eye_source_mode_for_request(960, 540),
        EyeSourceMode {
            width: 1280,
            height: 720,
            fps: 60,
        }
    );
}

#[test]
fn downstream_eye_stream_logs_midstream_sample_stalls_without_killing_recovery() {
    for expected in [
        "LESAVKA_EYE_FIRST_FRAME_TIMEOUT_MS",
        "LESAVKA_EYE_STALL_WARN_MS",
        "eye_stall_warn_timeout",
        "last_sample_wall_ms",
        "wall_clock_ms",
        "downstream eye stream has produced no samples since the last frame",
        "stall_watchdog_alive",
        "_stall_watchdog_alive",
    ] {
        assert!(
            SERVER_VIDEO_STREAM_CORE.contains(expected)
                || SERVER_VIDEO_EYE_CAPTURE.contains(expected),
            "downstream black-screen diagnostics should contain marker {expected}"
        );
    }
    assert!(
        SERVER_VIDEO_EYE_CAPTURE
            .find("first_frame_timeout_ms")
            .unwrap()
            < SERVER_VIDEO_EYE_CAPTURE
                .find("eye_stall_warn_timeout()")
                .unwrap(),
        "first-frame failures should stay separate from midstream stall diagnostics"
    );
}

#[test]
#[serial]
fn testsrc_downstream_feed_emits_h264_for_both_eyes() {
    with_fast_video_env(|| {
        let runtime = tokio::runtime::Runtime::new().expect("runtime");
        runtime.block_on(async {
            assert_testsrc_downstream_eye(0, 1920, 1080, 60).await;
            assert_testsrc_downstream_eye(1, 1280, 720, 60).await;
        });
    });
}

async fn assert_testsrc_downstream_eye(eye_id: u32, width: u32, height: u32, fps: u32) {
    let setup = tokio::time::timeout(
        Duration::from_secs(2),
        video::eye_ball_with_request("testsrc", eye_id, 1_200, width, height, fps),
    )
    .await;
    let mut stream = match setup {
        Ok(Ok(stream)) => stream,
        Ok(Err(err)) => panic!("testsrc eye-{eye_id} setup failed: {err:#}"),
        Err(_) => panic!("testsrc eye-{eye_id} setup timed out"),
    };

    let mut last_pts = None;
    let mut saw_annex_b = false;
    let mut saw_idr = false;
    for packet_index in 0..8 {
        let packet = tokio::time::timeout(Duration::from_secs(2), stream.next())
            .await
            .unwrap_or_else(|_| panic!("eye-{eye_id} packet {packet_index} timed out"))
            .expect("stream should stay open")
            .unwrap_or_else(|err| panic!("eye-{eye_id} packet {packet_index} failed: {err}"));

        assert_eq!(packet.id, eye_id, "logical eye id must be preserved");
        assert!(
            !packet.data.is_empty(),
            "eye-{eye_id} packet {packet_index} must carry H.264 bytes"
        );
        if let Some(previous) = last_pts {
            assert!(
                packet.pts >= previous,
                "eye-{eye_id} PTS must not move backward"
            );
        }
        last_pts = Some(packet.pts);
        saw_annex_b |= contains_annex_b_start_code(&packet.data);
        saw_idr |= contains_idr(&packet.data);
    }

    assert!(saw_annex_b, "eye-{eye_id} should emit Annex-B H.264");
    assert!(
        saw_idr,
        "eye-{eye_id} should emit an IDR keyframe for decoder recovery"
    );
    drop(stream);
}

fn with_fast_video_env(run: impl FnOnce()) {
    temp_env::with_vars(
        [
            ("LESAVKA_EYE_ADAPTIVE", Some("0")),
            ("LESAVKA_EYE_QUEUE_BUFFERS", Some("8")),
            ("LESAVKA_EYE_APPSINK_BUFFERS", Some("8")),
            ("LESAVKA_EYE_CHAN_CAPACITY", Some("32")),
            ("LESAVKA_EYE_TESTSRC_KBIT", Some("1200")),
            ("LESAVKA_EYE_KEYFRAME_INTERVAL", Some("1")),
        ],
        run,
    );
}

fn contains_annex_b_start_code(bytes: &[u8]) -> bool {
    bytes.windows(3).any(|window| window == [0, 0, 1])
        || bytes.windows(4).any(|window| window == [0, 0, 0, 1])
}
