// Eye-hub coverage for shared server video fan-out.
//
// Scope: include `server/src/main.rs` and exercise hub fan-out, conflict
// pruning, and shutdown behavior.
// Targets: `server/src/main.rs`.
// Why: eye-feed hubs are latency-sensitive; stale hubs must stop and be
// replaced without freezing downstream previews.

#[allow(warnings)]
mod server_main_eye_hub {
    include!(env!("LESAVKA_SERVER_MAIN_SRC"));

    use futures_util::stream;
    use temp_env::with_var;

    fn with_capture_power_disabled(f: impl FnOnce()) {
        with_var("LESAVKA_CAPTURE_POWER_UNIT", Some("none"), f);
    }

    #[test]
    fn shared_eye_hub_forwards_inner_packets() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        with_capture_power_disabled(|| {
            rt.block_on(async {
                let lease = CapturePowerManager::new().acquire().await;
                let packet = VideoPacket {
                    id: 2,
                    pts: 42,
                    data: vec![9, 8, 7],
                    ..Default::default()
                };
                let (packet_tx, packet_rx) = tokio::sync::mpsc::channel(1);
                let hub = EyeHub::spawn(ReceiverStream::new(packet_rx), lease);
                hub.subscribers
                    .fetch_add(1, std::sync::atomic::Ordering::AcqRel);
                let mut rx = hub.tx.subscribe();
                packet_tx
                    .send(Ok(packet.clone()))
                    .await
                    .expect("send synthetic packet");
                let observed = tokio::time::timeout(std::time::Duration::from_secs(1), rx.recv())
                    .await
                    .expect("hub packet timeout")
                    .expect("hub packet");
                assert_eq!(observed.id, packet.id);
                assert_eq!(observed.pts, packet.pts);
                assert_eq!(observed.data, packet.data);
                drop(packet_tx);
                for _ in 0..20 {
                    if !hub.running.load(std::sync::atomic::Ordering::Relaxed) {
                        return;
                    }
                    tokio::time::sleep(std::time::Duration::from_millis(10)).await;
                }
                assert!(
                    !hub.running.load(std::sync::atomic::Ordering::Relaxed),
                    "hub should stop after the synthetic packet stream closes"
                );
            });
        });
    }

    #[test]
    fn conflicting_eye_hubs_for_the_same_source_are_pruned_before_reopen() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        with_capture_power_disabled(|| {
            rt.block_on(async {
                let requested_key = EyeHubKey {
                    source_id: 1,
                    requested_width: 1280,
                    requested_height: 720,
                    requested_fps: 60,
                };
                let stale_same_source_key = EyeHubKey {
                    source_id: 1,
                    requested_width: 1920,
                    requested_height: 1080,
                    requested_fps: 60,
                };
                let keep_other_source_key = EyeHubKey {
                    source_id: 0,
                    requested_width: 1920,
                    requested_height: 1080,
                    requested_fps: 60,
                };
                let stale_same_source = EyeHub::spawn(
                    stream::pending::<Result<VideoPacket, tonic::Status>>(),
                    CapturePowerManager::new().acquire().await,
                );
                let stopped_other_source = EyeHub::spawn(
                    stream::pending::<Result<VideoPacket, tonic::Status>>(),
                    CapturePowerManager::new().acquire().await,
                );
                stopped_other_source.shutdown();
                let keep_other_source = EyeHub::spawn(
                    stream::pending::<Result<VideoPacket, tonic::Status>>(),
                    CapturePowerManager::new().acquire().await,
                );

                let mut hubs = std::collections::HashMap::new();
                hubs.insert(stale_same_source_key, stale_same_source.clone());
                hubs.insert(
                    EyeHubKey {
                        source_id: 0,
                        requested_width: 1280,
                        requested_height: 720,
                        requested_fps: 60,
                    },
                    stopped_other_source,
                );
                hubs.insert(keep_other_source_key, keep_other_source.clone());

                let removed = take_conflicting_eye_hubs(&mut hubs, requested_key);

                assert_eq!(removed.len(), 2);
                assert!(!hubs.contains_key(&stale_same_source_key));
                assert!(hubs.contains_key(&keep_other_source_key));
            });
        });
    }

    #[test]
    fn eye_hub_shutdown_marks_the_hub_as_not_running() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        with_capture_power_disabled(|| {
            rt.block_on(async {
                let hub = EyeHub::spawn(
                    stream::pending::<Result<VideoPacket, tonic::Status>>(),
                    CapturePowerManager::new().acquire().await,
                );
                assert!(hub.running.load(std::sync::atomic::Ordering::Relaxed));
                hub.shutdown();
                assert!(!hub.running.load(std::sync::atomic::Ordering::Relaxed));
            });
        });
    }

    #[test]
    fn eye_hub_shutdown_and_wait_releases_pending_stream_before_reopen() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        with_capture_power_disabled(|| {
            rt.block_on(async {
                let hub = EyeHub::spawn(
                    stream::pending::<Result<VideoPacket, tonic::Status>>(),
                    CapturePowerManager::new().acquire().await,
                );
                hub.shutdown_and_wait(std::time::Duration::from_millis(100))
                    .await;
                assert!(
                    !hub.running.load(std::sync::atomic::Ordering::Relaxed),
                    "conflicting eye hubs must be fully shut down before reopening the same V4L2 card"
                );
            });
        });
    }

    #[test]
    fn eye_hub_reopen_grace_defaults_to_v4l2_release_window() {
        with_var("LESAVKA_EYE_HUB_REOPEN_GRACE_MS", None::<&str>, || {
            assert_eq!(
                eye_hub_reopen_grace(),
                std::time::Duration::from_millis(250)
            );
        });
        with_var("LESAVKA_EYE_HUB_REOPEN_GRACE_MS", Some("75"), || {
            assert_eq!(eye_hub_reopen_grace(), std::time::Duration::from_millis(75));
        });
    }

    #[test]
    #[cfg(coverage)]
    fn eye_hub_forwarder_handles_dropped_downstream() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        with_capture_power_disabled(|| {
            rt.block_on(async {
                let hub = EyeHub::spawn(
                    stream::pending::<Result<VideoPacket, tonic::Status>>(),
                    CapturePowerManager::new().acquire().await,
                );
                hub.subscribers
                    .store(1, std::sync::atomic::Ordering::Relaxed);
                let (source_tx, source_rx) = tokio::sync::broadcast::channel(1);
                let (out_tx, out_rx) = tokio::sync::mpsc::channel(1);
                drop(out_rx);

                source_tx
                    .send(VideoPacket {
                        id: 0,
                        pts: 1,
                        data: vec![1],
                        ..Default::default()
                    })
                    .expect("send packet");
                forward_eye_hub_packets(
                    9,
                    source_rx,
                    out_tx,
                    std::sync::Arc::clone(&hub.subscribers),
                    std::sync::Arc::clone(&hub),
                )
                .await;

                assert_eq!(
                    hub.subscribers.load(std::sync::atomic::Ordering::Relaxed),
                    0
                );
                assert!(!hub.running.load(std::sync::atomic::Ordering::Relaxed));
            });
        });
    }

    #[test]
    #[cfg(coverage)]
    fn eye_hub_forwarder_skips_lagged_frames_and_closes_cleanly() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        with_capture_power_disabled(|| {
            rt.block_on(async {
                let hub = EyeHub::spawn(
                    stream::pending::<Result<VideoPacket, tonic::Status>>(),
                    CapturePowerManager::new().acquire().await,
                );
                hub.subscribers
                    .store(1, std::sync::atomic::Ordering::Relaxed);
                let (source_tx, source_rx) = tokio::sync::broadcast::channel(1);
                let (out_tx, mut out_rx) = tokio::sync::mpsc::channel(2);

                source_tx
                    .send(VideoPacket {
                        id: 0,
                        pts: 1,
                        data: vec![1],
                        ..Default::default()
                    })
                    .expect("send first packet");
                source_tx
                    .send(VideoPacket {
                        id: 0,
                        pts: 2,
                        data: vec![2],
                        ..Default::default()
                    })
                    .expect("send second packet");
                drop(source_tx);

                forward_eye_hub_packets(
                    7,
                    source_rx,
                    out_tx,
                    std::sync::Arc::clone(&hub.subscribers),
                    std::sync::Arc::clone(&hub),
                )
                .await;

                let packet = out_rx
                    .recv()
                    .await
                    .expect("forwarded packet")
                    .expect("packet");
                assert_eq!(packet.id, 7);
                assert_eq!(packet.pts, 2);
                assert_eq!(packet.data, vec![2]);
                assert!(out_rx.recv().await.is_none());
                assert_eq!(
                    hub.subscribers.load(std::sync::atomic::Ordering::Relaxed),
                    0
                );
                assert!(!hub.running.load(std::sync::atomic::Ordering::Relaxed));
            });
        });
    }
}
