// Routing, swap-key, and failsafe coverage for client input aggregation.
//
// Scope: include the input aggregator source and exercise local/remote routing
// behavior, quick-toggle handling, and opt-in remote failsafe behavior.
// Targets: `client/src/input/inputs.rs`.
// Why: swap-key and routing regressions can lock the operator out of local
// control, so these paths need dedicated contract coverage.

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;

    fn open_virtual_device(vdev: &mut VirtualDevice) -> Option<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(dev);
                    }
                }
            }
            thread::sleep(std::time::Duration::from_millis(10));
        }
        None
    }

    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])
    }

    fn build_keyboard_pair_with_keys(
        name: &str,
        supported_keys: &[evdev::KeyCode],
    ) -> Option<(VirtualDevice, 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 dev = open_virtual_device(&mut vdev)?;
        Some((vdev, dev))
    }

    fn build_mouse_pair(name: &str) -> Option<(VirtualDevice, 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 dev = open_virtual_device(&mut vdev)?;
        Some((vdev, 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]
    fn quick_toggle_key_parser_handles_supported_aliases_and_disable_switch() {
        assert_eq!(
            parse_quick_toggle_key("scrolllock"),
            Some(evdev::KeyCode::KEY_SCROLLLOCK)
        );
        assert_eq!(
            parse_quick_toggle_key("pause"),
            Some(evdev::KeyCode::KEY_PAUSE)
        );
        assert_eq!(
            parse_quick_toggle_key("sysrq"),
            Some(evdev::KeyCode::KEY_SYSRQ)
        );
        assert_eq!(parse_quick_toggle_key("f12"), Some(evdev::KeyCode::KEY_F12));
        assert_eq!(parse_quick_toggle_key("off"), None);
        assert_eq!(parse_quick_toggle_key("none"), None);
        assert_eq!(
            parse_quick_toggle_key("definitely-unknown"),
            Some(evdev::KeyCode::KEY_PAUSE)
        );
    }

    #[test]
    fn wake_interval_parser_accepts_launcher_dropdown_values_only() {
        assert_eq!(parse_wake_interval("off 123"), None);
        assert_eq!(parse_wake_interval("0 123"), None);
        assert_eq!(
            parse_wake_interval("5 123"),
            Some(std::time::Duration::from_secs(5 * 60))
        );
        assert_eq!(
            parse_wake_interval("10m 123"),
            Some(std::time::Duration::from_secs(10 * 60))
        );
        assert_eq!(
            parse_wake_interval("60 123"),
            Some(std::time::Duration::from_secs(60 * 60))
        );
        assert_eq!(parse_wake_interval("2 123"), None);
        assert_eq!(parse_wake_interval("banana"), None);
    }

    #[test]
    #[serial]
    fn quick_toggle_key_env_defaults_and_respects_explicit_disable() {
        with_var("LESAVKA_INPUT_TOGGLE_KEY", None::<&str>, || {
            assert_eq!(quick_toggle_key_from_env(), Some(evdev::KeyCode::KEY_PAUSE));
        });
        with_var("LESAVKA_INPUT_TOGGLE_KEY", Some("off"), || {
            assert_eq!(quick_toggle_key_from_env(), None);
        });
        with_var("LESAVKA_INPUT_TOGGLE_KEY", Some("f11"), || {
            assert_eq!(quick_toggle_key_from_env(), Some(evdev::KeyCode::KEY_F11));
        });
    }

    #[test]
    #[serial]
    fn quick_toggle_debounce_env_uses_defaults_and_applies_safety_floor() {
        with_var("LESAVKA_INPUT_TOGGLE_DEBOUNCE_MS", None::<&str>, || {
            assert_eq!(quick_toggle_debounce_from_env(), Duration::from_millis(350));
        });
        with_var("LESAVKA_INPUT_TOGGLE_DEBOUNCE_MS", Some("20"), || {
            assert_eq!(quick_toggle_debounce_from_env(), Duration::from_millis(50));
        });
        with_var("LESAVKA_INPUT_TOGGLE_DEBOUNCE_MS", Some("900"), || {
            assert_eq!(quick_toggle_debounce_from_env(), Duration::from_millis(900));
        });
    }

    #[test]
    #[serial]
    fn release_timeout_env_uses_default_and_safety_floor_for_bad_values() {
        with_var("LESAVKA_INPUT_RELEASE_TIMEOUT_MS", None::<&str>, || {
            assert_eq!(
                pending_release_timeout_from_env(),
                Duration::from_millis(750)
            );
        });
        with_var("LESAVKA_INPUT_RELEASE_TIMEOUT_MS", Some("bad"), || {
            assert_eq!(
                pending_release_timeout_from_env(),
                Duration::from_millis(750)
            );
        });
        with_var("LESAVKA_INPUT_RELEASE_TIMEOUT_MS", Some("25"), || {
            assert_eq!(
                pending_release_timeout_from_env(),
                Duration::from_millis(100)
            );
        });
    }

    #[test]
    #[serial]
    fn input_device_override_env_ignores_blank_and_all_values() {
        with_var("LESAVKA_KEYBOARD_DEVICE", Some("all"), || {
            assert_eq!(
                input_device_override_from_env("LESAVKA_KEYBOARD_DEVICE"),
                None
            );
        });
        with_var("LESAVKA_KEYBOARD_DEVICE", Some("   "), || {
            assert_eq!(
                input_device_override_from_env("LESAVKA_KEYBOARD_DEVICE"),
                None
            );
        });
        with_var("LESAVKA_KEYBOARD_DEVICE", Some("/dev/input/event7"), || {
            assert_eq!(
                input_device_override_from_env("LESAVKA_KEYBOARD_DEVICE"),
                Some("/dev/input/event7".to_string())
            );
        });

        let path = std::path::Path::new("/dev/input/event7");
        assert!(matches_selected_input_device(path, None));
        assert!(matches_selected_input_device(
            path,
            Some("/dev/input/event7")
        ));
        assert!(!matches_selected_input_device(
            path,
            Some("/dev/input/event8")
        ));
    }

    #[test]
    #[serial]
    fn remote_failsafe_timeout_env_is_opt_in_and_allows_disable() {
        with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_SECS", None::<&str>, || {
            with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", None::<&str>, || {
                assert_eq!(remote_failsafe_timeout_from_env(), Duration::from_millis(0));
            });
            with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", Some("0"), || {
                assert_eq!(remote_failsafe_timeout_from_env(), Duration::from_millis(0));
            });
            with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", Some("60000"), || {
                assert_eq!(
                    remote_failsafe_timeout_from_env(),
                    Duration::from_millis(60_000)
                );
            });
            with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", Some("1500"), || {
                assert_eq!(
                    remote_failsafe_timeout_from_env(),
                    Duration::from_millis(1_500)
                );
            });
        });
        with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_SECS", Some("0"), || {
            with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", Some("60000"), || {
                assert_eq!(remote_failsafe_timeout_from_env(), Duration::ZERO);
            });
        });
        with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_SECS", Some("60"), || {
            assert_eq!(remote_failsafe_timeout_from_env(), Duration::from_secs(60));
        });
        with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_SECS", Some("bad"), || {
            with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", Some("123"), || {
                assert_eq!(
                    remote_failsafe_timeout_from_env(),
                    Duration::from_millis(123)
                );
            });
        });
        with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_SECS", Some("bad"), || {
            with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", Some("bad"), || {
                assert_eq!(remote_failsafe_timeout_from_env(), Duration::ZERO);
            });
        });
    }

    #[test]
    #[serial]
    fn boot_remote_capture_only_arms_failsafe_when_launch_option_is_nonzero() {
        with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_SECS", Some("0"), || {
            let agg = new_aggregator();
            assert_eq!(agg.remote_failsafe_timeout, Duration::ZERO);
            assert!(agg.remote_failsafe_started_at.is_none());
        });
        with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_SECS", Some("60"), || {
            let agg = new_aggregator();
            assert_eq!(agg.remote_failsafe_timeout, Duration::from_secs(60));
            assert!(agg.remote_failsafe_started_at.is_some());
        });
    }

    #[test]
    fn enable_remote_capture_arms_failsafe_and_local_release_clears_it() {
        let mut agg = new_aggregator();
        let remote_capture_enabled = agg.remote_capture_enabled_handle();
        agg.released = true;
        agg.pending_release = false;
        agg.remote_failsafe_timeout = Duration::from_millis(5_000);

        agg.enable_remote_capture();
        assert!(remote_capture_enabled.load(Ordering::Relaxed));
        assert!(agg.remote_capture_active());
        assert!(
            agg.remote_failsafe_started_at.is_some(),
            "remote capture should arm the temporary failsafe window"
        );

        agg.begin_local_release();
        assert!(!agg.remote_capture_active());
        assert!(
            agg.remote_failsafe_started_at.is_none(),
            "returning control locally should clear the failsafe timer"
        );
    }

    #[test]
    fn begin_local_release_publishes_local_before_ungrab_finishes() {
        let dir = tempfile::tempdir().expect("tempdir");
        let state_path = dir.path().join("routing-state");
        let mut agg = new_aggregator();
        agg.routing_state_path = Some(state_path.clone());

        agg.publish_routing_state_if_changed();
        assert_eq!(
            std::fs::read_to_string(&state_path).expect("initial routing state"),
            "remote\n"
        );

        agg.begin_local_release();

        assert!(agg.pending_release);
        assert!(!agg.remote_capture_active());
        assert_eq!(
            std::fs::read_to_string(&state_path).expect("local routing state"),
            "local\n",
            "launcher polling must see local as soon as remote sends are disabled, not only after final ungrab"
        );
    }

    #[test]
    fn local_release_timeout_helpers_are_stable() {
        let mut agg = new_aggregator();
        assert!(!agg.pending_release_timed_out());

        agg.pending_release = true;
        agg.pending_release_timeout = Duration::from_millis(1);
        agg.pending_release_started_at = Some(Instant::now() - Duration::from_millis(5));
        assert!(agg.pending_release_timed_out());
    }

    #[test]
    fn enable_remote_capture_does_not_auto_cutoff_when_failsafe_is_disabled() {
        let mut agg = new_aggregator();
        agg.released = true;
        agg.pending_release = false;
        agg.remote_failsafe_timeout = Duration::ZERO;

        agg.enable_remote_capture();
        assert!(
            agg.remote_failsafe_started_at.is_none(),
            "normal remote input sessions should not silently flip back to local"
        );
        assert!(agg.remote_capture_enabled.load(Ordering::Relaxed));
        assert!(!agg.released);
    }

    #[tokio::test(flavor = "current_thread")]
    async fn run_remote_failsafe_returns_control_to_local_machine() {
        let mut agg = new_aggregator();
        agg.remote_failsafe_timeout = Duration::from_millis(1);
        agg.remote_failsafe_started_at =
            Some(std::time::Instant::now() - Duration::from_millis(10));

        let result = tokio::time::timeout(Duration::from_millis(120), agg.run()).await;
        assert!(
            result.is_err(),
            "run should keep looping after the failsafe returns control locally"
        );
        assert!(
            agg.released,
            "failsafe expiry should release devices back to the local machine"
        );
        assert!(
            !agg.pending_release,
            "failsafe expiry should complete the local-release handoff"
        );
        assert!(
            agg.remote_failsafe_started_at.is_none(),
            "failsafe timer should clear once local control is restored"
        );
    }

    #[test]
    #[serial]
    fn quick_toggle_tap_flips_routing_when_processed_through_input_aggregator() {
        let Some((mut vdev, dev)) = build_keyboard_pair_with_keys(
            "lesavka-input-toggle-pause",
            &[
                evdev::KeyCode::KEY_A,
                evdev::KeyCode::KEY_ENTER,
                evdev::KeyCode::KEY_PAUSE,
            ],
        ) else {
            return;
        };

        let (kbd_tx, _) = tokio::sync::broadcast::channel(16);
        let (mou_tx, _) = tokio::sync::broadcast::channel(16);
        let keyboard = KeyboardAggregator::new(dev, false, kbd_tx.clone(), None);
        let mut agg = InputAggregator::new(false, kbd_tx, mou_tx, None);
        agg.quick_toggle_key = Some(evdev::KeyCode::KEY_PAUSE);
        agg.quick_toggle_debounce = Duration::from_millis(0);
        agg.keyboards.push(keyboard);

        vdev.emit(&[
            evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::KEY_PAUSE.0, 1),
            evdev::InputEvent::new(evdev::EventType::KEY.0, evdev::KeyCode::KEY_PAUSE.0, 0),
        ])
        .expect("emit pause tap");
        thread::sleep(std::time::Duration::from_millis(20));

        agg.process_keyboard_updates();
        let quick_toggle_now = agg.quick_toggle_active();
        agg.observe_quick_toggle(quick_toggle_now);

        assert!(
            agg.pending_release,
            "a quick swap-key tap should start the local handoff path"
        );
        assert!(
            !agg.released,
            "the relay should still be in pending-release until the local handoff completes"
        );
    }
}
