use gpui::{
    Animation, AnimationExt, Context, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext,
    KeybindingKeystroke, Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions,
};
use ui::{
    ActiveTheme as _, Color, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize,
    ParentElement as _, Render, Styled as _, Tooltip, Window, prelude::*,
};

actions!(
    keystroke_input,
    [
        /// Starts recording keystrokes
        StartRecording,
        /// Stops recording keystrokes
        StopRecording,
        /// Clears the recorded keystrokes
        ClearKeystrokes,
    ]
);

const KEY_CONTEXT_VALUE: &str = "KeystrokeInput";

const CLOSE_KEYSTROKE_CAPTURE_END_TIMEOUT: std::time::Duration =
    std::time::Duration::from_millis(300);

enum CloseKeystrokeResult {
    Partial,
    Close,
    None,
}

impl PartialEq for CloseKeystrokeResult {
    fn eq(&self, other: &Self) -> bool {
        matches!(
            (self, other),
            (CloseKeystrokeResult::Partial, CloseKeystrokeResult::Partial)
                | (CloseKeystrokeResult::Close, CloseKeystrokeResult::Close)
                | (CloseKeystrokeResult::None, CloseKeystrokeResult::None)
        )
    }
}

pub struct KeystrokeInput {
    keystrokes: Vec<KeybindingKeystroke>,
    placeholder_keystrokes: Option<Vec<KeybindingKeystroke>>,
    outer_focus_handle: FocusHandle,
    inner_focus_handle: FocusHandle,
    intercept_subscription: Option<Subscription>,
    _focus_subscriptions: [Subscription; 2],
    search: bool,
    /// The sequence of close keystrokes being typed
    close_keystrokes: Option<Vec<Keystroke>>,
    close_keystrokes_start: Option<usize>,
    previous_modifiers: Modifiers,
    /// In order to support inputting keystrokes that end with a prefix of the
    /// close keybind keystrokes, we clear the close keystroke capture info
    /// on a timeout after a close keystroke is pressed
    ///
    /// e.g. if close binding is `esc esc esc` and user wants to search for
    /// `ctrl-g esc`, after entering the `ctrl-g esc`, hitting `esc` twice would
    /// stop recording because of the sequence of three escapes making it
    /// impossible to search for anything ending in `esc`
    clear_close_keystrokes_timer: Option<Task<()>>,
    #[cfg(test)]
    recording: bool,
    pub actions_slot: Option<AnyElement>,
}

impl KeystrokeInput {
    const KEYSTROKE_COUNT_MAX: usize = 3;

    pub fn new(
        placeholder_keystrokes: Option<Vec<KeybindingKeystroke>>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Self {
        let outer_focus_handle = cx.focus_handle();
        let inner_focus_handle = cx.focus_handle();
        let _focus_subscriptions = [
            cx.on_focus_in(&inner_focus_handle, window, Self::on_inner_focus_in),
            cx.on_focus_out(&inner_focus_handle, window, Self::on_inner_focus_out),
        ];
        Self {
            keystrokes: Vec::new(),
            placeholder_keystrokes,
            inner_focus_handle,
            outer_focus_handle,
            intercept_subscription: None,
            _focus_subscriptions,
            search: false,
            close_keystrokes: None,
            close_keystrokes_start: None,
            previous_modifiers: Modifiers::default(),
            clear_close_keystrokes_timer: None,
            #[cfg(test)]
            recording: false,
            actions_slot: None,
        }
    }

    pub fn set_keystrokes(&mut self, keystrokes: Vec<KeybindingKeystroke>, cx: &mut Context<Self>) {
        self.keystrokes = keystrokes;
        self.keystrokes_changed(cx);
    }

    pub fn set_search(&mut self, search: bool) {
        self.search = search;
    }

    pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
        if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
            && self.keystrokes.is_empty()
        {
            return placeholders;
        }
        if !self.search
            && self
                .keystrokes
                .last()
                .is_some_and(|last| last.key().is_empty())
        {
            return &self.keystrokes[..self.keystrokes.len() - 1];
        }
        &self.keystrokes
    }

    fn dummy(modifiers: Modifiers) -> KeybindingKeystroke {
        KeybindingKeystroke::from_keystroke(Keystroke {
            modifiers,
            key: "".to_string(),
            key_char: None,
        })
    }

    fn keystrokes_changed(&self, cx: &mut Context<Self>) {
        cx.emit(());
        cx.notify();
    }

    fn key_context() -> KeyContext {
        let mut key_context = KeyContext::default();
        key_context.add(KEY_CONTEXT_VALUE);
        key_context
    }

    fn determine_stop_recording_binding(window: &mut Window) -> Option<gpui::KeyBinding> {
        if cfg!(test) {
            Some(gpui::KeyBinding::new(
                "escape escape escape",
                StopRecording,
                Some(KEY_CONTEXT_VALUE),
            ))
        } else {
            window.highest_precedence_binding_for_action_in_context(
                &StopRecording,
                Self::key_context(),
            )
        }
    }

    fn upsert_close_keystrokes_start(&mut self, start: usize, cx: &mut Context<Self>) {
        if self.close_keystrokes_start.is_some() {
            return;
        }
        self.close_keystrokes_start = Some(start);
        self.update_clear_close_keystrokes_timer(cx);
    }

    fn update_clear_close_keystrokes_timer(&mut self, cx: &mut Context<Self>) {
        self.clear_close_keystrokes_timer = Some(cx.spawn(async |this, cx| {
            cx.background_executor()
                .timer(CLOSE_KEYSTROKE_CAPTURE_END_TIMEOUT)
                .await;
            this.update(cx, |this, _cx| {
                this.end_close_keystrokes_capture();
            })
            .ok();
        }));
    }

    /// Interrupt the capture of close keystrokes, but do not clear the close keystrokes
    /// from the input
    fn end_close_keystrokes_capture(&mut self) -> Option<usize> {
        self.close_keystrokes.take();
        self.clear_close_keystrokes_timer.take();
        self.close_keystrokes_start.take()
    }

    fn handle_possible_close_keystroke(
        &mut self,
        keystroke: &Keystroke,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> CloseKeystrokeResult {
        let Some(keybind_for_close_action) = Self::determine_stop_recording_binding(window) else {
            log::trace!("No keybinding to stop recording keystrokes in keystroke input");
            self.end_close_keystrokes_capture();
            return CloseKeystrokeResult::None;
        };
        let action_keystrokes = keybind_for_close_action.keystrokes();

        if let Some(mut close_keystrokes) = self.close_keystrokes.take() {
            let mut index = 0;

            while index < action_keystrokes.len() && index < close_keystrokes.len() {
                if !close_keystrokes[index].should_match(&action_keystrokes[index]) {
                    break;
                }
                index += 1;
            }
            if index == close_keystrokes.len() {
                if index >= action_keystrokes.len() {
                    self.end_close_keystrokes_capture();
                    return CloseKeystrokeResult::None;
                }
                if keystroke.should_match(&action_keystrokes[index]) {
                    close_keystrokes.push(keystroke.clone());
                    if close_keystrokes.len() == action_keystrokes.len() {
                        return CloseKeystrokeResult::Close;
                    } else {
                        self.close_keystrokes = Some(close_keystrokes);
                        self.update_clear_close_keystrokes_timer(cx);
                        return CloseKeystrokeResult::Partial;
                    }
                } else {
                    self.end_close_keystrokes_capture();
                    return CloseKeystrokeResult::None;
                }
            }
        } else if let Some(first_action_keystroke) = action_keystrokes.first()
            && keystroke.should_match(first_action_keystroke)
        {
            self.close_keystrokes = Some(vec![keystroke.clone()]);
            return CloseKeystrokeResult::Partial;
        }
        self.end_close_keystrokes_capture();
        CloseKeystrokeResult::None
    }

    fn on_modifiers_changed(
        &mut self,
        event: &ModifiersChangedEvent,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        cx.stop_propagation();
        let keystrokes_len = self.keystrokes.len();

        if self.previous_modifiers.modified()
            && event.modifiers.is_subset_of(&self.previous_modifiers)
        {
            self.previous_modifiers &= event.modifiers;
            return;
        }
        self.keystrokes_changed(cx);

        if let Some(last) = self.keystrokes.last_mut()
            && last.key().is_empty()
            && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
        {
            if !self.search && !event.modifiers.modified() {
                self.keystrokes.pop();
                return;
            }
            if self.search {
                if self.previous_modifiers.modified() {
                    let modifiers = *last.modifiers() | event.modifiers;
                    last.set_modifiers(modifiers);
                } else {
                    self.keystrokes.push(Self::dummy(event.modifiers));
                }
                self.previous_modifiers |= event.modifiers;
            } else {
                last.set_modifiers(event.modifiers);
                return;
            }
        } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
            self.keystrokes.push(Self::dummy(event.modifiers));
            if self.search {
                self.previous_modifiers |= event.modifiers;
            }
        }
        if keystrokes_len >= Self::KEYSTROKE_COUNT_MAX {
            self.clear_keystrokes(&ClearKeystrokes, window, cx);
        }
    }

    fn handle_keystroke(
        &mut self,
        keystroke: &Keystroke,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        cx.stop_propagation();

        let close_keystroke_result = self.handle_possible_close_keystroke(keystroke, window, cx);
        if close_keystroke_result == CloseKeystrokeResult::Close {
            self.stop_recording(&StopRecording, window, cx);
            return;
        }

        let keystroke = KeybindingKeystroke::new_with_mapper(
            keystroke.clone(),
            false,
            cx.keyboard_mapper().as_ref(),
        );
        if let Some(last) = self.keystrokes.last()
            && last.key().is_empty()
            && (!self.search || self.previous_modifiers.modified())
        {
            self.keystrokes.pop();
        }

        if close_keystroke_result == CloseKeystrokeResult::Partial {
            self.upsert_close_keystrokes_start(self.keystrokes.len(), cx);
            if self.keystrokes.len() >= Self::KEYSTROKE_COUNT_MAX {
                return;
            }
        }

        if self.keystrokes.len() >= Self::KEYSTROKE_COUNT_MAX {
            self.clear_keystrokes(&ClearKeystrokes, window, cx);
            return;
        }

        self.keystrokes.push(keystroke);
        self.keystrokes_changed(cx);

        // The reason we use the real modifiers from the window instead of the keystroke's modifiers
        // is that for keystrokes like `ctrl-$` the modifiers reported by keystroke is `ctrl` which
        // is wrong, it should be `ctrl-shift`. The window's modifiers are always correct.
        let real_modifiers = window.modifiers();
        if self.search {
            self.previous_modifiers = real_modifiers;
            return;
        }
        if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX && real_modifiers.modified() {
            self.keystrokes.push(Self::dummy(real_modifiers));
        }
    }

    fn on_inner_focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
        if self.intercept_subscription.is_none() {
            let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, window, cx| {
                this.handle_keystroke(&event.keystroke, window, cx);
            });
            self.intercept_subscription = Some(cx.intercept_keystrokes(listener))
        }
    }

    fn on_inner_focus_out(
        &mut self,
        _event: gpui::FocusOutEvent,
        _window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.intercept_subscription.take();
        cx.notify();
    }

    fn render_keystrokes(&self, is_recording: bool) -> impl Iterator<Item = Div> {
        let keystrokes = if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
            && self.keystrokes.is_empty()
        {
            if is_recording {
                &[]
            } else {
                placeholders.as_slice()
            }
        } else {
            &self.keystrokes
        };
        keystrokes.iter().map(move |keystroke| {
            h_flex().children(ui::render_keybinding_keystroke(
                keystroke,
                Some(Color::Default),
                Some(rems(0.875).into()),
                ui::PlatformStyle::platform(),
                false,
            ))
        })
    }

    pub fn start_recording(
        &mut self,
        _: &StartRecording,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        window.focus(&self.inner_focus_handle, cx);
        self.clear_keystrokes(&ClearKeystrokes, window, cx);
        self.previous_modifiers = window.modifiers();
        #[cfg(test)]
        {
            self.recording = true;
        }
        cx.stop_propagation();
    }

    pub fn stop_recording(
        &mut self,
        _: &StopRecording,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        if !self.is_recording(window) {
            return;
        }
        window.focus(&self.outer_focus_handle, cx);
        if let Some(close_keystrokes_start) = self.close_keystrokes_start.take()
            && close_keystrokes_start < self.keystrokes.len()
        {
            self.keystrokes.drain(close_keystrokes_start..);
            self.keystrokes_changed(cx);
        }
        self.end_close_keystrokes_capture();
        #[cfg(test)]
        {
            self.recording = false;
        }
        cx.notify();
    }

    pub fn clear_keystrokes(
        &mut self,
        _: &ClearKeystrokes,
        _window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.keystrokes.clear();
        self.keystrokes_changed(cx);
        self.end_close_keystrokes_capture();
    }

    fn is_recording(&self, window: &Window) -> bool {
        #[cfg(test)]
        {
            if true {
                // in tests, we just need a simple bool that is toggled on start and stop recording
                return self.recording;
            }
        }
        // however, in the real world, checking if the inner focus handle is focused
        // is a much more reliable check, as the intercept keystroke handlers are installed
        // on focus of the inner focus handle, thereby ensuring our recording state does
        // not get de-synced
        self.inner_focus_handle.is_focused(window)
    }

    pub fn actions_slot(mut self, action: impl IntoElement) -> Self {
        self.actions_slot = Some(action.into_any_element());
        self
    }
}

impl EventEmitter<()> for KeystrokeInput {}

impl Focusable for KeystrokeInput {
    fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle {
        self.outer_focus_handle.clone()
    }
}

impl Render for KeystrokeInput {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let colors = cx.theme().colors();
        let is_focused = self.outer_focus_handle.contains_focused(window, cx);
        let is_recording = self.is_recording(window);

        let width = rems_from_px(64.);

        let recording_bg_color = colors
            .editor_background
            .blend(colors.text_accent.opacity(0.1));

        let recording_pulse = |color: Color| {
            Icon::new(IconName::Circle)
                .size(IconSize::Small)
                .color(Color::Error)
                .with_animation(
                    "recording-pulse",
                    Animation::new(std::time::Duration::from_secs(2))
                        .repeat()
                        .with_easing(gpui::pulsating_between(0.4, 0.8)),
                    {
                        let color = color.color(cx);
                        move |this, delta| this.color(Color::Custom(color.opacity(delta)))
                    },
                )
        };

        let recording_indicator = h_flex()
            .h_4()
            .pr_1()
            .gap_0p5()
            .border_1()
            .border_color(colors.border)
            .bg(colors
                .editor_background
                .blend(colors.text_accent.opacity(0.1)))
            .rounded_sm()
            .child(recording_pulse(Color::Error))
            .child(
                Label::new("REC")
                    .size(LabelSize::XSmall)
                    .weight(FontWeight::SEMIBOLD)
                    .color(Color::Error),
            );

        let search_indicator = h_flex()
            .h_4()
            .pr_1()
            .gap_0p5()
            .border_1()
            .border_color(colors.border)
            .bg(colors
                .editor_background
                .blend(colors.text_accent.opacity(0.1)))
            .rounded_sm()
            .child(recording_pulse(Color::Accent))
            .child(
                Label::new("SEARCH")
                    .size(LabelSize::XSmall)
                    .weight(FontWeight::SEMIBOLD)
                    .color(Color::Accent),
            );

        let record_icon = if self.search {
            IconName::MagnifyingGlass
        } else {
            IconName::PlayFilled
        };

        h_flex()
            .id("keystroke-input")
            .track_focus(&self.outer_focus_handle)
            .key_context(Self::key_context())
            .on_action(cx.listener(Self::start_recording))
            .on_action(cx.listener(Self::clear_keystrokes))
            .py_2()
            .px_3()
            .gap_2()
            .min_h_10()
            .w_full()
            .flex_1()
            .justify_between()
            .rounded_md()
            .overflow_hidden()
            .map(|this| {
                if is_recording {
                    this.bg(recording_bg_color)
                } else {
                    this.bg(colors.editor_background)
                }
            })
            .border_1()
            .map(|this| {
                if is_focused {
                    this.border_color(colors.border_focused)
                } else {
                    this.border_color(colors.border_variant)
                }
            })
            .child(
                h_flex()
                    .w(width)
                    .gap_0p5()
                    .justify_start()
                    .flex_none()
                    .when(is_recording, |this| {
                        this.map(|this| {
                            if self.search {
                                this.child(search_indicator)
                            } else {
                                this.child(recording_indicator)
                            }
                        })
                    }),
            )
            .child(
                h_flex()
                    .id("keystroke-input-inner")
                    .track_focus(&self.inner_focus_handle)
                    .on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
                    .when(!self.search, |this| {
                        this.focus(|mut style| {
                            style.border_color = Some(colors.border_focused);
                            style
                        })
                    })
                    .size_full()
                    .min_w_0()
                    .justify_center()
                    .flex_wrap()
                    .gap_1()
                    .children(self.render_keystrokes(is_recording)),
            )
            .child(
                h_flex()
                    .w(width)
                    .gap_0p5()
                    .justify_end()
                    .flex_none()
                    .map(|this| {
                        if is_recording {
                            this.child(
                                IconButton::new("stop-record-btn", IconName::Stop)
                                    .shape(IconButtonShape::Square)
                                    .map(|this| {
                                        this.tooltip(Tooltip::for_action_title(
                                            if self.search {
                                                "Stop Searching"
                                            } else {
                                                "Stop Recording"
                                            },
                                            &StopRecording,
                                        ))
                                    })
                                    .icon_color(Color::Error)
                                    .on_click(cx.listener(|this, _event, window, cx| {
                                        this.stop_recording(&StopRecording, window, cx);
                                    })),
                            )
                        } else {
                            this.child(
                                IconButton::new("record-btn", record_icon)
                                    .shape(IconButtonShape::Square)
                                    .map(|this| {
                                        this.tooltip(Tooltip::for_action_title(
                                            if self.search {
                                                "Start Searching"
                                            } else {
                                                "Start Recording"
                                            },
                                            &StartRecording,
                                        ))
                                    })
                                    .when(!is_focused, |this| this.icon_color(Color::Muted))
                                    .on_click(cx.listener(|this, _event, window, cx| {
                                        this.start_recording(&StartRecording, window, cx);
                                    })),
                            )
                        }
                    })
                    .when_some(self.actions_slot.take(), |this, action| this.child(action))
                    .when(is_recording, |this| {
                        this.child(
                            IconButton::new("clear-btn", IconName::Backspace)
                                .shape(IconButtonShape::Square)
                                .tooltip(move |_, cx| {
                                    Tooltip::with_meta(
                                        "Clear Keystrokes",
                                        Some(&ClearKeystrokes),
                                        "Hit it three times to execute",
                                        cx,
                                    )
                                })
                                .when(!is_focused, |this| this.icon_color(Color::Muted))
                                .on_click(cx.listener(|this, _event, window, cx| {
                                    this.clear_keystrokes(&ClearKeystrokes, window, cx);
                                })),
                        )
                    }),
            )
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use fs::FakeFs;
    use gpui::{Entity, TestAppContext, VisualTestContext};
    use itertools::Itertools as _;
    use project::Project;
    use settings::SettingsStore;
    use workspace::MultiWorkspace;

    pub struct KeystrokeInputTestHelper {
        input: Entity<KeystrokeInput>,
        current_modifiers: Modifiers,
        cx: VisualTestContext,
    }

    impl KeystrokeInputTestHelper {
        /// Creates a new test helper with default settings
        pub fn new(mut cx: VisualTestContext) -> Self {
            let input = cx.new_window_entity(|window, cx| KeystrokeInput::new(None, window, cx));

            let mut helper = Self {
                input,
                current_modifiers: Modifiers::default(),
                cx,
            };

            helper.start_recording();
            helper
        }

        /// Sets search mode on the input
        pub fn with_search_mode(&mut self, search: bool) -> &mut Self {
            self.input.update(&mut self.cx, |input, _| {
                input.set_search(search);
            });
            self
        }

        /// Sends a keystroke event based on string description
        /// Examples: "a", "ctrl-a", "cmd-shift-z", "escape"
        #[track_caller]
        pub fn send_keystroke(&mut self, keystroke_input: &str) -> &mut Self {
            self.expect_is_recording(true);
            let keystroke_str = if keystroke_input.ends_with('-') {
                format!("{}_", keystroke_input)
            } else {
                keystroke_input.to_string()
            };

            let mut keystroke = Keystroke::parse(&keystroke_str)
                .unwrap_or_else(|_| panic!("Invalid keystroke: {}", keystroke_input));

            // Remove the dummy key if we added it for modifier-only keystrokes
            if keystroke_input.ends_with('-') && keystroke_str.ends_with("_") {
                keystroke.key = "".to_string();
            }

            // Combine current modifiers with keystroke modifiers
            keystroke.modifiers |= self.current_modifiers;
            let real_modifiers = keystroke.modifiers;
            keystroke = to_gpui_keystroke(keystroke);

            self.update_input(|input, window, cx| {
                window.set_modifiers(real_modifiers);
                input.handle_keystroke(&keystroke, window, cx);
            });

            // Don't update current_modifiers for keystrokes with actual keys
            if keystroke.key.is_empty() {
                self.current_modifiers = keystroke.modifiers;
            }
            self
        }

        /// Sends a modifier change event based on string description
        /// Examples: "+ctrl", "-ctrl", "+cmd+shift", "-all"
        #[track_caller]
        pub fn send_modifiers(&mut self, modifiers: &str) -> &mut Self {
            self.expect_is_recording(true);
            let new_modifiers = if modifiers == "-all" {
                Modifiers::default()
            } else {
                self.parse_modifier_change(modifiers)
            };

            let event = ModifiersChangedEvent {
                modifiers: new_modifiers,
                capslock: gpui::Capslock::default(),
            };

            self.update_input(|input, window, cx| {
                window.set_modifiers(new_modifiers);
                input.on_modifiers_changed(&event, window, cx);
            });

            self.current_modifiers = new_modifiers;
            self
        }

        /// Sends multiple events in sequence
        /// Each event string is either a keystroke or modifier change
        #[track_caller]
        pub fn send_events(&mut self, events: &[&str]) -> &mut Self {
            self.expect_is_recording(true);
            for event in events {
                if event.starts_with('+') || event.starts_with('-') {
                    self.send_modifiers(event);
                } else {
                    self.send_keystroke(event);
                }
            }
            self
        }

        #[track_caller]
        fn expect_keystrokes_equal(actual: &[Keystroke], expected: &[&str]) {
            let expected_keystrokes: Result<Vec<Keystroke>, _> = expected
                .iter()
                .map(|s| {
                    let keystroke_str = if s.ends_with('-') {
                        format!("{}_", s)
                    } else {
                        s.to_string()
                    };

                    let mut keystroke = Keystroke::parse(&keystroke_str)?;

                    // Remove the dummy key if we added it for modifier-only keystrokes
                    if s.ends_with('-') && keystroke_str.ends_with("_") {
                        keystroke.key = "".to_string();
                    }

                    Ok(keystroke)
                })
                .collect();

            let expected_keystrokes = expected_keystrokes
                .unwrap_or_else(|e: anyhow::Error| panic!("Invalid expected keystroke: {}", e));

            assert_eq!(
                actual.len(),
                expected_keystrokes.len(),
                "Keystroke count mismatch. Expected: {:?}, Actual: {:?}",
                expected_keystrokes
                    .iter()
                    .map(|k| k.unparse())
                    .collect::<Vec<_>>(),
                actual.iter().map(|k| k.unparse()).collect::<Vec<_>>()
            );

            for (i, (actual, expected)) in actual.iter().zip(expected_keystrokes.iter()).enumerate()
            {
                assert_eq!(
                    actual.unparse(),
                    expected.unparse(),
                    "Keystroke {} mismatch. Expected: '{}', Actual: '{}'",
                    i,
                    expected.unparse(),
                    actual.unparse()
                );
            }
        }

        /// Verifies that the keystrokes match the expected strings
        #[track_caller]
        pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self {
            let actual: Vec<Keystroke> = self.input.read_with(&self.cx, |input, _| {
                input
                    .keystrokes
                    .iter()
                    .map(|keystroke| keystroke.inner().clone())
                    .collect()
            });
            Self::expect_keystrokes_equal(&actual, expected);
            self
        }

        #[track_caller]
        pub fn expect_close_keystrokes(&mut self, expected: &[&str]) -> &mut Self {
            let actual = self
                .input
                .read_with(&self.cx, |input, _| input.close_keystrokes.clone())
                .unwrap_or_default();
            Self::expect_keystrokes_equal(&actual, expected);
            self
        }

        /// Verifies that there are no keystrokes
        #[track_caller]
        pub fn expect_empty(&mut self) -> &mut Self {
            self.expect_keystrokes(&[])
        }

        /// Starts recording keystrokes
        #[track_caller]
        pub fn start_recording(&mut self) -> &mut Self {
            self.expect_is_recording(false);
            self.input.update_in(&mut self.cx, |input, window, cx| {
                input.start_recording(&StartRecording, window, cx);
            });
            self
        }

        /// Stops recording keystrokes
        pub fn stop_recording(&mut self) -> &mut Self {
            self.expect_is_recording(true);
            self.input.update_in(&mut self.cx, |input, window, cx| {
                input.stop_recording(&StopRecording, window, cx);
            });
            self
        }

        /// Clears all keystrokes
        #[track_caller]
        pub fn clear_keystrokes(&mut self) -> &mut Self {
            let change_tracker = KeystrokeUpdateTracker::new(self.input.clone(), &mut self.cx);
            self.input.update_in(&mut self.cx, |input, window, cx| {
                input.clear_keystrokes(&ClearKeystrokes, window, cx);
            });
            KeystrokeUpdateTracker::finish(change_tracker, &self.cx);
            self.current_modifiers = Default::default();
            self
        }

        /// Verifies the recording state
        #[track_caller]
        pub fn expect_is_recording(&mut self, expected: bool) -> &mut Self {
            let actual = self
                .input
                .update_in(&mut self.cx, |input, window, _| input.is_recording(window));
            assert_eq!(
                actual, expected,
                "Recording state mismatch. Expected: {}, Actual: {}",
                expected, actual
            );
            self
        }

        pub async fn wait_for_close_keystroke_capture_end(&mut self) -> &mut Self {
            let task = self.input.update_in(&mut self.cx, |input, _, _| {
                input.clear_close_keystrokes_timer.take()
            });
            let task = task.expect("No close keystroke capture end timer task");
            self.cx
                .executor()
                .advance_clock(CLOSE_KEYSTROKE_CAPTURE_END_TIMEOUT);
            task.await;
            self
        }

        /// Parses modifier change strings like "+ctrl", "-shift", "+cmd+alt"
        #[track_caller]
        fn parse_modifier_change(&self, modifiers_str: &str) -> Modifiers {
            let mut modifiers = self.current_modifiers;

            assert!(!modifiers_str.is_empty(), "Empty modifier string");

            let value;
            let split_char;
            let remaining;
            if let Some(to_add) = modifiers_str.strip_prefix('+') {
                value = true;
                split_char = '+';
                remaining = to_add;
            } else {
                let to_remove = modifiers_str
                    .strip_prefix('-')
                    .expect("Modifier string must start with '+' or '-'");
                value = false;
                split_char = '-';
                remaining = to_remove;
            }

            for modifier in remaining.split(split_char) {
                match modifier {
                    "ctrl" | "control" => modifiers.control = value,
                    "alt" | "option" => modifiers.alt = value,
                    "shift" => modifiers.shift = value,
                    "cmd" | "command" | "platform" => modifiers.platform = value,
                    "fn" | "function" => modifiers.function = value,
                    _ => panic!("Unknown modifier: {}", modifier),
                }
            }

            modifiers
        }

        #[track_caller]
        fn update_input<R>(
            &mut self,
            cb: impl FnOnce(&mut KeystrokeInput, &mut Window, &mut Context<KeystrokeInput>) -> R,
        ) -> R {
            let change_tracker = KeystrokeUpdateTracker::new(self.input.clone(), &mut self.cx);
            let result = self.input.update_in(&mut self.cx, cb);
            KeystrokeUpdateTracker::finish(change_tracker, &self.cx);
            result
        }
    }

    /// For GPUI, when you press `ctrl-shift-2`, it produces `ctrl-@` without the shift modifier.
    fn to_gpui_keystroke(mut keystroke: Keystroke) -> Keystroke {
        if keystroke.modifiers.shift {
            match keystroke.key.as_str() {
                "`" => {
                    keystroke.key = "~".into();
                    keystroke.modifiers.shift = false;
                }
                "1" => {
                    keystroke.key = "!".into();
                    keystroke.modifiers.shift = false;
                }
                "2" => {
                    keystroke.key = "@".into();
                    keystroke.modifiers.shift = false;
                }
                "3" => {
                    keystroke.key = "#".into();
                    keystroke.modifiers.shift = false;
                }
                "4" => {
                    keystroke.key = "$".into();
                    keystroke.modifiers.shift = false;
                }
                "5" => {
                    keystroke.key = "%".into();
                    keystroke.modifiers.shift = false;
                }
                "6" => {
                    keystroke.key = "^".into();
                    keystroke.modifiers.shift = false;
                }
                "7" => {
                    keystroke.key = "&".into();
                    keystroke.modifiers.shift = false;
                }
                "8" => {
                    keystroke.key = "*".into();
                    keystroke.modifiers.shift = false;
                }
                "9" => {
                    keystroke.key = "(".into();
                    keystroke.modifiers.shift = false;
                }
                "0" => {
                    keystroke.key = ")".into();
                    keystroke.modifiers.shift = false;
                }
                "-" => {
                    keystroke.key = "_".into();
                    keystroke.modifiers.shift = false;
                }
                "=" => {
                    keystroke.key = "+".into();
                    keystroke.modifiers.shift = false;
                }
                "[" => {
                    keystroke.key = "{".into();
                    keystroke.modifiers.shift = false;
                }
                "]" => {
                    keystroke.key = "}".into();
                    keystroke.modifiers.shift = false;
                }
                "\\" => {
                    keystroke.key = "|".into();
                    keystroke.modifiers.shift = false;
                }
                ";" => {
                    keystroke.key = ":".into();
                    keystroke.modifiers.shift = false;
                }
                "'" => {
                    keystroke.key = "\"".into();
                    keystroke.modifiers.shift = false;
                }
                "," => {
                    keystroke.key = "<".into();
                    keystroke.modifiers.shift = false;
                }
                "." => {
                    keystroke.key = ">".into();
                    keystroke.modifiers.shift = false;
                }
                "/" => {
                    keystroke.key = "?".into();
                    keystroke.modifiers.shift = false;
                }
                _ => {}
            }
        }
        keystroke
    }

    struct KeystrokeUpdateTracker {
        initial_keystrokes: Vec<KeybindingKeystroke>,
        _subscription: Subscription,
        input: Entity<KeystrokeInput>,
        received_keystrokes_updated: bool,
    }

    impl KeystrokeUpdateTracker {
        fn new(input: Entity<KeystrokeInput>, cx: &mut VisualTestContext) -> Entity<Self> {
            cx.new(|cx| Self {
                initial_keystrokes: input.read_with(cx, |input, _| input.keystrokes.clone()),
                _subscription: cx.subscribe(&input, |this: &mut Self, _, _, _| {
                    this.received_keystrokes_updated = true;
                }),
                input,
                received_keystrokes_updated: false,
            })
        }
        #[track_caller]
        fn finish(this: Entity<Self>, cx: &VisualTestContext) {
            let (received_keystrokes_updated, initial_keystrokes_str, updated_keystrokes_str) =
                this.read_with(cx, |this, cx| {
                    let updated_keystrokes = this
                        .input
                        .read_with(cx, |input, _| input.keystrokes.clone());
                    let initial_keystrokes_str = keystrokes_str(&this.initial_keystrokes);
                    let updated_keystrokes_str = keystrokes_str(&updated_keystrokes);
                    (
                        this.received_keystrokes_updated,
                        initial_keystrokes_str,
                        updated_keystrokes_str,
                    )
                });
            if received_keystrokes_updated {
                assert_ne!(
                    initial_keystrokes_str, updated_keystrokes_str,
                    "Received keystrokes_updated event, expected different keystrokes"
                );
            } else {
                assert_eq!(
                    initial_keystrokes_str, updated_keystrokes_str,
                    "Received no keystrokes_updated event, expected same keystrokes"
                );
            }

            fn keystrokes_str(ks: &[KeybindingKeystroke]) -> String {
                ks.iter().map(|ks| ks.inner().unparse()).join(" ")
            }
        }
    }

    async fn init_test(cx: &mut TestAppContext) -> KeystrokeInputTestHelper {
        cx.update(|cx| {
            let settings_store = SettingsStore::test(cx);
            cx.set_global(settings_store);
            theme_settings::init(theme::LoadThemes::JustBase, cx);
        });

        let fs = FakeFs::new(cx.executor());
        let project = Project::test(fs, [], cx).await;
        let window_handle =
            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
        let cx = VisualTestContext::from_window(window_handle.into(), cx);
        KeystrokeInputTestHelper::new(cx)
    }

    #[gpui::test]
    async fn test_basic_keystroke_input(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .send_keystroke("a")
            .clear_keystrokes()
            .expect_empty();
    }

    #[gpui::test]
    async fn test_modifier_handling(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["+ctrl", "a", "-ctrl"])
            .expect_keystrokes(&["ctrl-a"]);
    }

    #[gpui::test]
    async fn test_multiple_modifiers(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .send_keystroke("cmd-shift-z")
            .expect_keystrokes(&["cmd-shift-z", "cmd-shift-"]);
    }

    #[gpui::test]
    async fn test_search_mode_behavior(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["+cmd", "shift-f", "-cmd"])
            // In search mode, when completing a modifier-only keystroke with a key,
            // only the original modifiers are preserved, not the keystroke's modifiers
            //
            // Update:
            // This behavior was changed to preserve all modifiers in search mode, this is now reflected in the expected keystrokes.
            // Specifically, considering the sequence: `+cmd +shift -shift 2`, we expect it to produce the same result as `+cmd +shift 2`
            // which is `cmd-@`. But in the case of `+cmd +shift -shift 2`, the keystroke we receive is `cmd-2`, which means that
            // we need to dynamically map the key from `2` to `@` when the shift modifier is not present, which is not possible.
            // Therefore, we now preserve all modifiers in search mode to ensure consistent behavior.
            // And also, VSCode seems to preserve all modifiers in search mode as well.
            .expect_keystrokes(&["cmd-shift-f"]);
    }

    #[gpui::test]
    async fn test_keystroke_limit(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .send_keystroke("a")
            .send_keystroke("b")
            .send_keystroke("c")
            .expect_keystrokes(&["a", "b", "c"]) // At max limit
            .send_keystroke("d")
            .expect_empty(); // Should clear when exceeding limit
    }

    #[gpui::test]
    async fn test_modifier_release_all(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["+ctrl+shift", "a", "-all"])
            .expect_keystrokes(&["ctrl-shift-a"]);
    }

    #[gpui::test]
    async fn test_search_new_modifiers_not_added_until_all_released(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["+ctrl+shift", "a", "-ctrl"])
            .expect_keystrokes(&["ctrl-shift-a"])
            .send_events(&["+ctrl"])
            .expect_keystrokes(&["ctrl-shift-a", "ctrl-shift-"]);
    }

    #[gpui::test]
    async fn test_previous_modifiers_no_effect_when_not_search(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(false)
            .send_events(&["+ctrl+shift", "a", "-all"])
            .expect_keystrokes(&["ctrl-shift-a"]);
    }

    #[gpui::test]
    async fn test_keystroke_limit_overflow_non_search_mode(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(false)
            .send_events(&["a", "b", "c", "d"]) // 4 keystrokes, exceeds limit of 3
            .expect_empty(); // Should clear when exceeding limit
    }

    #[gpui::test]
    async fn test_complex_modifier_sequences(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["+ctrl", "+shift", "+alt", "a", "-ctrl", "-shift", "-alt"])
            .expect_keystrokes(&["ctrl-shift-alt-a"]);
    }

    #[gpui::test]
    async fn test_modifier_only_keystrokes_search_mode(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["+ctrl", "+shift", "-ctrl", "-shift"])
            .expect_keystrokes(&["ctrl-shift-"]); // Modifier-only sequences create modifier-only keystrokes
    }

    #[gpui::test]
    async fn test_modifier_only_keystrokes_non_search_mode(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(false)
            .send_events(&["+ctrl", "+shift", "-ctrl", "-shift"])
            .expect_empty(); // Modifier-only sequences get filtered in non-search mode
    }

    #[gpui::test]
    async fn test_rapid_modifier_changes(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["+ctrl", "-ctrl", "+shift", "-shift", "+alt", "a", "-alt"])
            .expect_keystrokes(&["ctrl-", "shift-", "alt-a"]);
    }

    #[gpui::test]
    async fn test_clear_keystrokes_search_mode(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["+ctrl", "a", "-ctrl", "b"])
            .expect_keystrokes(&["ctrl-a", "b"])
            .clear_keystrokes()
            .expect_empty();
    }

    #[gpui::test]
    async fn test_non_search_mode_modifier_key_sequence(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(false)
            .send_events(&["+ctrl", "a"])
            .expect_keystrokes(&["ctrl-a", "ctrl-"])
            .send_events(&["-ctrl"])
            .expect_keystrokes(&["ctrl-a"]); // Non-search mode filters trailing empty keystrokes
    }

    #[gpui::test]
    async fn test_all_modifiers_at_once(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["+ctrl+shift+alt+cmd", "a", "-all"])
            .expect_keystrokes(&["ctrl-shift-alt-cmd-a"]);
    }

    #[gpui::test]
    async fn test_keystrokes_at_exact_limit(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["a", "b", "c"]) // exactly 3 keystrokes (at limit)
            .expect_keystrokes(&["a", "b", "c"])
            .send_events(&["d"]) // should clear when exceeding
            .expect_empty();
    }

    #[gpui::test]
    async fn test_function_modifier_key(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["+fn", "f1", "-fn"])
            .expect_keystrokes(&["fn-f1"]);
    }

    #[gpui::test]
    async fn test_start_stop_recording(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .send_events(&["a", "b"])
            .expect_keystrokes(&["a", "b"]) // start_recording clears existing keystrokes
            .stop_recording()
            .expect_is_recording(false)
            .start_recording()
            .send_events(&["c"])
            .expect_keystrokes(&["c"]);
    }

    #[gpui::test]
    async fn test_modifier_sequence_with_interruption(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["+ctrl", "+shift", "a", "-shift", "b", "-ctrl"])
            .expect_keystrokes(&["ctrl-shift-a", "ctrl-b"]);
    }

    #[gpui::test]
    async fn test_empty_key_sequence_search_mode(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&[]) // No events at all
            .expect_empty();
    }

    #[gpui::test]
    async fn test_modifier_sequence_completion_search_mode(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["+ctrl", "+shift", "-shift", "a", "-ctrl"])
            .expect_keystrokes(&["ctrl-a"]);
    }

    #[gpui::test]
    async fn test_triple_escape_stops_recording_search_mode(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["a", "escape", "escape", "escape"])
            .expect_keystrokes(&["a"]) // Triple escape removes final escape, stops recording
            .expect_is_recording(false);
    }

    #[gpui::test]
    async fn test_triple_escape_stops_recording_non_search_mode(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(false)
            .send_events(&["a", "escape", "escape", "escape"])
            .expect_keystrokes(&["a"]); // Triple escape stops recording but only removes final escape
    }

    #[gpui::test]
    async fn test_triple_escape_at_keystroke_limit(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["a", "b", "c", "escape", "escape", "escape"]) // 6 keystrokes total, exceeds limit
            .expect_keystrokes(&["a", "b", "c"]); // Triple escape stops recording and removes escapes, leaves original keystrokes
    }

    #[gpui::test]
    async fn test_interrupted_escape_sequence(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["escape", "escape", "a", "escape"]) // Partial escape sequence interrupted by 'a'
            .expect_keystrokes(&["escape", "escape", "a"]); // Escape sequence interrupted by 'a', no close triggered
    }

    #[gpui::test]
    async fn test_interrupted_escape_sequence_within_limit(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["escape", "escape", "a"]) // Partial escape sequence interrupted by 'a' (3 keystrokes, at limit)
            .expect_keystrokes(&["escape", "escape", "a"]); // Should not trigger close, interruption resets escape detection
    }

    #[gpui::test]
    async fn test_partial_escape_sequence_no_close(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["escape", "escape"]) // Only 2 escapes, not enough to close
            .expect_keystrokes(&["escape", "escape"])
            .expect_is_recording(true); // Should remain in keystrokes, no close triggered
    }

    #[gpui::test]
    async fn test_recording_state_after_triple_escape(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["a", "escape", "escape", "escape"])
            .expect_keystrokes(&["a"]) // Triple escape stops recording, removes final escape
            .expect_is_recording(false);
    }

    #[gpui::test]
    async fn test_triple_escape_mixed_with_other_keystrokes(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["a", "escape", "b", "escape", "escape"]) // Mixed sequence, should not trigger close
            .expect_keystrokes(&["a", "escape", "b"]); // No complete triple escape sequence, stays at limit
    }

    #[gpui::test]
    async fn test_triple_escape_only(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["escape", "escape", "escape"]) // Pure triple escape sequence
            .expect_empty();
    }

    #[gpui::test]
    async fn test_end_close_keystroke_capture(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .send_events(&["+ctrl", "g", "-ctrl", "escape"])
            .expect_keystrokes(&["ctrl-g", "escape"])
            .wait_for_close_keystroke_capture_end()
            .await
            .send_events(&["escape", "escape"])
            .expect_keystrokes(&["ctrl-g", "escape", "escape"])
            .expect_close_keystrokes(&["escape", "escape"])
            .send_keystroke("escape")
            .expect_keystrokes(&["ctrl-g", "escape"]);
    }

    #[gpui::test]
    async fn test_search_previous_modifiers_are_sticky(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["+ctrl+alt", "-ctrl", "j"])
            .expect_keystrokes(&["alt-j"]);
    }

    #[gpui::test]
    async fn test_previous_modifiers_can_be_entered_separately(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["+ctrl", "-ctrl"])
            .expect_keystrokes(&["ctrl-"])
            .send_events(&["+alt", "-alt"])
            .expect_keystrokes(&["ctrl-", "alt-"]);
    }

    #[gpui::test]
    async fn test_previous_modifiers_reset_on_key(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["+ctrl+alt", "-ctrl", "+shift"])
            .expect_keystrokes(&["ctrl-shift-alt-"])
            .send_keystroke("j")
            .expect_keystrokes(&["shift-alt-j"])
            .send_keystroke("i")
            .expect_keystrokes(&["shift-alt-j", "shift-alt-i"])
            .send_events(&["-shift-alt", "+cmd"])
            .expect_keystrokes(&["shift-alt-j", "shift-alt-i", "cmd-"]);
    }

    #[gpui::test]
    async fn test_previous_modifiers_reset_on_release_all(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["+ctrl+alt", "-ctrl", "+shift"])
            .expect_keystrokes(&["ctrl-shift-alt-"])
            .send_events(&["-all", "j"])
            .expect_keystrokes(&["ctrl-shift-alt-", "j"]);
    }

    #[gpui::test]
    async fn test_search_repeat_modifiers(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(true)
            .send_events(&["+ctrl", "-ctrl", "+alt", "-alt", "+shift", "-shift"])
            .expect_keystrokes(&["ctrl-", "alt-", "shift-"])
            .send_events(&["+cmd"])
            .expect_empty();
    }

    #[gpui::test]
    async fn test_not_search_repeat_modifiers(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(false)
            .send_events(&["+ctrl", "-ctrl", "+alt", "-alt", "+shift", "-shift"])
            .expect_empty();
    }

    #[gpui::test]
    async fn test_not_search_shifted_keys(cx: &mut TestAppContext) {
        init_test(cx)
            .await
            .with_search_mode(false)
            .send_events(&["+ctrl", "+shift", "4", "-all"])
            .expect_keystrokes(&["ctrl-$"]);
    }
}
