// USB reset and eye-hub coverage for server main relay branches.
//
// Scope: include `server/src/main.rs` and exercise USB recovery plus shared
// eye-feed hub behavior with synthetic endpoints.
// Targets: `server/src/main.rs`.
// Why: USB recovery and shared downstream video hubs are operational escape
// hatches; regressions here can leave HID or eye feeds unavailable.

#[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)
    }

    fn handler_for_hid_dir(hid_dir: &Path) -> Handler {
        let kb = tokio::fs::File::from_std(
            std::fs::OpenOptions::new()
                .read(true)
                .write(true)
                .open(hid_dir.join("hidg0"))
                .expect("open hidg0"),
        );
        let ms = tokio::fs::File::from_std(
            std::fs::OpenOptions::new()
                .read(true)
                .write(true)
                .open(hid_dir.join("hidg1"))
                .expect("open hidg1"),
        );
        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 prepare_reset_fixture(state: &str) -> (tempfile::TempDir, std::path::PathBuf) {
        let dir = tempdir().expect("tempdir");
        let hid_dir = dir.path().join("hid");
        std::fs::create_dir_all(&hid_dir).expect("create hid dir");
        std::fs::write(hid_dir.join("hidg0"), "").expect("create hidg0");
        std::fs::write(hid_dir.join("hidg1"), "").expect("create hidg1");
        build_fake_gadget_tree(dir.path(), "fake-ctrl.usb", "lesavka", state);
        (dir, hid_dir)
    }

    #[test]
    #[serial]
    fn reset_usb_reports_host_not_attached_after_fake_cycle() {
        let dir = tempdir().expect("tempdir");
        let hid_dir = dir.path().join("hid");
        std::fs::create_dir_all(&hid_dir).expect("create hid dir");
        std::fs::write(hid_dir.join("hidg0"), "").expect("create hidg0");
        std::fs::write(hid_dir.join("hidg1"), "").expect("create hidg1");
        build_fake_gadget_tree(dir.path(), "fake-ctrl.usb", "lesavka", "not attached");
        let helper = dir.path().join("noop-core.sh");
        write_helper(
            &helper,
            r#"#!/usr/bin/env bash
set -euo pipefail
echo noop core helper >&2
"#,
        );

        with_fake_gadget_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
            with_var(
                "LESAVKA_HID_DIR",
                Some(hid_dir.to_string_lossy().to_string()),
                || {
                    with_fast_usb_recovery(&helper, || {
                        let kb = tokio::fs::File::from_std(
                            std::fs::OpenOptions::new()
                                .read(true)
                                .write(true)
                                .open(hid_dir.join("hidg0"))
                                .expect("open hidg0"),
                        );
                        let ms = tokio::fs::File::from_std(
                            std::fs::OpenOptions::new()
                                .read(true)
                                .write(true)
                                .open(hid_dir.join("hidg1"))
                                .expect("open hidg1"),
                        );
                        let handler = 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(),
                            )),
                        };
                        let rt = tokio::runtime::Runtime::new().expect("runtime");
                        let err = rt
                            .block_on(async {
                                handler.reset_usb(tonic::Request::new(Empty {})).await
                            })
                            .expect_err("reset usb should report a host that never enumerates");
                        assert_eq!(err.code(), tonic::Code::FailedPrecondition);
                        assert!(
                            err.message().contains("still not attached"),
                            "unexpected reset error: {}",
                            err.message()
                        );
                    });
                },
            );
        });
    }

    #[test]
    #[serial]
    fn reset_usb_forced_rebuild_can_recover_unattached_fake_udc() {
        let dir = tempdir().expect("tempdir");
        let hid_dir = dir.path().join("hid");
        std::fs::create_dir_all(&hid_dir).expect("create hid dir");
        std::fs::write(hid_dir.join("hidg0"), "").expect("create hidg0");
        std::fs::write(hid_dir.join("hidg1"), "").expect("create hidg1");
        build_fake_gadget_tree(dir.path(), "fake-ctrl.usb", "lesavka", "not attached");
        let helper = dir.path().join("recover-core.sh");
        write_helper(
            &helper,
            r#"#!/usr/bin/env bash
set -euo pipefail
echo recover core helper >&2
printf 'configured\n' > "$LESAVKA_GADGET_SYSFS_ROOT/class/udc/fake-ctrl.usb/state"
"#,
        );

        with_fake_gadget_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
            with_var(
                "LESAVKA_HID_DIR",
                Some(hid_dir.to_string_lossy().to_string()),
                || {
                    with_fast_usb_recovery(&helper, || {
                        let kb = tokio::fs::File::from_std(
                            std::fs::OpenOptions::new()
                                .read(true)
                                .write(true)
                                .open(hid_dir.join("hidg0"))
                                .expect("open hidg0"),
                        );
                        let ms = tokio::fs::File::from_std(
                            std::fs::OpenOptions::new()
                                .read(true)
                                .write(true)
                                .open(hid_dir.join("hidg1"))
                                .expect("open hidg1"),
                        );
                        let handler = 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(),
                            )),
                        };
                        let rt = tokio::runtime::Runtime::new().expect("runtime");
                        let reply = rt
                            .block_on(async {
                                handler.reset_usb(tonic::Request::new(Empty {})).await
                            })
                            .expect("reset usb should recover fake host")
                            .into_inner();
                        assert!(reply.ok);
                    });
                },
            );
        });
    }

    #[test]
    #[cfg(coverage)]
    #[serial]
    fn reset_usb_reports_non_enumerated_and_unreadable_state_after_recovery() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        let (dir, hid_dir) = prepare_reset_fixture("configured");
        with_fake_gadget_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
            with_var(
                "LESAVKA_HID_DIR",
                Some(hid_dir.to_string_lossy().to_string()),
                || {
                    let handler = handler_for_hid_dir(&hid_dir);
                    with_var("LESAVKA_TEST_RECOVERY_STATE", Some("not attached"), || {
                        let err = rt
                            .block_on(async {
                                handler.reset_usb(tonic::Request::new(Empty {})).await
                            })
                            .expect_err("non-enumerated recovery state");
                        assert_eq!(err.code(), tonic::Code::FailedPrecondition);
                        assert!(err.message().contains("still not attached"));
                    });

                    with_var("LESAVKA_TEST_RECOVERY_STATE_ERROR", Some("1"), || {
                        let err = rt
                            .block_on(async {
                                handler.reset_usb(tonic::Request::new(Empty {})).await
                            })
                            .expect_err("unreadable recovery state");
                        assert_eq!(err.code(), tonic::Code::FailedPrecondition);
                        assert!(err.message().contains("cannot read UDC state"));
                    });
                },
            );
        });
    }

    #[test]
    #[cfg(coverage)]
    #[serial]
    fn reset_usb_reports_uvc_helper_restart_failure_after_recovery() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        let (dir, hid_dir) = prepare_reset_fixture("configured");
        with_fake_gadget_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
            with_var(
                "LESAVKA_HID_DIR",
                Some(hid_dir.to_string_lossy().to_string()),
                || {
                    with_var(
                        "LESAVKA_TEST_UVC_HELPER_RESTART_ERR",
                        Some("synthetic restart failure"),
                        || {
                            let handler = handler_for_hid_dir(&hid_dir);
                            let err = rt
                                .block_on(async {
                                    handler.reset_usb(tonic::Request::new(Empty {})).await
                                })
                                .expect_err("restart helper failure");
                            assert_eq!(err.code(), tonic::Code::Internal);
                            assert!(err.message().contains("synthetic restart failure"));
                        },
                    );
                },
            );
        });
    }

    #[test]
    #[cfg(coverage)]
    #[serial]
    fn reset_usb_reports_reopen_hid_failure_after_successful_recovery() {
        let rt = tokio::runtime::Runtime::new().expect("runtime");
        let (dir, hid_dir) = prepare_reset_fixture("configured");
        let bad_hid_dir = dir.path().join("bad-hid");
        std::fs::create_dir_all(bad_hid_dir.join("hidg0")).expect("create bad hidg0 dir");
        std::fs::create_dir_all(bad_hid_dir.join("hidg1")).expect("create bad hidg1 dir");

        with_fake_gadget_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
            with_var(
                "LESAVKA_HID_DIR",
                Some(bad_hid_dir.to_string_lossy().to_string()),
                || {
                    let handler = handler_for_hid_dir(&hid_dir);
                    let err = rt
                        .block_on(async { handler.reset_usb(tonic::Request::new(Empty {})).await })
                        .expect_err("bad HID paths should fail after USB recovery");
                    assert_eq!(err.code(), tonic::Code::Internal);
                    assert!(
                        err.message().contains("opening"),
                        "unexpected reset error: {}",
                        err.message()
                    );
                },
            );
        });
    }
}
