// Include-based coverage for USB gadget orchestration helpers.
//
// Scope: include `server/src/gadget.rs` and exercise deterministic helper
// logic and error branches without touching live gadget sysfs state.
// Targets: `server/src/gadget.rs`.
// Why: most gadget logic is file-system and errno handling that should remain
// stable regardless of host environment.

#[allow(warnings)]
mod gadget_include_contract {
    include!(env!("LESAVKA_SERVER_GADGET_SRC"));

    use serial_test::serial;
    use std::os::unix::fs::{PermissionsExt, symlink};
    use temp_env::with_var;
    use tempfile::{NamedTempFile, tempdir};

    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_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 build_fake_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!("sys/class/udc/{ctrl}/soft_connect")),
            "1\n",
        );
        write_file(
            &base.join("sys/bus/platform/drivers/dwc2/unbind"),
            "placeholder\n",
        );
        write_file(
            &base.join("sys/bus/platform/drivers/dwc2/bind"),
            "placeholder\n",
        );
        let driver_target = base.join("sys/bus/platform/drivers/dwc2");
        let driver_link = base.join(format!("sys/bus/platform/devices/{ctrl}/driver"));
        if let Some(parent) = driver_link.parent() {
            std::fs::create_dir_all(parent).expect("create driver link parent");
        }
        symlink(&driver_target, &driver_link).expect("link controller driver");
        write_file(
            &base.join(format!("cfg/{gadget_name}/UDC")),
            &format!("{ctrl}\n"),
        );
    }

    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_recovery_env(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);
                })
            })
        });
    }

    #[test]
    fn new_builds_expected_udc_path() {
        let gadget = UsbGadget::new("lesavka-test");
        assert!(gadget.udc_file.ends_with("/lesavka-test/UDC"));
    }

    #[test]
    fn state_errors_for_missing_controller() {
        let result = UsbGadget::state("definitely-missing-udc");
        assert!(result.is_err());
    }

    #[test]
    fn host_attached_state_matches_udc_states_that_can_accept_hid() {
        for state in ["configured", "addressed", "default", "suspended", "unknown"] {
            assert!(UsbGadget::host_attached_state(state), "{state}");
        }
        assert!(!UsbGadget::host_attached_state("not attached"));
        assert!(!UsbGadget::host_attached_state("broken"));
    }

    #[test]
    fn host_enumerated_state_excludes_unknown_and_not_attached() {
        for state in ["configured", "addressed", "default", "suspended"] {
            assert!(UsbGadget::host_enumerated_state(state), "{state}");
        }
        assert!(!UsbGadget::host_enumerated_state("unknown"));
        assert!(!UsbGadget::host_enumerated_state("not attached"));
    }

    #[test]
    #[serial]
    fn current_controller_state_reads_fake_udc_tree() {
        let dir = tempdir().expect("tempdir");
        let ctrl = "fake-ctrl.usb";
        build_fake_tree(dir.path(), ctrl, "lesavka-test", "configured");

        with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
            let (found_ctrl, state) = UsbGadget::current_controller_state().expect("current state");
            assert_eq!(found_ctrl, ctrl);
            assert_eq!(state, "configured");
        });
    }

    #[test]
    fn wait_state_any_times_out_for_missing_controller() {
        let result = UsbGadget::wait_state_any("definitely-missing-udc", 0);
        assert!(result.is_err());
    }

    #[test]
    #[serial]
    fn wait_state_any_accepts_configured_and_not_attached_fake_states() {
        let dir = tempdir().expect("tempdir");
        let ctrl = "fake-ctrl.usb";
        build_fake_tree(dir.path(), ctrl, "lesavka-test", "configured");

        with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
            assert_eq!(
                UsbGadget::wait_state_any(ctrl, 0).expect("configured state"),
                "configured"
            );
            let state_path = dir.path().join(format!("sys/class/udc/{ctrl}/state"));
            std::fs::write(state_path, "not attached\n").expect("flip state");
            assert_eq!(
                UsbGadget::wait_state_any(ctrl, 0).expect("not attached state"),
                "not attached"
            );
        });
    }

    #[test]
    fn wait_state_times_out_for_missing_controller() {
        let result = UsbGadget::wait_state("definitely-missing-udc", "configured", 0);
        assert!(result.is_err());
    }

    #[test]
    #[serial]
    fn wait_state_accepts_matching_fake_state() {
        let dir = tempdir().expect("tempdir");
        let ctrl = "fake-ctrl.usb";
        build_fake_tree(dir.path(), ctrl, "lesavka-test", "configured");

        with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
            UsbGadget::wait_state(ctrl, "configured", 0).expect("configured state");
        });
    }

    #[test]
    fn write_attr_writes_value_with_trailing_newline() {
        let file = NamedTempFile::new().expect("temp file");
        UsbGadget::write_attr(file.path(), "configured").expect("write attr");
        let content = std::fs::read_to_string(file.path()).expect("read back");
        assert_eq!(content, "configured\n");
    }

    #[test]
    fn wait_udc_present_times_out_for_missing_path() {
        let result = UsbGadget::wait_udc_present("definitely-missing-udc", 0);
        assert!(result.is_err());
    }

    #[test]
    fn probe_platform_udc_is_non_panicking() {
        let _ = UsbGadget::probe_platform_udc();
    }

    #[test]
    #[serial]
    fn soft_connect_path_skips_dwc2_platform_driver() {
        let dir = tempdir().expect("tempdir");
        let ctrl = "fake-ctrl.usb";
        build_fake_tree(dir.path(), ctrl, "lesavka-test", "not attached");

        with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
            assert!(UsbGadget::soft_connect_path(ctrl).is_none());
        });
    }

    #[test]
    #[serial]
    fn soft_connect_path_can_be_forced_for_dwc2() {
        let dir = tempdir().expect("tempdir");
        let ctrl = "fake-ctrl.usb";
        build_fake_tree(dir.path(), ctrl, "lesavka-test", "not attached");

        with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
            with_var("LESAVKA_FORCE_SOFT_CONNECT", Some("1"), || {
                let path = UsbGadget::soft_connect_path(ctrl).expect("forced soft_connect path");
                assert!(path.ends_with("/soft_connect"));
            });
        });
    }

    #[test]
    fn find_controller_returns_name_or_error_without_panicking() {
        let result = UsbGadget::find_controller();
        if let Ok(name) = result {
            assert!(!name.is_empty());
        }
    }

    #[test]
    fn is_still_detaching_matches_expected_errno_set() {
        let busy = anyhow::Error::from(std::io::Error::from_raw_os_error(libc::EBUSY));
        let missing = anyhow::Error::from(std::io::Error::from_raw_os_error(libc::ENOENT));
        let no_dev = anyhow::Error::from(std::io::Error::from_raw_os_error(libc::ENODEV));
        let other = anyhow::Error::from(std::io::Error::from_raw_os_error(libc::EACCES));
        let non_io = anyhow::anyhow!("plain error");

        assert!(UsbGadget::is_still_detaching(&busy));
        assert!(UsbGadget::is_still_detaching(&missing));
        assert!(UsbGadget::is_still_detaching(&no_dev));
        assert!(!UsbGadget::is_still_detaching(&other));
        assert!(!UsbGadget::is_still_detaching(&non_io));
    }

    #[test]
    fn rebind_driver_errors_when_driver_nodes_are_absent() {
        let dir = tempdir().expect("tempdir");
        with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
            let result = UsbGadget::rebind_driver("definitely-missing-udc");
            assert!(result.is_err());
        });
    }

    #[test]
    #[serial]
    fn cycle_handles_missing_gadget_sysfs_gracefully() {
        let gadget = UsbGadget::new("lesavka-test");
        with_var("LESAVKA_GADGET_FORCE_CYCLE", None::<&str>, || {
            let result = gadget.cycle();
            assert!(result.is_err() || result.is_ok());
        });
    }

    #[test]
    #[serial]
    fn cycle_force_mode_still_returns_without_panicking() {
        let dir = tempdir().expect("tempdir");
        let fake_udc = dir.path().join("UDC");
        std::fs::write(&fake_udc, "").expect("create fake udc file");

        let gadget = UsbGadget {
            udc_file: Box::leak(fake_udc.to_string_lossy().to_string().into_boxed_str()),
        };

        with_var("LESAVKA_GADGET_FORCE_CYCLE", Some("1"), || {
            let result = gadget.cycle();
            assert!(result.is_err() || result.is_ok());
        });
    }

    #[test]
    #[serial]
    fn cycle_short_circuits_when_host_is_attached_without_force() {
        let dir = tempdir().expect("tempdir");
        let ctrl = "fake-ctrl.usb";
        build_fake_tree(dir.path(), ctrl, "lesavka-test", "configured");

        with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
            let gadget = UsbGadget::new("lesavka-test");
            let result = gadget.cycle();
            assert!(
                result.is_ok(),
                "configured host state should short-circuit safely"
            );
        });
    }

    #[test]
    #[serial]
    fn cycle_short_circuits_when_state_file_disappears_without_force() {
        let dir = tempdir().expect("tempdir");
        let ctrl = "fake-ctrl.usb";
        std::fs::create_dir_all(dir.path().join(format!("sys/class/udc/{ctrl}")))
            .expect("create fake controller without state");
        write_file(
            &dir.path().join("cfg/lesavka-test/UDC"),
            &format!("{ctrl}\n"),
        );

        with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
            let gadget = UsbGadget::new("lesavka-test");
            gadget
                .cycle()
                .expect("missing state should allow standby short-circuit");
        });
    }

    #[test]
    #[serial]
    fn cycle_force_mode_completes_on_fake_tree_when_state_stays_not_attached() {
        let dir = tempdir().expect("tempdir");
        let ctrl = "fake-ctrl.usb";
        build_fake_tree(dir.path(), ctrl, "lesavka-test", "not attached");

        with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
            with_var("LESAVKA_GADGET_FORCE_CYCLE", Some("1"), || {
                let gadget = UsbGadget::new("lesavka-test");
                let result = gadget.cycle();
                assert!(
                    result.is_ok(),
                    "force cycle should complete on fake sysfs tree"
                );
                let udc_path = dir.path().join("cfg/lesavka-test/UDC");
                let value = std::fs::read_to_string(udc_path).expect("read udc file");
                assert_eq!(value.trim(), ctrl);
            });
        });
    }

    #[test]
    #[serial]
    fn cycle_force_mode_accepts_late_configured_transition() {
        let dir = tempdir().expect("tempdir");
        let ctrl = "fake-ctrl.usb";
        build_fake_tree(dir.path(), ctrl, "lesavka-test", "not attached");
        let state_path = dir.path().join(format!("sys/class/udc/{ctrl}/state"));
        let state_path_bg = state_path.clone();

        let writer = std::thread::spawn(move || {
            std::thread::sleep(std::time::Duration::from_millis(150));
            std::fs::write(state_path_bg, "configured\n").expect("flip state");
        });

        with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
            with_var("LESAVKA_GADGET_FORCE_CYCLE", Some("1"), || {
                let gadget = UsbGadget::new("lesavka-test");
                let result = gadget.cycle();
                assert!(
                    result.is_ok(),
                    "configured transition should satisfy final wait_state"
                );
            });
        });

        writer.join().expect("join state writer");
    }
}
