// Integration coverage for client mouse aggregator internals.
//
// Scope: include mouse input source and validate relative/absolute event
// handling, flush behavior, and threshold helpers.
// Targets: `client/src/input/mouse.rs`.
// Why: mouse state transitions are regress-prone and must remain deterministic
// under synthetic device traffic.

#[allow(warnings)]
mod mouse_contract {
    include!(env!("LESAVKA_CLIENT_MOUSE_SRC"));

    use serial_test::serial;
    use std::path::PathBuf;
    use std::thread;

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

    fn open_virtual_device(vdev: &mut evdev::uinput::VirtualDevice) -> Option<evdev::Device> {
        let node = open_virtual_node(vdev)?;
        let dev = evdev::Device::open(node).ok()?;
        dev.set_nonblocking(true).ok()?;
        Some(dev)
    }

    fn open_any_mouse_device() -> Option<evdev::Device> {
        let entries = std::fs::read_dir("/dev/input").ok()?;
        for entry in entries.flatten() {
            let path = entry.path();
            let name = path.file_name()?.to_string_lossy();
            if !name.starts_with("event") {
                continue;
            }
            let dev = evdev::Device::open(path).ok()?;
            let _ = dev.set_nonblocking(true);
            let rel_mouse = dev
                .supported_relative_axes()
                .map(|axes| {
                    axes.contains(evdev::RelativeAxisCode::REL_X)
                        && axes.contains(evdev::RelativeAxisCode::REL_Y)
                })
                .unwrap_or(false)
                && dev
                    .supported_keys()
                    .map(|keys| {
                        keys.contains(evdev::KeyCode::BTN_LEFT)
                            || keys.contains(evdev::KeyCode::BTN_RIGHT)
                    })
                    .unwrap_or(false);
            let abs_touch = dev
                .supported_absolute_axes()
                .map(|axes| {
                    axes.contains(evdev::AbsoluteAxisCode::ABS_X)
                        || axes.contains(evdev::AbsoluteAxisCode::ABS_MT_POSITION_X)
                })
                .unwrap_or(false);
            if rel_mouse || abs_touch {
                return Some(dev);
            }
        }
        None
    }

    fn build_relative_mouse(name: &str) -> Option<(evdev::uinput::VirtualDevice, evdev::Device)> {
        let mut keys = evdev::AttributeSet::<evdev::KeyCode>::new();
        keys.insert(evdev::KeyCode::BTN_LEFT);
        keys.insert(evdev::KeyCode::BTN_RIGHT);
        keys.insert(evdev::KeyCode::BTN_MIDDLE);

        let mut rel = evdev::AttributeSet::<evdev::RelativeAxisCode>::new();
        rel.insert(evdev::RelativeAxisCode::REL_X);
        rel.insert(evdev::RelativeAxisCode::REL_Y);
        rel.insert(evdev::RelativeAxisCode::REL_WHEEL);

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

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

    fn build_touch_device(name: &str) -> Option<(evdev::uinput::VirtualDevice, evdev::Device)> {
        let mut keys = evdev::AttributeSet::<evdev::KeyCode>::new();
        keys.insert(evdev::KeyCode::BTN_TOUCH);

        let abs = evdev::AbsInfo::new(0, 0, 1000, 0, 0, 0);
        let mut vdev = evdev::uinput::VirtualDevice::builder()
            .ok()?
            .name(name)
            .with_keys(&keys)
            .ok()?
            .with_absolute_axis(&evdev::UinputAbsSetup::new(
                evdev::AbsoluteAxisCode::ABS_X,
                abs,
            ))
            .ok()?
            .with_absolute_axis(&evdev::UinputAbsSetup::new(
                evdev::AbsoluteAxisCode::ABS_Y,
                abs,
            ))
            .ok()?
            .with_absolute_axis(&evdev::UinputAbsSetup::new(
                evdev::AbsoluteAxisCode::ABS_MT_TRACKING_ID,
                evdev::AbsInfo::new(0, -1, 10, 0, 0, 0),
            ))
            .ok()?
            .build()
            .ok()?;

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

    fn build_absolute_mouse_without_touch(
        name: &str,
    ) -> Option<(evdev::uinput::VirtualDevice, evdev::Device)> {
        let mut keys = evdev::AttributeSet::<evdev::KeyCode>::new();
        keys.insert(evdev::KeyCode::BTN_LEFT);
        let abs = evdev::AbsInfo::new(0, 0, 1000, 0, 0, 0);
        let mut vdev = evdev::uinput::VirtualDevice::builder()
            .ok()?
            .name(name)
            .with_keys(&keys)
            .ok()?
            .with_absolute_axis(&evdev::UinputAbsSetup::new(
                evdev::AbsoluteAxisCode::ABS_X,
                abs,
            ))
            .ok()?
            .with_absolute_axis(&evdev::UinputAbsSetup::new(
                evdev::AbsoluteAxisCode::ABS_Y,
                abs,
            ))
            .ok()?
            .build()
            .ok()?;
        let dev = open_virtual_device(&mut vdev)?;
        Some((vdev, dev))
    }

    #[test]
    #[serial]
    fn relative_events_emit_button_motion_and_wheel_packets() {
        let Some((mut vdev, dev)) = build_relative_mouse("lesavka-include-mouse-rel") else {
            return;
        };

        let (tx, mut rx) = tokio::sync::broadcast::channel(32);
        let mut agg = MouseAggregator::new(dev, true, tx);
        agg.set_grab(false);
        agg.set_send(true);

        vdev.emit(&[
            evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::BTN_LEFT.0, 1),
            evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::BTN_RIGHT.0, 1),
            evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::BTN_MIDDLE.0, 1),
            evdev::InputEvent::new(
                evdev::EventType::RELATIVE.0,
                evdev::RelativeAxisCode::REL_X.0,
                11,
            ),
            evdev::InputEvent::new(
                evdev::EventType::RELATIVE.0,
                evdev::RelativeAxisCode::REL_Y.0,
                -7,
            ),
            evdev::InputEvent::new(
                evdev::EventType::RELATIVE.0,
                evdev::RelativeAxisCode::REL_WHEEL.0,
                1,
            ),
        ])
        .expect("emit relative frame");

        thread::sleep(std::time::Duration::from_millis(20));
        agg.process_events();

        let pkt = rx.try_recv().expect("mouse packet");
        assert_eq!(pkt.data[0] & 0b0000_0111, 0b0000_0111);
        assert_eq!(pkt.data[1], 11);
        assert_eq!(pkt.data[2], (-7_i8) as u8);
        assert_eq!(pkt.data[3], 1);
    }

    #[test]
    #[serial]
    fn touch_tracking_updates_touch_state_and_clears_abs_origins_on_release() {
        let Some((mut vdev, dev)) = build_touch_device("lesavka-include-mouse-touch") else {
            return;
        };

        let (tx, _rx) = tokio::sync::broadcast::channel(8);
        let mut agg = MouseAggregator::new(dev, true, tx);

        vdev.emit(&[
            evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::BTN_TOUCH.0, 1),
            evdev::InputEvent::new(
                evdev::EventType::ABSOLUTE.0,
                evdev::AbsoluteAxisCode::ABS_X.0,
                100,
            ),
            evdev::InputEvent::new(
                evdev::EventType::ABSOLUTE.0,
                evdev::AbsoluteAxisCode::ABS_Y.0,
                120,
            ),
            evdev::InputEvent::new(
                evdev::EventType::ABSOLUTE.0,
                evdev::AbsoluteAxisCode::ABS_MT_TRACKING_ID.0,
                1,
            ),
        ])
        .expect("emit touch start");
        thread::sleep(std::time::Duration::from_millis(20));
        agg.process_events();

        assert!(agg.touch_guarded);
        assert!(agg.touch_active);

        vdev.emit(&[evdev::InputEvent::new(
            evdev::EventType::ABSOLUTE.0,
            evdev::AbsoluteAxisCode::ABS_MT_TRACKING_ID.0,
            -1,
        )])
        .expect("emit touch end");
        thread::sleep(std::time::Duration::from_millis(20));
        agg.process_events();

        assert!(agg.touch_guarded);
        assert!(!agg.touch_active);
        assert!(agg.last_abs_x.is_none());
        assert!(agg.last_abs_y.is_none());
    }

    #[test]
    #[serial]
    fn flush_and_reset_state_honor_send_toggle_and_clear_accumulators() {
        let Some(dev) = open_any_mouse_device()
            .or_else(|| build_relative_mouse("lesavka-include-mouse-flush").map(|(_, dev)| dev))
        else {
            return;
        };

        let (tx, mut rx) = tokio::sync::broadcast::channel(32);
        let mut agg = MouseAggregator::new(dev, false, tx);

        agg.buttons = 1;
        agg.last_buttons = 0;
        agg.dx = 7;
        agg.dy = -3;
        agg.wheel = 1;
        agg.next_send = std::time::Instant::now() - std::time::Duration::from_millis(10);
        agg.set_send(false);
        agg.flush();
        assert!(
            rx.try_recv().is_err(),
            "send-disabled flush should not emit"
        );

        agg.buttons = 2;
        agg.last_buttons = 0;
        agg.dx = 4;
        agg.dy = -2;
        agg.wheel = -1;
        agg.next_send = std::time::Instant::now() - std::time::Duration::from_millis(10);
        agg.set_send(true);
        agg.flush();

        let pkt = rx.try_recv().expect("flush packet");
        assert_eq!(pkt.data[0], 2);
        assert_eq!(pkt.data[1], 4);
        assert_eq!(pkt.data[2], (-2_i8) as u8);
        assert_eq!(pkt.data[3], (-1_i8) as u8);
        assert_eq!(agg.dx, 0);
        assert_eq!(agg.dy, 0);
        assert_eq!(agg.wheel, 0);

        agg.reset_state();
        let reset_pkt = rx.try_recv().expect("reset packet");
        assert_eq!(reset_pkt.data, vec![0, 0, 0, 0]);
    }

    #[test]
    #[serial]
    fn abs_jump_threshold_uses_scale_based_minimum_without_absinfo() {
        let Some(dev) = open_any_mouse_device().or_else(|| {
            build_relative_mouse("lesavka-include-mouse-threshold").map(|(_, dev)| dev)
        }) else {
            return;
        };

        let threshold =
            MouseAggregator::abs_jump_threshold(&dev, &[evdev::AbsoluteAxisCode::ABS_X], 3);
        assert!(
            threshold >= 120,
            "threshold should honor scale-derived minimum"
        );
    }

    #[test]
    #[serial]
    fn set_btn_toggles_expected_button_bits() {
        let Some(dev) = open_any_mouse_device()
            .or_else(|| build_relative_mouse("lesavka-include-mouse-bits").map(|(_, dev)| dev))
        else {
            return;
        };

        let (tx, _rx) = tokio::sync::broadcast::channel(8);
        let mut agg = MouseAggregator::new(dev, false, tx);
        agg.set_btn(0, 1);
        agg.set_btn(1, 1);
        assert_eq!(agg.buttons & 0b11, 0b11);

        agg.set_btn(0, 0);
        assert_eq!(agg.buttons & 0b11, 0b10);
    }

    #[test]
    #[serial]
    fn absolute_motion_ignores_large_jumps_without_touch_state() {
        let Some((mut vdev, dev)) = build_absolute_mouse_without_touch("lesavka-include-abs-jump")
        else {
            return;
        };
        let (tx, _rx) = tokio::sync::broadcast::channel(8);
        let mut agg = MouseAggregator::new(dev, false, tx);

        agg.last_abs_x = Some(0);
        agg.last_abs_y = Some(0);
        assert!(!agg.has_touch_state);

        vdev.emit(&[
            evdev::InputEvent::new(
                evdev::EventType::ABSOLUTE.0,
                evdev::AbsoluteAxisCode::ABS_X.0,
                900,
            ),
            evdev::InputEvent::new(
                evdev::EventType::ABSOLUTE.0,
                evdev::AbsoluteAxisCode::ABS_Y.0,
                900,
            ),
        ])
        .expect("emit large abs jump");

        thread::sleep(std::time::Duration::from_millis(20));
        agg.process_events();

        assert_eq!(agg.dx, 0);
        assert_eq!(agg.dy, 0);
        assert_eq!(agg.last_abs_x, Some(900));
        assert_eq!(agg.last_abs_y, Some(900));
    }

    #[test]
    #[serial]
    fn absolute_motion_applies_scaled_delta_within_threshold() {
        let Some((mut vdev, dev)) = build_absolute_mouse_without_touch("lesavka-include-abs-delta")
        else {
            return;
        };
        let (tx, _rx) = tokio::sync::broadcast::channel(8);
        let mut agg = MouseAggregator::new(dev, false, tx);
        agg.last_abs_x = Some(100);
        agg.last_abs_y = Some(100);

        vdev.emit(&[
            evdev::InputEvent::new(
                evdev::EventType::ABSOLUTE.0,
                evdev::AbsoluteAxisCode::ABS_X.0,
                140,
            ),
            evdev::InputEvent::new(
                evdev::EventType::ABSOLUTE.0,
                evdev::AbsoluteAxisCode::ABS_Y.0,
                180,
            ),
        ])
        .expect("emit small abs delta");

        thread::sleep(std::time::Duration::from_millis(20));
        agg.process_events();

        assert_ne!(agg.dx, 0);
        assert_ne!(agg.dy, 0);
    }

    #[test]
    #[serial]
    fn touch_guarded_inactive_abs_events_only_update_origins() {
        let Some((mut vdev, dev)) = build_touch_device("lesavka-include-touch-guarded") else {
            return;
        };

        let (tx, _rx) = tokio::sync::broadcast::channel(8);
        let mut agg = MouseAggregator::new(dev, false, tx);
        agg.touch_guarded = true;
        agg.touch_active = false;
        agg.last_abs_x = Some(10);
        agg.last_abs_y = Some(10);

        vdev.emit(&[
            evdev::InputEvent::new(
                evdev::EventType::ABSOLUTE.0,
                evdev::AbsoluteAxisCode::ABS_X.0,
                50,
            ),
            evdev::InputEvent::new(
                evdev::EventType::ABSOLUTE.0,
                evdev::AbsoluteAxisCode::ABS_Y.0,
                70,
            ),
        ])
        .expect("emit guarded abs updates");

        thread::sleep(std::time::Duration::from_millis(20));
        agg.process_events();

        assert_eq!(agg.dx, 0);
        assert_eq!(agg.dy, 0);
        assert_eq!(agg.last_abs_x, Some(50));
        assert_eq!(agg.last_abs_y, Some(70));
    }

    #[test]
    #[serial]
    fn flush_skips_when_state_unchanged_before_interval_deadline() {
        let Some(dev) = open_any_mouse_device()
            .or_else(|| build_relative_mouse("lesavka-include-mouse-skip").map(|(_, dev)| dev))
        else {
            return;
        };
        let (tx, mut rx) = tokio::sync::broadcast::channel(8);
        let mut agg = MouseAggregator::new(dev, false, tx);
        agg.buttons = 1;
        agg.last_buttons = 1;
        agg.next_send = std::time::Instant::now() + std::time::Duration::from_millis(50);

        agg.flush();
        assert!(rx.try_recv().is_err());
    }

    #[test]
    #[serial]
    fn drop_emits_shutdown_packet() {
        let Some(dev) = open_any_mouse_device()
            .or_else(|| build_relative_mouse("lesavka-include-mouse-drop").map(|(_, dev)| dev))
        else {
            return;
        };
        let (tx, mut rx) = tokio::sync::broadcast::channel(8);
        let agg = MouseAggregator::new(dev, false, tx);
        drop(agg);
        let pkt = rx.try_recv().expect("drop packet");
        assert_eq!(pkt.data, vec![0; 4]);
    }
}
