// Integration coverage for client keyboard aggregator internals.
//
// Scope: include keyboard input source and validate report shaping, magic
// chords, paste handling, and event processing against synthetic keyboards.
// Targets: `client/src/input/keyboard.rs`.
// Why: keyboard chord and paste logic is stateful and needs direct branch
// coverage to avoid regressions.

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

#[allow(warnings)]
mod keyboard_contract {
    include!(env!("LESAVKA_CLIENT_KEYBOARD_SRC"));

    use serial_test::serial;
    use std::fs;
    use std::os::unix::fs::PermissionsExt;
    use std::path::Path;
    use std::thread;
    use temp_env::with_var;
    use tempfile::tempdir;

    fn write_executable(dir: &Path, name: &str, body: &str) {
        let path = dir.join(name);
        fs::write(&path, body).expect("write script");
        let mut perms = fs::metadata(&path).expect("metadata").permissions();
        perms.set_mode(0o755);
        fs::set_permissions(path, perms).expect("chmod");
    }

    fn with_fake_path_command(name: &str, script_body: &str, f: impl FnOnce()) {
        let dir = tempdir().expect("tempdir");
        write_executable(dir.path(), name, script_body);
        let prior = std::env::var("PATH").unwrap_or_default();
        let merged = if prior.is_empty() {
            dir.path().display().to_string()
        } else {
            format!("{}:{prior}", dir.path().display())
        };
        with_var("PATH", Some(merged), f);
    }

    fn open_virtual_device(vdev: &mut evdev::uinput::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() {
                    let dev = evdev::Device::open(path).ok()?;
                    dev.set_nonblocking(true).ok()?;
                    return Some(dev);
                }
            }
            thread::sleep(std::time::Duration::from_millis(10));
        }
        None
    }

    fn open_any_keyboard_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 looks_like_keyboard = dev
                .supported_keys()
                .map(|keys| {
                    keys.contains(evdev::KeyCode::KEY_A)
                        && keys.contains(evdev::KeyCode::KEY_ENTER)
                        && keys.contains(evdev::KeyCode::KEY_LEFTCTRL)
                })
                .unwrap_or(false);
            if looks_like_keyboard {
                return Some(dev);
            }
        }
        None
    }

    fn build_keyboard(name: &str) -> Option<(evdev::uinput::VirtualDevice, evdev::Device)> {
        let mut keys = evdev::AttributeSet::<evdev::KeyCode>::new();
        keys.insert(evdev::KeyCode::KEY_A);
        keys.insert(evdev::KeyCode::KEY_B);
        keys.insert(evdev::KeyCode::KEY_C);
        keys.insert(evdev::KeyCode::KEY_D);
        keys.insert(evdev::KeyCode::KEY_E);
        keys.insert(evdev::KeyCode::KEY_F);
        keys.insert(evdev::KeyCode::KEY_G);
        keys.insert(evdev::KeyCode::KEY_V);
        keys.insert(evdev::KeyCode::KEY_LEFTCTRL);
        keys.insert(evdev::KeyCode::KEY_LEFTALT);
        keys.insert(evdev::KeyCode::KEY_LEFTSHIFT);
        keys.insert(evdev::KeyCode::KEY_ESC);
        keys.insert(evdev::KeyCode::KEY_LEFT);
        keys.insert(evdev::KeyCode::KEY_RIGHT);

        let mut vdev = evdev::uinput::VirtualDevice::builder()
            .ok()?
            .name(name)
            .with_keys(&keys)
            .ok()?
            .build()
            .ok()?;
        let dev = open_virtual_device(&mut vdev)?;
        Some((vdev, dev))
    }

    fn new_aggregator(
        dev: evdev::Device,
    ) -> (
        KeyboardAggregator,
        tokio::sync::broadcast::Receiver<KeyboardReport>,
    ) {
        let (tx, rx) = tokio::sync::broadcast::channel(128);
        (KeyboardAggregator::new(dev, true, tx, None), rx)
    }

    #[test]
    #[serial]
    fn process_events_emits_press_and_release_reports() {
        let Some((mut vdev, dev)) = build_keyboard("lesavka-include-kbd-events") else {
            return;
        };
        let (mut agg, mut rx) = new_aggregator(dev);

        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));
        agg.process_events();
        let press = rx.try_recv().expect("press report");
        assert_ne!(press.data[2], 0);

        vdev.emit(&[evdev::InputEvent::new(
            evdev::EventType::KEY.0,
            evdev::KeyCode::KEY_A.0,
            0,
        )])
        .expect("emit key release");
        thread::sleep(std::time::Duration::from_millis(20));
        agg.process_events();
        let release = rx.try_recv().expect("release report");
        assert_eq!(release.data[2], 0);
    }

    #[test]
    #[serial]
    fn build_report_sets_modifiers_and_limits_to_six_keys() {
        let Some(dev) = open_any_keyboard_device()
            .or_else(|| build_keyboard("lesavka-include-kbd-report").map(|(_, dev)| dev))
        else {
            return;
        };
        let (mut agg, _) = new_aggregator(dev);

        agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTCTRL);
        for key in [
            evdev::KeyCode::KEY_A,
            evdev::KeyCode::KEY_B,
            evdev::KeyCode::KEY_C,
            evdev::KeyCode::KEY_D,
            evdev::KeyCode::KEY_E,
            evdev::KeyCode::KEY_F,
            evdev::KeyCode::KEY_G,
        ] {
            agg.pressed_keys.insert(key);
        }

        let report = agg.build_report();
        assert_ne!(report[0], 0);
        assert!(report[2..].iter().filter(|value| **value != 0).count() <= 6);
    }

    #[test]
    #[serial]
    fn magic_chords_track_expected_combinations() {
        let Some(dev) = open_any_keyboard_device()
            .or_else(|| build_keyboard("lesavka-include-kbd-magic").map(|(_, dev)| dev))
        else {
            return;
        };
        let (mut agg, _) = new_aggregator(dev);

        agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTCTRL);
        agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTSHIFT);
        agg.pressed_keys.insert(evdev::KeyCode::KEY_G);
        assert!(agg.magic_grab());

        agg.pressed_keys.remove(&evdev::KeyCode::KEY_G);
        agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFT);
        assert!(agg.magic_left());

        agg.pressed_keys.remove(&evdev::KeyCode::KEY_LEFT);
        agg.pressed_keys.insert(evdev::KeyCode::KEY_RIGHT);
        assert!(agg.magic_right());

        agg.pressed_keys.clear();
        agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTCTRL);
        agg.pressed_keys.insert(evdev::KeyCode::KEY_ESC);
        assert!(agg.magic_kill());
    }

    #[test]
    fn paste_rpc_enabled_contract_requires_flag_and_key() {
        assert!(!paste_rpc_enabled(false, true));
        assert!(!paste_rpc_enabled(true, false));
        assert!(paste_rpc_enabled(true, true));
        assert!(is_paste_modifier(evdev::KeyCode::KEY_LEFTCTRL));
        assert!(is_paste_modifier(evdev::KeyCode::KEY_RIGHTALT));
        assert!(!is_paste_modifier(evdev::KeyCode::KEY_A));
    }

    #[test]
    #[serial]
    fn try_handle_paste_event_consumes_chord_and_sends_rpc_payload() {
        let Some(dev) = open_any_keyboard_device()
            .or_else(|| build_keyboard("lesavka-include-kbd-paste-rpc").map(|(_, dev)| dev))
        else {
            return;
        };
        let (paste_tx, mut rx_rpc) = tokio::sync::mpsc::unbounded_channel::<String>();
        let (kbd_tx, _rx) = tokio::sync::broadcast::channel(128);
        let mut agg = KeyboardAggregator::new(dev, true, kbd_tx, Some(paste_tx));
        agg.paste_enabled = true;
        agg.paste_rpc_enabled = true;

        agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTCTRL);
        agg.pressed_keys.insert(evdev::KeyCode::KEY_LEFTALT);
        agg.pressed_keys.insert(evdev::KeyCode::KEY_V);

        with_var(
            "LESAVKA_CLIPBOARD_CMD",
            Some("printf 'hello-from-clipboard'"),
            || {
                with_var("LESAVKA_CLIPBOARD_CHORD", Some("ctrl+alt+v"), || {
                    with_var("LESAVKA_CLIPBOARD_DEBOUNCE_MS", Some("0"), || {
                        assert!(agg.try_handle_paste_event(evdev::KeyCode::KEY_V, 1));
                    });
                });
            },
        );

        let payload: String = rx_rpc.try_recv().expect("rpc payload");
        assert!(payload.contains("hello-from-clipboard"));
        assert!(agg.paste_chord_consumed);

        assert!(agg.try_handle_paste_event(evdev::KeyCode::KEY_V, 0));
        assert!(!agg.paste_chord_consumed);
    }

    #[test]
    #[serial]
    fn paste_clipboard_emits_hid_reports_for_supported_chars() {
        let Some(dev) = open_any_keyboard_device()
            .or_else(|| build_keyboard("lesavka-include-kbd-paste-hid").map(|(_, dev)| dev))
        else {
            return;
        };
        let (mut agg, mut rx) = new_aggregator(dev);
        agg.paste_enabled = true;

        with_var("LESAVKA_CLIPBOARD_CMD", Some("printf 'ab'"), || {
            with_var("LESAVKA_CLIPBOARD_MAX", Some("8"), || {
                with_var("LESAVKA_CLIPBOARD_DELAY_MS", Some("0"), || {
                    agg.paste_clipboard();
                });
            });
        });

        let mut seen = 0usize;
        while rx.try_recv().is_ok() {
            seen += 1;
        }
        assert!(
            seen >= 2,
            "expected multiple key reports for pasted characters"
        );
    }

    #[test]
    #[serial]
    fn reset_state_clears_pressed_keys_and_emits_empty_report() {
        let Some(dev) = open_any_keyboard_device()
            .or_else(|| build_keyboard("lesavka-include-kbd-reset").map(|(_, dev)| dev))
        else {
            return;
        };
        let (mut agg, mut rx) = new_aggregator(dev);
        agg.pressed_keys.insert(evdev::KeyCode::KEY_A);
        agg.reset_state();
        assert!(agg.pressed_keys.is_empty());
        let pkt = rx.try_recv().expect("empty report after reset");
        assert_eq!(pkt.data, vec![0; 8]);
    }
}
