// Integration coverage for client input-device classification helpers.
//
// Scope: include the input aggregator source and exercise private device
// classification against synthetic uinput keyboard/mouse devices.
// Targets: `client/src/input/inputs.rs`.
// Why: device classification regressions can silently break all input capture
// at runtime, so classifier behavior should stay under contract.

mod layout {
    pub use lesavka_client::layout::*;
}

mod keyboard {
    pub use lesavka_client::input::keyboard::*;
}

mod mouse {
    pub use lesavka_client::input::mouse::*;
}

#[allow(warnings)]
mod inputs_contract {
    include!(env!("LESAVKA_CLIENT_INPUTS_SRC"));

    use evdev::AttributeSet;
    use evdev::uinput::VirtualDevice;
    use serial_test::serial;
    use std::thread;
    use temp_env::{with_var, with_vars};

    fn open_virtual_device_with_path(
        vdev: &mut VirtualDevice,
    ) -> Option<(std::path::PathBuf, evdev::Device)> {
        for _ in 0..40 {
            if let Ok(mut nodes) = vdev.enumerate_dev_nodes_blocking() {
                if let Some(Ok(path)) = nodes.next() {
                    if let Ok(dev) = evdev::Device::open(&path) {
                        let _ = dev.set_nonblocking(true);
                        return Some((path.to_path_buf(), dev));
                    }
                }
            }
            thread::sleep(std::time::Duration::from_millis(10));
        }
        None
    }

    fn open_virtual_device(vdev: &mut VirtualDevice) -> Option<evdev::Device> {
        open_virtual_device_with_path(vdev).map(|(_, dev)| dev)
    }

    fn build_keyboard() -> Option<evdev::Device> {
        let mut keys = AttributeSet::<evdev::KeyCode>::new();
        keys.insert(evdev::KeyCode::KEY_A);
        keys.insert(evdev::KeyCode::KEY_ENTER);

        let mut vdev = VirtualDevice::builder()
            .ok()?
            .name("input-classify-kbd")
            .with_keys(&keys)
            .ok()?
            .build()
            .ok()?;

        open_virtual_device(&mut vdev)
    }

    fn build_mouse() -> Option<evdev::Device> {
        let mut keys = AttributeSet::<evdev::KeyCode>::new();
        keys.insert(evdev::KeyCode::BTN_LEFT);
        let mut rel = AttributeSet::<evdev::RelativeAxisCode>::new();
        rel.insert(evdev::RelativeAxisCode::REL_X);
        rel.insert(evdev::RelativeAxisCode::REL_Y);

        let mut vdev = VirtualDevice::builder()
            .ok()?
            .name("lesavka-input-classify-mouse")
            .with_keys(&keys)
            .ok()?
            .with_relative_axes(&rel)
            .ok()?
            .build()
            .ok()?;

        open_virtual_device(&mut vdev)
    }

    fn build_touch_mouse() -> Option<evdev::Device> {
        let mut keys = AttributeSet::<evdev::KeyCode>::new();
        keys.insert(evdev::KeyCode::BTN_TOUCH);
        let abs = evdev::AbsInfo::new(0, 0, 1024, 0, 0, 0);

        let mut vdev = VirtualDevice::builder()
            .ok()?
            .name("lesavka-input-classify-touch")
            .with_keys(&keys)
            .ok()?
            .with_absolute_axis(&evdev::UinputAbsSetup::new(
                evdev::AbsoluteAxisCode::ABS_MT_POSITION_X,
                abs,
            ))
            .ok()?
            .with_absolute_axis(&evdev::UinputAbsSetup::new(
                evdev::AbsoluteAxisCode::ABS_MT_POSITION_Y,
                abs,
            ))
            .ok()?
            .build()
            .ok()?;

        open_virtual_device(&mut vdev)
    }

    fn build_misc_key_device() -> Option<evdev::Device> {
        let mut keys = AttributeSet::<evdev::KeyCode>::new();
        keys.insert(evdev::KeyCode::KEY_VOLUMEUP);

        let mut vdev = VirtualDevice::builder()
            .ok()?
            .name("lesavka-input-classify-other")
            .with_keys(&keys)
            .ok()?
            .build()
            .ok()?;

        open_virtual_device(&mut vdev)
    }

    fn build_named_keyboard(name: &str) -> Option<evdev::Device> {
        let mut keys = AttributeSet::<evdev::KeyCode>::new();
        keys.insert(evdev::KeyCode::KEY_A);
        keys.insert(evdev::KeyCode::KEY_ENTER);

        let mut vdev = VirtualDevice::builder()
            .ok()?
            .name(name)
            .with_keys(&keys)
            .ok()?
            .build()
            .ok()?;

        open_virtual_device(&mut vdev)
    }

    fn build_keyboard_pair(name: &str) -> Option<(VirtualDevice, evdev::Device)> {
        build_keyboard_pair_with_keys(name, &[evdev::KeyCode::KEY_A, evdev::KeyCode::KEY_ENTER])
            .map(|(vdev, _, dev)| (vdev, dev))
    }

    fn build_keyboard_pair_with_keys(
        name: &str,
        supported_keys: &[evdev::KeyCode],
    ) -> Option<(VirtualDevice, std::path::PathBuf, evdev::Device)> {
        let mut keys = AttributeSet::<evdev::KeyCode>::new();
        for key in supported_keys {
            keys.insert(*key);
        }

        let mut vdev = VirtualDevice::builder()
            .ok()?
            .name(name)
            .with_keys(&keys)
            .ok()?
            .build()
            .ok()?;

        let (path, dev) = open_virtual_device_with_path(&mut vdev)?;
        Some((vdev, path, dev))
    }

    fn build_mouse_pair(name: &str) -> Option<(VirtualDevice, evdev::Device)> {
        build_mouse_pair_with_path(name).map(|(vdev, _, dev)| (vdev, dev))
    }

    fn build_mouse_pair_with_path(
        name: &str,
    ) -> Option<(VirtualDevice, std::path::PathBuf, evdev::Device)> {
        let mut keys = AttributeSet::<evdev::KeyCode>::new();
        keys.insert(evdev::KeyCode::BTN_LEFT);
        let mut rel = AttributeSet::<evdev::RelativeAxisCode>::new();
        rel.insert(evdev::RelativeAxisCode::REL_X);
        rel.insert(evdev::RelativeAxisCode::REL_Y);

        let mut vdev = VirtualDevice::builder()
            .ok()?
            .name(name)
            .with_keys(&keys)
            .ok()?
            .with_relative_axes(&rel)
            .ok()?
            .build()
            .ok()?;

        let (path, dev) = open_virtual_device_with_path(&mut vdev)?;
        Some((vdev, path, dev))
    }

    fn new_aggregator() -> InputAggregator {
        let (kbd_tx, _) = tokio::sync::broadcast::channel(32);
        let (mou_tx, _) = tokio::sync::broadcast::channel(32);
        InputAggregator::new(false, kbd_tx, mou_tx, None)
    }

    #[test]
    #[serial]
    fn classify_device_recognizes_keyboard_and_mouse_capabilities() {
        if let Some(kbd) = build_keyboard() {
            assert!(matches!(classify_device(&kbd), DeviceKind::Keyboard));
        }

        if let Some(mouse) = build_mouse() {
            assert!(matches!(classify_device(&mouse), DeviceKind::Mouse));
        }

        if let Some(touch) = build_touch_mouse() {
            assert!(matches!(classify_device(&touch), DeviceKind::Mouse));
        }

        if let Some(other) = build_misc_key_device() {
            assert!(matches!(classify_device(&other), DeviceKind::Other));
        }
    }

    #[test]
    #[serial]
    fn classify_device_ignores_synthetic_automation_keyboards() {
        if let Some(automation) = build_named_keyboard("Lesavka Automation Input") {
            assert!(matches!(classify_device(&automation), DeviceKind::Other));
        }
        if let Some(persistent) = build_named_keyboard("codex-persistent-kbd") {
            assert!(matches!(classify_device(&persistent), DeviceKind::Other));
        }
    }

    #[test]
    fn toggle_grab_switches_into_local_control_mode() {
        let mut agg = new_aggregator();
        agg.toggle_grab();
        assert!(agg.pending_release);
        assert!(!agg.released);
    }

    #[test]
    fn toggle_grab_switches_back_to_remote_control_when_released() {
        let mut agg = new_aggregator();
        agg.released = true;
        agg.pending_release = false;
        agg.toggle_grab();
        assert!(!agg.released);
        assert!(!agg.pending_release);
    }

    #[test]
    fn toggle_grab_ignores_requests_while_release_is_pending() {
        let mut agg = new_aggregator();
        agg.pending_release = true;
        agg.toggle_grab();
        assert!(agg.pending_release);
        assert!(!agg.released);
    }

    #[test]
    fn toggle_grab_ignores_requests_while_kill_release_is_pending() {
        let mut agg = new_aggregator();
        agg.pending_kill = true;
        agg.toggle_grab();
        assert!(agg.pending_kill);
        assert!(!agg.pending_release);
        assert!(!agg.released);
    }

    #[test]
    #[serial]
    fn capture_pending_keys_collects_current_keyboard_state() {
        let Some((mut vdev, dev)) = build_keyboard_pair("lesavka-input-pending-keys") else {
            return;
        };

        let (kbd_tx, _) = tokio::sync::broadcast::channel(16);
        let (agg_kbd_tx, _) = tokio::sync::broadcast::channel(16);
        let (agg_mou_tx, _) = tokio::sync::broadcast::channel(16);
        let mut keyboard = KeyboardAggregator::new(dev, false, kbd_tx, None);

        vdev.emit(&[evdev::InputEvent::new(
            evdev::EventType::KEY.0,
            evdev::KeyCode::KEY_A.0,
            1,
        )])
        .expect("emit key press");
        thread::sleep(std::time::Duration::from_millis(20));
        keyboard.process_events();

        let mut agg = InputAggregator::new(false, agg_kbd_tx, agg_mou_tx, None);
        agg.keyboards.push(keyboard);
        agg.capture_pending_keys();

        assert!(agg.pending_keys.contains(&evdev::KeyCode::KEY_A));
    }

    #[test]
    #[serial]
    fn init_grabs_virtual_keyboard_and_mouse_when_available() {
        let Some((_kbd_vdev, kbd_path, _kbd_dev)) = build_keyboard_pair_with_keys(
            "lesavka-input-init-kbd",
            &[evdev::KeyCode::KEY_A, evdev::KeyCode::KEY_ENTER],
        ) else {
            return;
        };
        let Some((_mouse_vdev, mouse_path, _mouse_dev)) =
            build_mouse_pair_with_path("lesavka-input-init-mouse")
        else {
            return;
        };

        let kbd_path = kbd_path.to_string_lossy().into_owned();
        let mouse_path = mouse_path.to_string_lossy().into_owned();
        with_vars(
            [
                ("LESAVKA_KEYBOARD_DEVICE", Some(kbd_path.as_str())),
                ("LESAVKA_MOUSE_DEVICE", Some(mouse_path.as_str())),
            ],
            || {
                let mut agg = new_aggregator();
                let result = agg.init();
                assert!(
                    result.is_ok(),
                    "init should succeed with selected virtual input devices"
                );
                assert!(
                    !agg.keyboards.is_empty() || !agg.mice.is_empty(),
                    "init should discover at least one selected virtual input device"
                );
            },
        );
    }

    #[tokio::test(flavor = "current_thread")]
    async fn run_returns_once_pending_kill_chord_is_released() {
        let mut agg = new_aggregator();
        agg.pending_kill = true;

        let result = tokio::time::timeout(Duration::from_millis(200), agg.run()).await;
        assert!(
            result.is_ok(),
            "run should resolve instead of looping forever"
        );
        assert!(result.expect("timeout result").is_ok());
        assert!(agg.released);
    }

    #[tokio::test(flavor = "current_thread")]
    async fn run_releases_pending_kill_when_captured_keys_are_not_pressed() {
        let mut agg = new_aggregator();
        agg.pending_kill = true;
        agg.pending_keys.insert(evdev::KeyCode::KEY_A);

        let result = tokio::time::timeout(Duration::from_millis(200), agg.run()).await;
        assert!(
            result.is_ok(),
            "run should resolve when pending keys are released"
        );
        assert!(result.expect("timeout result").is_ok());
        assert!(agg.released);
    }

    #[test]
    #[serial]
    fn toggle_grab_updates_attached_keyboard_and_mouse_modes() {
        let Some((_kbd_vdev, kbd_dev)) = build_keyboard_pair("lesavka-input-toggle-kbd") else {
            return;
        };
        let Some((_mouse_vdev, mouse_dev)) = build_mouse_pair("lesavka-input-toggle-mouse") else {
            return;
        };

        let (kbd_tx, _) = tokio::sync::broadcast::channel(16);
        let (mou_tx, _) = tokio::sync::broadcast::channel(16);

        let keyboard = KeyboardAggregator::new(kbd_dev, false, kbd_tx.clone(), None);
        let mouse = MouseAggregator::new(mouse_dev, false, mou_tx.clone());

        let mut agg = InputAggregator::new(false, kbd_tx, mou_tx, None);
        agg.keyboards.push(keyboard);
        agg.mice.push(mouse);

        agg.toggle_grab();
        assert!(
            agg.pending_release,
            "toggle should enter pending-release mode"
        );
        assert!(!agg.released);

        agg.released = true;
        agg.pending_release = false;
        agg.toggle_grab();
        assert!(
            !agg.pending_release,
            "remote-control toggle clears pending-release"
        );
        assert!(!agg.released, "remote-control toggle restores grabbed mode");
    }

    #[tokio::test(flavor = "current_thread")]
    #[serial]
    async fn run_pending_release_branch_resets_attached_devices() {
        let Some((_kbd_vdev, kbd_dev)) = build_keyboard_pair("lesavka-input-run-release-kbd")
        else {
            return;
        };
        let Some((_mouse_vdev, mouse_dev)) = build_mouse_pair("lesavka-input-run-release-mouse")
        else {
            return;
        };

        let (kbd_tx, _) = tokio::sync::broadcast::channel(16);
        let (mou_tx, _) = tokio::sync::broadcast::channel(16);
        let keyboard = KeyboardAggregator::new(kbd_dev, false, kbd_tx.clone(), None);
        let mouse = MouseAggregator::new(mouse_dev, false, mou_tx.clone());

        let mut agg = InputAggregator::new(false, kbd_tx, mou_tx, None);
        agg.keyboards.push(keyboard);
        agg.mice.push(mouse);
        agg.pending_release = true;

        let result = tokio::time::timeout(Duration::from_millis(120), agg.run()).await;
        assert!(
            result.is_err(),
            "run should continue looping after release handling"
        );
        assert!(
            agg.released,
            "pending-release flow should mark local control as released"
        );
        assert!(
            !agg.pending_release,
            "pending-release flow should clear pending flag"
        );
    }
}
