// Extra integration coverage for server main HID startup branches.
//
// Scope: include `server/src/main.rs` and exercise successful handler startup
// with synthetic HID endpoints.
// Targets: `server/src/main.rs`.
// Why: the main contract file is near the 500 LOC cap, so additional branch
// coverage lives here.

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

    use futures_util::stream;
    use lesavka_common::lesavka::relay_client::RelayClient;
    use serial_test::serial;
    use std::os::unix::fs::PermissionsExt;
    use std::path::Path;
    use temp_env::with_var;
    use tempfile::tempdir;

    async fn connect_with_retry(addr: std::net::SocketAddr) -> tonic::transport::Channel {
        let endpoint = tonic::transport::Endpoint::from_shared(format!("http://{addr}"))
            .expect("endpoint")
            .tcp_nodelay(true);
        for _ in 0..40 {
            if let Ok(channel) = endpoint.clone().connect().await {
                return channel;
            }
            tokio::time::sleep(std::time::Duration::from_millis(25)).await;
        }
        panic!("failed to connect to local tonic server");
    }

    fn write_file(path: &Path, content: &str) {
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent).expect("create parent");
        }
        std::fs::write(path, content).expect("write file");
    }

    fn with_fake_gadget_roots(sys_root: &Path, cfg_root: &Path, f: impl FnOnce()) {
        let sys_root = sys_root.to_string_lossy().to_string();
        let cfg_root = cfg_root.to_string_lossy().to_string();
        with_var("LESAVKA_GADGET_SYSFS_ROOT", Some(sys_root), || {
            with_var("LESAVKA_GADGET_CONFIGFS_ROOT", Some(cfg_root), f);
        });
    }

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

    fn build_fake_gadget_tree(base: &Path, ctrl: &str, gadget_name: &str, state: &str) {
        write_file(
            &base.join(format!("sys/class/udc/{ctrl}/state")),
            &format!("{state}\n"),
        );
        write_file(
            &base.join(format!("cfg/{gadget_name}/UDC")),
            &format!("{ctrl}\n"),
        );
        write_file(&base.join("sys/bus/platform/drivers/dwc3/unbind"), "");
        write_file(&base.join("sys/bus/platform/drivers/dwc3/bind"), "");
    }

    fn write_helper(path: &Path, body: &str) {
        write_file(path, body);
        let mut perms = std::fs::metadata(path)
            .expect("helper metadata")
            .permissions();
        perms.set_mode(0o755);
        std::fs::set_permissions(path, perms).expect("chmod helper");
    }

    fn with_fast_usb_recovery(helper: &Path, f: impl FnOnce()) {
        let helper = helper.to_string_lossy().to_string();
        with_var("LESAVKA_CORE_HELPER", Some(helper), || {
            with_var("LESAVKA_USB_RECOVERY_CYCLE_WAIT_MS", Some("0"), || {
                with_var("LESAVKA_USB_RECOVERY_REBUILD_WAIT_MS", Some("0"), || {
                    with_var("LESAVKA_USB_RECOVERY_FINAL_WAIT_MS", Some("0"), f);
                })
            })
        });
    }

    fn build_handler_for_tests_with_modes(
        kb_writable: bool,
        ms_writable: bool,
    ) -> (tempfile::TempDir, Handler) {
        let dir = tempdir().expect("tempdir");
        let kb_path = dir.path().join("hidg0.bin");
        let ms_path = dir.path().join("hidg1.bin");
        std::fs::write(&kb_path, []).expect("create kb file");
        std::fs::write(&ms_path, []).expect("create ms file");

        let kb_std = std::fs::OpenOptions::new()
            .read(true)
            .write(kb_writable)
            .create(kb_writable)
            .truncate(kb_writable)
            .open(&kb_path)
            .expect("open kb");
        let ms_std = std::fs::OpenOptions::new()
            .read(true)
            .write(ms_writable)
            .create(ms_writable)
            .truncate(ms_writable)
            .open(&ms_path)
            .expect("open ms");
        let kb = tokio::fs::File::from_std(kb_std);
        let ms = tokio::fs::File::from_std(ms_std);

        (
            dir,
            Handler {
                kb: std::sync::Arc::new(tokio::sync::Mutex::new(Some(kb))),
                ms: std::sync::Arc::new(tokio::sync::Mutex::new(Some(ms))),
                gadget: UsbGadget::new("lesavka"),
                did_cycle: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
                camera_rt: std::sync::Arc::new(CameraRuntime::new()),
                upstream_media_rt: std::sync::Arc::new(UpstreamMediaRuntime::new()),
                calibration: std::sync::Arc::new(CalibrationStore::load(std::sync::Arc::new(
                    UpstreamMediaRuntime::new(),
                ))),
                capture_power: CapturePowerManager::new(),
                eye_hubs: std::sync::Arc::new(tokio::sync::Mutex::new(
                    std::collections::HashMap::new(),
                )),
            },
        )
    }

    fn build_handler_for_tests() -> (tempfile::TempDir, Handler) {
        build_handler_for_tests_with_modes(true, true)
    }

    #[test]
    #[serial]
    fn handler_new_and_reopen_hid_succeed_with_override_paths() {
        let dir = tempdir().expect("tempdir");
        std::fs::write(dir.path().join("hidg0"), "").expect("create hidg0");
        std::fs::write(dir.path().join("hidg1"), "").expect("create hidg1");
        let hid_dir = dir.path().to_string_lossy().to_string();

        with_var("LESAVKA_HID_DIR", Some(hid_dir), || {
            with_var("LESAVKA_ALLOW_GADGET_CYCLE", None::<&str>, || {
                let rt = tokio::runtime::Runtime::new().expect("runtime");
                rt.block_on(async {
                    let handler = Handler::new(UsbGadget::new("lesavka"))
                        .await
                        .expect("handler startup");
                    handler.reopen_hid().await.expect("reopen hid");
                });
            });
        });
    }

    #[test]
    #[serial]
    fn handler_new_with_cycle_enabled_can_still_open_override_paths() {
        let dir = tempdir().expect("tempdir");
        std::fs::write(dir.path().join("hidg0"), "").expect("create hidg0");
        std::fs::write(dir.path().join("hidg1"), "").expect("create hidg1");
        let hid_dir = dir.path().to_string_lossy().to_string();

        with_var("LESAVKA_HID_DIR", Some(hid_dir), || {
            with_var("LESAVKA_ALLOW_GADGET_CYCLE", Some("1"), || {
                let rt = tokio::runtime::Runtime::new().expect("runtime");
                rt.block_on(async {
                    let _handler = Handler::new(UsbGadget::new("lesavka"))
                        .await
                        .expect("handler startup with cycle enabled");
                });
            });
        });
    }

    #[test]
    #[serial]
    fn stream_keyboard_writes_reports_to_hid_file() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        with_capture_power_disabled(|| {
            rt.block_on(async {
                let (dir, handler) = build_handler_for_tests();
                let kb_path = dir.path().join("hidg0.bin");

                let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
                let addr = listener.local_addr().expect("addr");
                drop(listener);

                let server = tokio::spawn(async move {
                    let _ = tonic::transport::Server::builder()
                        .add_service(RelayServer::new(handler))
                        .serve(addr)
                        .await;
                });

                let channel = connect_with_retry(addr).await;
                let mut cli = RelayClient::new(channel);
                let (tx, rx) = tokio::sync::mpsc::channel(4);
                tx.send(KeyboardReport {
                    data: vec![1, 2, 3, 4, 5, 6, 7, 8],
                })
                .await
                .expect("send keyboard packet");
                drop(tx);

                let outbound = tokio_stream::wrappers::ReceiverStream::new(rx);
                let mut resp = cli
                    .stream_keyboard(tonic::Request::new(outbound))
                    .await
                    .expect("stream keyboard");
                let echoed = resp
                    .get_mut()
                    .message()
                    .await
                    .expect("grpc result")
                    .expect("echo packet");
                assert_eq!(echoed.data, vec![1, 2, 3, 4, 5, 6, 7, 8]);

                tokio::time::sleep(std::time::Duration::from_millis(30)).await;
                let written = std::fs::read(&kb_path).expect("read hidg0 file");
                assert!(
                    !written.is_empty(),
                    "keyboard stream should write HID bytes to target file"
                );

                server.abort();
            });
        });
    }

    #[test]
    #[serial]
    fn stream_mouse_writes_reports_to_hid_file() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        with_capture_power_disabled(|| {
            rt.block_on(async {
                let (dir, handler) = build_handler_for_tests();
                let ms_path = dir.path().join("hidg1.bin");

                let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
                let addr = listener.local_addr().expect("addr");
                drop(listener);

                let server = tokio::spawn(async move {
                    let _ = tonic::transport::Server::builder()
                        .add_service(RelayServer::new(handler))
                        .serve(addr)
                        .await;
                });

                let channel = connect_with_retry(addr).await;
                let mut cli = RelayClient::new(channel);
                let (tx, rx) = tokio::sync::mpsc::channel(4);
                tx.send(MouseReport {
                    data: vec![8, 7, 6, 5, 4, 3, 2, 1],
                })
                .await
                .expect("send mouse packet");
                drop(tx);

                let outbound = tokio_stream::wrappers::ReceiverStream::new(rx);
                let mut resp = cli
                    .stream_mouse(tonic::Request::new(outbound))
                    .await
                    .expect("stream mouse");
                let echoed = resp
                    .get_mut()
                    .message()
                    .await
                    .expect("grpc result")
                    .expect("echo packet");
                assert_eq!(echoed.data, vec![8, 7, 6, 5, 4, 3, 2, 1]);

                tokio::time::sleep(std::time::Duration::from_millis(30)).await;
                let written = std::fs::read(&ms_path).expect("read hidg1 file");
                assert!(
                    !written.is_empty(),
                    "mouse stream should write HID bytes to target file"
                );

                server.abort();
            });
        });
    }

    #[test]
    #[serial]
    fn stream_keyboard_recovers_when_hid_write_fails() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        with_capture_power_disabled(|| {
            rt.block_on(async {
                let (_dir, handler) = build_handler_for_tests_with_modes(false, true);

                let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
                let addr = listener.local_addr().expect("addr");
                drop(listener);

                let server = tokio::spawn(async move {
                    let _ = tonic::transport::Server::builder()
                        .add_service(RelayServer::new(handler))
                        .serve(addr)
                        .await;
                });

                let channel = connect_with_retry(addr).await;
                let mut cli = RelayClient::new(channel);
                let (tx, rx) = tokio::sync::mpsc::channel(4);
                tx.send(KeyboardReport {
                    data: vec![11, 12, 13, 14, 15, 16, 17, 18],
                })
                .await
                .expect("send keyboard packet");
                drop(tx);

                let outbound = tokio_stream::wrappers::ReceiverStream::new(rx);
                let mut resp = cli
                    .stream_keyboard(tonic::Request::new(outbound))
                    .await
                    .expect("stream keyboard");
                let echoed = resp
                    .get_mut()
                    .message()
                    .await
                    .expect("grpc result")
                    .expect("echo packet");
                assert_eq!(echoed.data, vec![11, 12, 13, 14, 15, 16, 17, 18]);

                server.abort();
            });
        });
    }

    #[test]
    #[serial]
    fn stream_mouse_recovers_when_hid_write_fails() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        with_capture_power_disabled(|| {
            rt.block_on(async {
                let (_dir, handler) = build_handler_for_tests_with_modes(true, false);

                let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
                let addr = listener.local_addr().expect("addr");
                drop(listener);

                let server = tokio::spawn(async move {
                    let _ = tonic::transport::Server::builder()
                        .add_service(RelayServer::new(handler))
                        .serve(addr)
                        .await;
                });

                let channel = connect_with_retry(addr).await;
                let mut cli = RelayClient::new(channel);
                let (tx, rx) = tokio::sync::mpsc::channel(4);
                tx.send(MouseReport {
                    data: vec![21, 22, 23, 24, 25, 26, 27, 28],
                })
                .await
                .expect("send mouse packet");
                drop(tx);

                let outbound = tokio_stream::wrappers::ReceiverStream::new(rx);
                let mut resp = cli
                    .stream_mouse(tonic::Request::new(outbound))
                    .await
                    .expect("stream mouse");
                let echoed = resp
                    .get_mut()
                    .message()
                    .await
                    .expect("grpc result")
                    .expect("echo packet");
                assert_eq!(echoed.data, vec![21, 22, 23, 24, 25, 26, 27, 28]);

                server.abort();
            });
        });
    }

    #[test]
    #[cfg(not(coverage))]
    #[serial]
    fn stream_microphone_returns_internal_error_without_uac_device() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        rt.block_on(async {
            let (_dir, handler) = build_handler_for_tests();

            let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
            let addr = listener.local_addr().expect("addr");
            drop(listener);

            let server = tokio::spawn(async move {
                let _ = tonic::transport::Server::builder()
                    .add_service(RelayServer::new(handler))
                    .serve(addr)
                    .await;
            });

            let channel = connect_with_retry(addr).await;
            let mut cli = RelayClient::new(channel);
            let (_tx, rx) = tokio::sync::mpsc::channel::<AudioPacket>(4);
            let outbound = tokio_stream::wrappers::ReceiverStream::new(rx);
            let err = cli
                .stream_microphone(tonic::Request::new(outbound))
                .await
                .expect_err("missing UAC sink should fail stream setup");
            assert_eq!(err.code(), tonic::Code::Internal);

            server.abort();
        });
    }
}
