// Include-based coverage for client app startup reactor behavior.
//
// Scope: compile the client app reactor with deterministic local stubs for
// capture/render dependencies, then exercise `new` + `run`.
// Targets: `client/src/app.rs`, `client/src/app/downlink_media.rs`.
// Why: app orchestration branches should stay stable in CI without physical
// devices.

mod handshake {
    #[allow(dead_code)]
    #[derive(Default, Clone, Debug)]
    pub struct PeerCaps {
        pub camera: bool,
        pub microphone: bool,
        pub bundled_webcam_media: bool,
        pub server_version: Option<String>,
    }

    pub async fn negotiate(_uri: &str) -> PeerCaps {
        PeerCaps {
            camera: std::env::var("LESAVKA_TEST_CAP_CAMERA").is_ok(),
            microphone: std::env::var("LESAVKA_TEST_CAP_MIC").is_ok(),
            bundled_webcam_media: std::env::var("LESAVKA_TEST_CAP_BUNDLED").is_ok(),
            server_version: None,
        }
    }
}

#[allow(warnings)]
mod live_capture_clock {
    include!(concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/tests/helpers/support/live_capture_clock_shim.rs"
    ));
}

#[path = "../../../../client/src/uplink_fresh_queue.rs"]
#[allow(warnings)]
mod uplink_fresh_queue;

#[path = "../../../../client/src/uplink_telemetry.rs"]
#[allow(warnings)]
mod uplink_telemetry;

#[path = "../../../../client/src/live_media_control.rs"]
#[allow(warnings)]
mod live_media_control;

#[path = "../../../../client/src/video_support.rs"]
#[allow(warnings)]
mod video_support;

mod app_support {
    use super::handshake::PeerCaps;
    use std::time::Duration;

    #[allow(dead_code)]
    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    pub enum CameraCodec {
        H264,
        Hevc,
        Mjpeg,
    }

    #[derive(Clone, Copy, Debug)]
    pub struct CameraConfig {
        pub codec: CameraCodec,
        pub width: u32,
        pub height: u32,
        pub fps: u32,
    }

    pub fn resolve_server_addr(_args: &[String], env_addr: Option<&str>) -> String {
        env_addr.unwrap_or("http://127.0.0.1:9").to_string()
    }

    pub fn camera_config_from_caps(caps: &PeerCaps) -> Option<CameraConfig> {
        if !caps.camera {
            return None;
        }
        Some(CameraConfig {
            codec: CameraCodec::H264,
            width: 1280,
            height: 720,
            fps: 30,
        })
    }

    pub fn sanitize_video_queue(queue: Option<usize>) -> usize {
        queue.unwrap_or(64).max(1)
    }

    pub fn next_delay(delay: Duration) -> Duration {
        std::cmp::min(delay.saturating_mul(2), Duration::from_secs(8))
    }
}

mod relay_transport {
    pub fn endpoint(server_addr: &str) -> anyhow::Result<tonic::transport::Endpoint> {
        Ok(tonic::transport::Channel::from_shared(
            server_addr.to_string(),
        )?)
    }
}

mod input {
    pub mod audio_codec {
        pub use lesavka_common::audio_transport::{UpstreamAudioCodec, parse_upstream_audio_codec};

        pub fn requested_upstream_audio_codec_from_env() -> UpstreamAudioCodec {
            std::env::var("LESAVKA_UPLINK_AUDIO_CODEC")
                .ok()
                .or_else(|| std::env::var("LESAVKA_AUDIO_CODEC").ok())
                .as_deref()
                .and_then(parse_upstream_audio_codec)
                .unwrap_or(UpstreamAudioCodec::Opus)
        }
    }

    pub mod camera {
        pub use crate::app_support::{CameraCodec, CameraConfig};
        use lesavka_common::lesavka::VideoPacket;

        pub struct CameraCapture;

        impl CameraCapture {
            pub fn new(_source: Option<&str>, _cfg: Option<CameraConfig>) -> anyhow::Result<Self> {
                Ok(Self)
            }

            pub fn new_with_capture_profile(
                source: Option<&str>,
                cfg: Option<CameraConfig>,
                _profile: Option<(u32, u32, u32)>,
            ) -> anyhow::Result<Self> {
                Self::new(source, cfg)
            }

            pub fn pull(&self) -> Option<VideoPacket> {
                None
            }
        }
    }

    pub mod microphone {
        use lesavka_common::lesavka::AudioPacket;

        pub struct MicrophoneCapture;

        impl MicrophoneCapture {
            pub fn new() -> anyhow::Result<Self> {
                Ok(Self)
            }

            pub fn new_default_source() -> anyhow::Result<Self> {
                Self::new()
            }

            pub fn new_with_source(_source: Option<&str>) -> anyhow::Result<Self> {
                Self::new()
            }

            pub fn new_default_source_options(
                _codec: super::audio_codec::UpstreamAudioCodec,
                _noise_suppression: bool,
            ) -> anyhow::Result<Self> {
                Self::new_default_source()
            }

            pub fn new_with_source_options(
                source: Option<&str>,
                _codec: super::audio_codec::UpstreamAudioCodec,
                _noise_suppression: bool,
            ) -> anyhow::Result<Self> {
                Self::new_with_source(source)
            }

            pub fn pull(&self) -> Option<AudioPacket> {
                None
            }
        }
    }

    pub mod inputs {
        use lesavka_common::lesavka::{KeyboardReport, MouseReport};
        use std::sync::{Arc, atomic::AtomicBool};
        use tokio::sync::{broadcast::Sender, mpsc::UnboundedSender};

        pub struct InputAggregator {
            _kbd_tx: Sender<KeyboardReport>,
            _mou_tx: Sender<MouseReport>,
            _dev_mode: bool,
            _paste_tx: Option<UnboundedSender<String>>,
            remote_capture_enabled: Arc<AtomicBool>,
        }

        impl InputAggregator {
            pub fn new(
                dev_mode: bool,
                kbd_tx: Sender<KeyboardReport>,
                mou_tx: Sender<MouseReport>,
                paste_tx: Option<UnboundedSender<String>>,
            ) -> Self {
                Self {
                    _kbd_tx: kbd_tx,
                    _mou_tx: mou_tx,
                    _dev_mode: dev_mode,
                    _paste_tx: paste_tx,
                    remote_capture_enabled: Arc::new(AtomicBool::new(true)),
                }
            }

            pub fn new_with_capture_mode(
                dev_mode: bool,
                kbd_tx: Sender<KeyboardReport>,
                mou_tx: Sender<MouseReport>,
                paste_tx: Option<UnboundedSender<String>>,
                capture_remote_boot: bool,
            ) -> Self {
                let aggregator = Self::new(dev_mode, kbd_tx, mou_tx, paste_tx);
                aggregator
                    .remote_capture_enabled
                    .store(capture_remote_boot, std::sync::atomic::Ordering::Relaxed);
                aggregator
            }

            pub fn init(&mut self) -> anyhow::Result<()> {
                Ok(())
            }

            pub async fn run(&mut self) -> anyhow::Result<()> {
                std::future::pending::<()>().await;
                #[allow(unreachable_code)]
                Ok(())
            }

            pub fn remote_capture_enabled_handle(&self) -> Arc<AtomicBool> {
                Arc::clone(&self.remote_capture_enabled)
            }
        }
    }
}

mod output {
    pub mod audio {
        use lesavka_common::lesavka::AudioPacket;

        pub struct AudioOut;

        impl AudioOut {
            pub fn new() -> anyhow::Result<Self> {
                Ok(Self)
            }

            pub fn new_default_sink() -> anyhow::Result<Self> {
                Self::new()
            }

            pub fn new_with_sink(_sink: Option<&str>) -> anyhow::Result<Self> {
                Self::new()
            }

            pub fn push(&self, _pkt: AudioPacket) {}
        }
    }

    pub mod video {
        use lesavka_common::lesavka::VideoPacket;

        pub struct MonitorWindow;
        pub struct UnifiedMonitorWindow;

        impl MonitorWindow {
            pub fn new(_id: u32) -> anyhow::Result<Self> {
                Ok(Self)
            }

            pub fn push_packet(&self, _pkt: VideoPacket) {}
        }

        impl UnifiedMonitorWindow {
            pub fn new() -> anyhow::Result<Self> {
                Ok(Self)
            }

            pub fn push_packet(&self, _pkt: VideoPacket) {}
        }
    }
}

mod paste {
    use anyhow::bail;
    use lesavka_common::lesavka::PasteRequest;

    pub fn build_paste_request(text: &str) -> anyhow::Result<PasteRequest> {
        if text == "bad" {
            bail!("synthetic paste build failure");
        }
        Ok(PasteRequest {
            nonce: vec![],
            data: text.as_bytes().to_vec(),
            encrypted: true,
        })
    }
}

#[path = "../../../../client/src/app.rs"]
#[allow(warnings)]
mod app_include_contract;

mod tests {
    use super::app_include_contract::{
        LesavkaClientApp, keyboard_stream_report, mouse_stream_report,
    };
    use lesavka_common::lesavka::{KeyboardReport, MouseReport};
    use serial_test::serial;
    use temp_env::with_var;
    use tokio_stream::wrappers::errors::BroadcastStreamRecvError;

    const DOWNLINK_MEDIA_SRC: &str = include_str!(concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/client/src/app/downlink_media.rs"
    ));
    const INPUT_STREAMS_SRC: &str = include_str!(concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/client/src/app/input_streams.rs"
    ));

    #[test]
    #[serial]
    fn run_headless_reaches_pending_reactor_branch() {
        with_var("LESAVKA_HEADLESS", Some("1"), || {
            with_var("LESAVKA_SERVER_ADDR", Some("http://127.0.0.1:9"), || {
                with_var("LESAVKA_TEST_CAP_CAMERA", None::<&str>, || {
                    with_var("LESAVKA_TEST_CAP_MIC", None::<&str>, || {
                        let rt = tokio::runtime::Runtime::new().expect("runtime");
                        rt.block_on(async {
                            let mut app = LesavkaClientApp::new().expect("new app");
                            let result = tokio::time::timeout(
                                std::time::Duration::from_millis(80),
                                app.run(),
                            )
                            .await;
                            assert!(result.is_err(), "headless run should stay pending");
                        });
                    });
                });
            });
        });
    }

    #[test]
    #[serial]
    fn run_non_headless_starts_stream_tasks_with_stubbed_caps() {
        with_var("LESAVKA_HEADLESS", None::<&str>, || {
            with_var("LESAVKA_SERVER_ADDR", Some("http://127.0.0.1:9"), || {
                with_var("LESAVKA_TEST_CAP_CAMERA", Some("1"), || {
                    with_var("LESAVKA_TEST_CAP_MIC", Some("1"), || {
                        let rt = tokio::runtime::Runtime::new().expect("runtime");
                        rt.block_on(async {
                            let mut app = LesavkaClientApp::new().expect("new app");
                            let result = tokio::time::timeout(
                                std::time::Duration::from_millis(120),
                                app.run(),
                            )
                            .await;
                            assert!(
                                result.is_err(),
                                "run should stay in the central reactor loop"
                            );
                        });
                    });
                });
            });
        });
    }

    #[test]
    fn keyboard_stream_report_turns_lag_into_a_clean_reset() {
        let mut remote_capture_was_enabled = true;
        let pkt = keyboard_stream_report(
            Err(BroadcastStreamRecvError::Lagged(3)),
            true,
            &mut remote_capture_was_enabled,
        )
        .expect("lagged keyboard item should produce reset");
        assert_eq!(pkt.data, vec![0; 8]);

        let passthrough = keyboard_stream_report(
            Ok(KeyboardReport {
                data: vec![1, 2, 3],
            }),
            true,
            &mut remote_capture_was_enabled,
        )
        .expect("ok keyboard item should pass through");
        assert_eq!(passthrough.data, vec![1, 2, 3]);
    }

    #[test]
    fn mouse_stream_report_turns_lag_into_a_neutral_packet() {
        let mut remote_capture_was_enabled = true;
        let pkt = mouse_stream_report(
            Err(BroadcastStreamRecvError::Lagged(5)),
            true,
            &mut remote_capture_was_enabled,
        )
        .expect("lagged mouse item should produce neutral packet");
        assert_eq!(pkt.data, vec![0; 4]);

        let passthrough = mouse_stream_report(
            Ok(MouseReport {
                data: vec![9, 8, 7, 6],
            }),
            true,
            &mut remote_capture_was_enabled,
        )
        .expect("ok mouse item should pass through");
        assert_eq!(passthrough.data, vec![9, 8, 7, 6]);
    }

    #[test]
    fn keyboard_stream_report_blocks_stale_packets_after_local_handoff() {
        let mut remote_capture_was_enabled = true;
        let reset = keyboard_stream_report(
            Ok(KeyboardReport {
                data: vec![4, 0, 5, 0, 0, 0, 0, 0],
            }),
            false,
            &mut remote_capture_was_enabled,
        )
        .expect("switching local should emit one reset");
        assert_eq!(reset.data, vec![0; 8]);

        let dropped = keyboard_stream_report(
            Ok(KeyboardReport {
                data: vec![1, 2, 3],
            }),
            false,
            &mut remote_capture_was_enabled,
        );
        assert!(
            dropped.is_none(),
            "stale keyboard packets should be dropped locally"
        );
    }

    #[test]
    fn mouse_stream_report_blocks_stale_packets_after_local_handoff() {
        let mut remote_capture_was_enabled = true;
        let reset = mouse_stream_report(
            Ok(MouseReport {
                data: vec![9, 8, 7, 6],
            }),
            false,
            &mut remote_capture_was_enabled,
        )
        .expect("switching local should emit one neutral mouse packet");
        assert_eq!(reset.data, vec![0; 4]);

        let dropped = mouse_stream_report(
            Ok(MouseReport {
                data: vec![1, 1, 1, 1],
            }),
            false,
            &mut remote_capture_was_enabled,
        );
        assert!(
            dropped.is_none(),
            "stale mouse packets should be dropped locally"
        );
    }

    #[test]
    fn audio_loop_backoff_contract_protects_server_from_reconnect_storms() {
        assert!(DOWNLINK_MEDIA_SRC.contains("let mut delay = Duration::from_secs(1);"));
        assert!(DOWNLINK_MEDIA_SRC.contains("tokio::time::sleep(delay).await;"));
        assert!(DOWNLINK_MEDIA_SRC.contains("delay = app_support::next_delay(delay);"));
        assert!(DOWNLINK_MEDIA_SRC.contains("consecutive_source_failures = 0;"));
    }

    #[test]
    fn input_streams_reconnect_quickly_then_back_off_under_outage() {
        assert!(INPUT_STREAMS_SRC.contains("INPUT_RECONNECT_BASE_DELAY"));
        assert!(INPUT_STREAMS_SRC.contains("Duration::from_millis(250)"));
        assert!(INPUT_STREAMS_SRC.contains("delay = app_support::next_delay(delay);"));
        assert!(INPUT_STREAMS_SRC.contains("tokio::time::sleep(delay).await;"));
    }
}
