// Include-based coverage for client video output window plumbing.
//
// Scope: include `client/src/output/video.rs` with deterministic display/layout
// stubs and exercise backend/placement branches without a real desktop session.
// Targets: `client/src/output/video.rs`.
// Why: monitor window orchestration contains branch-heavy environment logic that
// should remain stable in CI.

mod output {
    pub mod display {
        #[derive(Clone, Copy, Debug)]
        pub struct MonitorInfo {
            pub x: i32,
            pub y: i32,
            pub w: i32,
            pub h: i32,
        }

        pub fn enumerate_monitors() -> Vec<MonitorInfo> {
            vec![
                MonitorInfo {
                    x: 0,
                    y: 0,
                    w: 1920,
                    h: 1080,
                },
                MonitorInfo {
                    x: 1920,
                    y: 0,
                    w: 1920,
                    h: 1080,
                },
            ]
        }
    }

    pub mod layout {
        #[derive(Clone, Copy, Debug)]
        pub struct Rect {
            pub x: i32,
            pub y: i32,
            pub w: i32,
            pub h: i32,
        }

        pub fn assign_rectangles(
            monitors: &[super::display::MonitorInfo],
            streams: &[(&str, i32, i32)],
        ) -> Vec<Rect> {
            streams
                .iter()
                .enumerate()
                .map(|(idx, _)| {
                    let mon = monitors.get(idx).unwrap_or(&monitors[0]);
                    Rect {
                        x: mon.x,
                        y: mon.y,
                        w: mon.w,
                        h: mon.h,
                    }
                })
                .collect()
        }
    }
}

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

    use serial_test::serial;
    use std::fs;
    use std::os::unix::fs::PermissionsExt;
    use std::path::Path;
    use temp_env::with_var;
    use tempfile::tempdir;

    fn write_executable(dir: &Path, name: &str, body: &str) {
        let path = dir.join(name);
        fs::write(&path, body).expect("write script");
        let mut perms = fs::metadata(&path).expect("metadata").permissions();
        perms.set_mode(0o755);
        fs::set_permissions(path, perms).expect("chmod");
    }

    fn with_fake_bin(name: &str, script_body: &str, f: impl FnOnce()) {
        let dir = tempdir().expect("tempdir");
        write_executable(dir.path(), name, script_body);
        let prior = std::env::var("PATH").unwrap_or_default();
        let merged = if prior.is_empty() {
            dir.path().display().to_string()
        } else {
            format!("{}:{prior}", dir.path().display())
        };
        with_var("PATH", Some(merged), f);
    }

    #[test]
    #[serial]
    fn h264_decoder_selection_honors_env_and_fallbacks() {
        gst::init().expect("initialize gstreamer");
        with_var("LESAVKA_ALLOW_SOFTWARE_VIDEO", Some("1"), || {
            with_var("LESAVKA_H264_DECODER", Some("decodebin"), || {
                assert_eq!(pick_h264_decoder().unwrap(), "decodebin");
            });
            with_var("LESAVKA_H264_DECODER", Some("fakesink"), || {
                assert_eq!(pick_h264_decoder().unwrap(), "fakesink");
            });
        });
        with_var("LESAVKA_ALLOW_SOFTWARE_VIDEO", None::<&str>, || {
            with_var("LESAVKA_H264_DECODER", Some("decodebin"), || {
                assert!(pick_h264_decoder().is_err());
            });
            with_var("LESAVKA_H264_DECODER", Some("fakesink"), || {
                assert!(pick_h264_decoder().is_err());
            });
        });
        with_var(
            "LESAVKA_H264_DECODER",
            Some("definitely-not-a-decoder"),
            || {
                assert!(pick_h264_decoder().is_err());
            },
        );
        with_var("LESAVKA_H264_DECODER", Some("   "), || {
            let result = pick_h264_decoder();
            assert!(result.is_ok() || result.unwrap_err().to_string().contains("H.264 decoder"));
        });
        with_var("LESAVKA_H264_DECODER", None::<&str>, || {
            let result = pick_h264_decoder();
            assert!(result.is_ok() || result.unwrap_err().to_string().contains("H.264 decoder"));
        });
        #[cfg(coverage)]
        with_var("LESAVKA_TEST_DISABLE_H264_DECODERS", Some("1"), || {
            with_var("LESAVKA_H264_DECODER", None::<&str>, || {
                assert!(pick_h264_decoder().is_err());
            });
        });
        assert!(buildable_decoder("fakesink"));
        assert!(!buildable_decoder("definitely-not-a-real-gst-element"));
    }

    #[test]
    #[serial]
    fn h264_decoder_selection_requires_hardware_unless_lab_fallback_is_explicit() {
        with_var("LESAVKA_H264_DECODER_PREFERENCE", None::<&str>, || {
            with_var("LESAVKA_ALLOW_SOFTWARE_VIDEO", None::<&str>, || {
                with_var("LESAVKA_ALLOW_VULKAN_H264_DECODER", None::<&str>, || {
                    let order = h264_decoder_preference_order();
                    assert_eq!(order.first(), Some(&"nvh264dec"));
                    assert!(order.contains(&"nvh264sldec"));
                    assert!(order.contains(&"vah264dec"));
                    assert!(order.contains(&"vaapih264dec"));
                    assert!(order.contains(&"v4l2h264dec"));
                    assert!(order.contains(&"v4l2slh264dec"));
                    assert!(!order.contains(&"avdec_h264"));
                    assert!(!order.contains(&"openh264dec"));
                    assert!(!order.contains(&"vulkanh264dec"));
                });
            });
        });

        with_var("LESAVKA_ALLOW_VULKAN_H264_DECODER", Some("1"), || {
            with_var("LESAVKA_H264_DECODER_PREFERENCE", None::<&str>, || {
                let order = h264_decoder_preference_order();
                assert!(order.contains(&"vulkanh264dec"));
                assert!(
                    !order.contains(&"avdec_h264") && !order.contains(&"openh264dec"),
                    "Vulkan diagnostics should not imply software fallback"
                );
            });
        });

        with_var("LESAVKA_ALLOW_SOFTWARE_VIDEO", None::<&str>, || {
            with_var("LESAVKA_H264_DECODER_PREFERENCE", None::<&str>, || {
                let order = h264_decoder_preference_order();
                assert!(!order.contains(&"avdec_h264"));
                assert!(!order.contains(&"openh264dec"));
            });
        });

        with_var("LESAVKA_ALLOW_SOFTWARE_VIDEO", Some("1"), || {
            with_var("LESAVKA_H264_DECODER_PREFERENCE", Some("cpu"), || {
                let order = h264_decoder_preference_order();
                assert_eq!(order.first(), Some(&"avdec_h264"));
                assert!(
                    order
                        .iter()
                        .position(|name| *name == "openh264dec")
                        .unwrap()
                        < order.iter().position(|name| *name == "nvh264dec").unwrap()
                );
            });
        });
    }

    #[test]
    fn vulkan_decoder_fragment_avoids_decoder_names_and_downloads_frames() {
        let fragment = h264_decoder_launch_fragment("vulkanh264dec");
        assert!(fragment.starts_with("vulkanh264dec "));
        assert!(fragment.contains("discard-corrupted-frames=true"));
        assert!(fragment.contains("automatic-request-sync-points=true"));
        assert!(fragment.contains("vulkandownload"));
        assert!(
            !fragment.contains(" name="),
            "decoder fragments should avoid name= for plugin wrappers that reject it"
        );

        let named = h264_decoder_launch_fragment_named("vulkanh264dec", "decoder1");
        assert!(named.starts_with("vulkanh264dec "));
        assert!(named.contains("vulkandownload"));
        assert_eq!(
            h264_decoder_launch_fragment_named("nvh264dec", "decoder1"),
            "nvh264dec"
        );
    }

    #[test]
    #[serial]
    fn monitor_window_new_covers_x11_backend_path() {
        with_var("GDK_BACKEND", Some("x11"), || {
            with_var("DISPLAY", Some(":99"), || {
                with_var("WAYLAND_DISPLAY", None::<&str>, || {
                    let result = MonitorWindow::new(0);
                    if let Ok(window) = result {
                        window.push_packet(VideoPacket {
                            id: 0,
                            pts: 5,
                            data: vec![0, 0, 0, 1, 0x67],
                            ..Default::default()
                        });
                    }
                });
            });
        });
    }

    #[test]
    #[serial]
    fn monitor_window_new_covers_wayland_swaymsg_placement_branch() {
        let swaymsg = r#"#!/usr/bin/env sh
if [ "$1" = "-t" ] && [ "$2" = "get_tree" ]; then
  echo '{}'
  exit 0
fi
exit 0
"#;
        with_fake_bin("swaymsg", swaymsg, || {
            with_var("WAYLAND_DISPLAY", Some("wayland-0"), || {
                with_var("DISPLAY", None::<&str>, || {
                    with_var("GDK_BACKEND", None::<&str>, || {
                        let _ = MonitorWindow::new(1);
                    });
                });
            });
        });
    }

    #[test]
    #[serial]
    fn monitor_window_new_covers_wayland_hyprctl_fallback_branch() {
        let hyprctl = r#"#!/usr/bin/env sh
if [ "$1" = "version" ]; then
  echo 'Hyprland test'
  exit 0
fi
if [ "$1" = "dispatch" ]; then
  exit 0
fi
exit 0
"#;
        with_fake_bin("hyprctl", hyprctl, || {
            with_var("WAYLAND_DISPLAY", Some("wayland-0"), || {
                with_var("DISPLAY", None::<&str>, || {
                    with_var("GDK_BACKEND", None::<&str>, || {
                        let _ = MonitorWindow::new(0);
                    });
                });
            });
        });
    }

    #[test]
    #[serial]
    fn monitor_window_new_covers_display_wmctrl_branch() {
        let wmctrl = r#"#!/usr/bin/env sh
exit 0
"#;
        with_fake_bin("wmctrl", wmctrl, || {
            with_var("WAYLAND_DISPLAY", None::<&str>, || {
                with_var("DISPLAY", Some(":99"), || {
                    with_var("GDK_BACKEND", None::<&str>, || {
                        let _ = MonitorWindow::new(0);
                    });
                });
            });
        });
    }

    #[test]
    fn push_packet_sets_pts_on_appsrc_buffers() {
        gst::init().ok();
        let pipeline = gst::Pipeline::new();
        let src = gst::ElementFactory::make("appsrc")
            .build()
            .expect("appsrc")
            .downcast::<gst_app::AppSrc>()
            .expect("downcast appsrc");
        pipeline
            .add(src.upcast_ref::<gst::Element>())
            .expect("add appsrc");

        let window = MonitorWindow {
            _pipeline: pipeline,
            src,
        };

        window.push_packet(VideoPacket {
            id: 1,
            pts: 12_345,
            data: vec![0, 0, 0, 1, 0x65],
            ..Default::default()
        });
    }

    #[test]
    fn drop_is_safe_for_manually_built_window() {
        gst::init().ok();
        let pipeline = gst::Pipeline::new();
        let src = gst::ElementFactory::make("appsrc")
            .build()
            .expect("appsrc")
            .downcast::<gst_app::AppSrc>()
            .expect("downcast appsrc");
        pipeline
            .add(src.upcast_ref::<gst::Element>())
            .expect("add appsrc");

        let window = MonitorWindow {
            _pipeline: pipeline,
            src,
        };
        drop(window);
    }

    #[test]
    fn unified_monitor_window_constructor_and_push_are_stable() {
        match UnifiedMonitorWindow::new() {
            Ok(window) => {
                window.push_packet(VideoPacket {
                    id: 0,
                    pts: 100,
                    data: vec![0, 0, 0, 1, 0x65],
                    ..Default::default()
                });
                window.push_packet(VideoPacket {
                    id: 1,
                    pts: 101,
                    data: vec![0, 0, 0, 1, 0x67],
                    ..Default::default()
                });
            }
            Err(err) => {
                assert!(
                    !err.to_string().trim().is_empty(),
                    "unified constructor returned an empty error"
                );
            }
        }
    }

    #[test]
    fn unified_drop_is_safe_for_manually_built_window() {
        gst::init().ok();
        let pipeline = gst::Pipeline::new();
        let left_src = gst::ElementFactory::make("appsrc")
            .build()
            .expect("left appsrc")
            .downcast::<gst_app::AppSrc>()
            .expect("downcast left appsrc");
        let right_src = gst::ElementFactory::make("appsrc")
            .build()
            .expect("right appsrc")
            .downcast::<gst_app::AppSrc>()
            .expect("downcast right appsrc");
        pipeline
            .add(left_src.upcast_ref::<gst::Element>())
            .expect("add left appsrc");
        pipeline
            .add(right_src.upcast_ref::<gst::Element>())
            .expect("add right appsrc");

        let window = UnifiedMonitorWindow {
            pipeline,
            left_src,
            right_src,
        };
        drop(window);
    }
}
