// Include-based coverage for server video stream plumbing.
//
// Scope: include `server/src/video.rs` and exercise deterministic stream
// behavior plus malformed pipeline inputs.
// Targets: `server/src/video.rs`.
// Why: keep eye-stream setup and wrapper behavior stable without depending on
// camera hardware in CI.

#[allow(warnings)]
mod video_sinks {
    pub struct CameraRelay;
    pub struct HdmiSink;
    pub struct WebcamSink;
}

mod video_support {
    pub use lesavka_server::video_support::{
        adjust_effective_fps, contains_idr, default_eye_fps, env_u32, env_usize, should_send_frame,
    };
}

#[allow(warnings)]
mod video_include_contract {
    include!(env!("LESAVKA_SERVER_VIDEO_SRC"));

    use futures_util::StreamExt;
    use serial_test::serial;
    use temp_env::with_var;

    fn init_gst() {
        let _ = gst::init();
    }

    #[test]
    fn eye_profile_and_telemetry_helpers_are_stable() {
        assert_eq!(eye_source_profile(), (1920, 1080, 60));

        let metric = server_process_cpu_metric();
        metric.store(123, std::sync::atomic::Ordering::Relaxed);
        assert_eq!(metric.load(std::sync::atomic::Ordering::Relaxed), 123);

        let last_window = AtomicU64::new(5);
        let source_gap = AtomicU32::new(99);
        let send_gap = AtomicU32::new(88);
        let queue_peak = AtomicU32::new(77);

        reset_stream_telemetry_window(&last_window, 5, &source_gap, &send_gap, &queue_peak);
        assert_eq!(source_gap.load(Ordering::Relaxed), 99);
        assert_eq!(send_gap.load(Ordering::Relaxed), 88);
        assert_eq!(queue_peak.load(Ordering::Relaxed), 77);

        reset_stream_telemetry_window(&last_window, 6, &source_gap, &send_gap, &queue_peak);
        assert_eq!(source_gap.load(Ordering::Relaxed), 0);
        assert_eq!(send_gap.load(Ordering::Relaxed), 0);
        assert_eq!(queue_peak.load(Ordering::Relaxed), 0);
    }

    #[tokio::test]
    async fn video_stream_forwards_inner_packets() {
        init_gst();
        let (tx, rx) = tokio::sync::mpsc::channel(2);
        tx.send(Ok(VideoPacket {
            id: 1,
            pts: 42,
            data: vec![1, 2, 3, 4],
            ..Default::default()
        }))
        .await
        .expect("send packet");
        drop(tx);

        let mut stream = VideoStream {
            _pipeline: gst::Pipeline::new(),
            #[cfg(not(coverage))]
            _bus_watch: None,
            #[cfg(not(coverage))]
            _stall_watchdog_alive: None,
            inner: ReceiverStream::new(rx),
        };

        let first = stream
            .next()
            .await
            .expect("stream item")
            .expect("packet ok");
        assert_eq!(first.id, 1);
        assert_eq!(first.pts, 42);
        assert_eq!(first.data, vec![1, 2, 3, 4]);
        assert!(stream.next().await.is_none(), "stream should be exhausted");
    }

    #[test]
    fn video_stream_drop_is_safe_for_empty_pipeline() {
        init_gst();
        let (_tx, rx) = tokio::sync::mpsc::channel(1);
        let stream = VideoStream {
            _pipeline: gst::Pipeline::new(),
            #[cfg(not(coverage))]
            _bus_watch: None,
            #[cfg(not(coverage))]
            _stall_watchdog_alive: None,
            inner: ReceiverStream::new(rx),
        };
        drop(stream);
    }

    #[test]
    #[serial]
    fn eye_ball_returns_error_for_malformed_device_string() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        with_var("LESAVKA_EYE_ADAPTIVE", Some("0"), || {
            with_var("LESAVKA_EYE_QUEUE_BUFFERS", Some("4"), || {
                let result = rt.block_on(eye_ball("/dev/video0\" ! thiswillneverparse", 0, 4_000));
                assert!(result.is_err(), "malformed pipeline should fail parse");
            });
        });
    }

    #[test]
    #[serial]
    fn eye_ball_respects_env_paths_before_parse_failure() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        with_var("LESAVKA_EYE_APPSINK_BUFFERS", Some("5"), || {
            with_var("LESAVKA_EYE_CHAN_CAPACITY", Some("17"), || {
                let result = rt.block_on(eye_ball("/dev/video1\" ! still-bad", 1, 8_000));
                assert!(result.is_err(), "malformed pipeline should fail parse");
            });
        });
    }

    #[test]
    fn eye_ball_panics_for_out_of_range_eye_id() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        let panic_result = std::panic::catch_unwind(|| {
            let _ = rt.block_on(eye_ball("/dev/video0", 2, 1_000));
        });
        assert!(
            panic_result.is_err(),
            "invalid eye id must panic before setup"
        );
    }

    #[test]
    #[serial]
    fn eye_ball_attempts_runtime_setup_for_existing_non_camera_device() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        with_var("LESAVKA_EYE_ADAPTIVE", Some("1"), || {
            with_var("LESAVKA_EYE_QUEUE_BUFFERS", Some("3"), || {
                with_var("LESAVKA_EYE_APPSINK_BUFFERS", Some("3"), || {
                    let result = rt.block_on(async {
                        tokio::time::timeout(
                            std::time::Duration::from_millis(250),
                            eye_ball("/dev/null", 0, 6_000),
                        )
                        .await
                    });
                    match result {
                        Ok(Ok(stream)) => drop(stream),
                        Ok(Err(_)) | Err(_) => {}
                    }
                });
            });
        });
    }

    #[test]
    #[serial]
    fn eye_ball_second_eye_branch_runs_without_panicking() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        with_var("LESAVKA_EYE_ADAPTIVE", Some("0"), || {
            let result = rt.block_on(async {
                tokio::time::timeout(
                    std::time::Duration::from_millis(250),
                    eye_ball("/dev/null", 1, 2_000),
                )
                .await
            });
            match result {
                Ok(Ok(stream)) => drop(stream),
                Ok(Err(_)) | Err(_) => {}
            }
        });
    }

    #[test]
    #[serial]
    fn eye_ball_testsrc_path_produces_stream_packets() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        with_var("LESAVKA_EYE_ADAPTIVE", Some("1"), || {
            with_var("LESAVKA_EYE_QUEUE_BUFFERS", Some("8"), || {
                with_var("LESAVKA_EYE_APPSINK_BUFFERS", Some("8"), || {
                    with_var("LESAVKA_EYE_TESTSRC_KBIT", Some("1200"), || {
                        rt.block_on(async {
                            let setup = tokio::time::timeout(
                                std::time::Duration::from_secs(2),
                                eye_ball("testsrc", 0, 1_200),
                            )
                            .await;
                            let mut stream = match setup {
                                Ok(Ok(stream)) => stream,
                                Ok(Err(err)) => panic!("testsrc setup failed: {err:#}"),
                                Err(_) => panic!("testsrc setup timed out"),
                            };

                            let packet = tokio::time::timeout(
                                std::time::Duration::from_secs(2),
                                stream.next(),
                            )
                            .await
                            .expect("video packet timeout")
                            .expect("stream item")
                            .expect("packet");
                            assert!(packet.id <= 1);
                            assert!(!packet.data.is_empty());
                            drop(stream);
                        });
                    });
                });
            });
        });
    }

    #[test]
    #[serial]
    fn eye_ball_testsrc_backpressure_path_is_non_panicking() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        with_var("LESAVKA_EYE_ADAPTIVE", Some("1"), || {
            with_var("LESAVKA_EYE_CHAN_CAPACITY", Some("16"), || {
                rt.block_on(async {
                    let setup = tokio::time::timeout(
                        std::time::Duration::from_secs(2),
                        eye_ball("testsrc", 1, 1_800),
                    )
                    .await;
                    let mut stream = match setup {
                        Ok(Ok(stream)) => stream,
                        Ok(Err(err)) => panic!("testsrc setup failed: {err:#}"),
                        Err(_) => panic!("testsrc setup timed out"),
                    };
                    tokio::time::sleep(std::time::Duration::from_millis(300)).await;
                    let _ = tokio::time::timeout(std::time::Duration::from_secs(1), stream.next())
                        .await;
                });
            });
        });
    }

    #[test]
    #[serial]
    fn wait_for_eye_device_accepts_existing_character_device() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        with_var("LESAVKA_EYE_DEVICE_WAIT_MS", Some("50"), || {
            rt.block_on(async {
                wait_for_eye_device("/dev/null", "l")
                    .await
                    .expect("/dev/null should be ready");
            });
        });
    }

    #[test]
    #[serial]
    fn wait_for_eye_device_times_out_for_missing_path() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        let dir = tempfile::tempdir().expect("tempdir");
        let missing = dir.path().join("lesavka-eye-missing");
        with_var("LESAVKA_EYE_DEVICE_WAIT_MS", Some("50"), || {
            with_var("LESAVKA_EYE_DEVICE_POLL_MS", Some("25"), || {
                rt.block_on(async {
                    let err = wait_for_eye_device(missing.to_str().expect("utf8 path"), "r")
                        .await
                        .expect_err("missing eye device should time out");
                    let rendered = format!("{err:#}");
                    assert!(rendered.contains("was not ready within 50 ms"));
                });
            });
        });
    }

    #[test]
    #[serial]
    fn wait_for_eye_device_reports_non_character_paths() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        let dir = tempfile::tempdir().expect("tempdir");
        let regular_file = dir.path().join("not-a-video-device");
        std::fs::write(&regular_file, "not a device").expect("write marker file");
        with_var("LESAVKA_EYE_DEVICE_WAIT_MS", Some("50"), || {
            with_var("LESAVKA_EYE_DEVICE_POLL_MS", Some("25"), || {
                rt.block_on(async {
                    let err = wait_for_eye_device(regular_file.to_str().expect("utf8 path"), "l")
                        .await
                        .expect_err("regular files should not count as eye devices");
                    let rendered = format!("{err:#}");
                    assert!(rendered.contains("not a character device"));
                });
            });
        });
    }
}
