use super::super::actions::*;
use super::super::keybinds::*;
use crate::data::{BareKey, Direction, KeyWithModifier};
use crate::input::config::Config;
use insta::assert_snapshot;
use strum::IntoEnumIterator;

#[test]
fn can_define_keybindings_in_configfile() {
    let config_contents = r#"
        keybinds {
            normal {
                bind "Ctrl g" { SwitchToMode "Locked"; }
            }
        }
    "#;
    let config = Config::from_kdl(config_contents, None).unwrap();
    let ctrl_g_normal_mode_action = config.keybinds.get_actions_for_key_in_mode(
        &InputMode::Normal,
        &KeyWithModifier::new(BareKey::Char('g')).with_ctrl_modifier(),
    );
    assert_eq!(
        ctrl_g_normal_mode_action,
        Some(&vec![Action::SwitchToMode {
            input_mode: InputMode::Locked
        }]),
        "Keybinding successfully defined in config"
    );
}

#[test]
fn can_define_multiple_keybinds_for_same_action() {
    let config_contents = r#"
        keybinds {
            normal {
                bind "Alt h" "Alt Left" { MoveFocusOrTab "Left"; }
            }
        }
    "#;
    let config = Config::from_kdl(config_contents, None).unwrap();
    let alt_h_normal_mode_action = config.keybinds.get_actions_for_key_in_mode(
        &InputMode::Normal,
        &KeyWithModifier::new(BareKey::Left).with_alt_modifier(),
    );
    let alt_left_normal_mode_action = config.keybinds.get_actions_for_key_in_mode(
        &InputMode::Normal,
        &KeyWithModifier::new(BareKey::Char('h')).with_alt_modifier(),
    );
    assert_eq!(
        alt_h_normal_mode_action,
        Some(&vec![Action::MoveFocusOrTab {
            direction: Direction::Left
        }]),
        "First keybinding successfully defined in config"
    );
    assert_eq!(
        alt_left_normal_mode_action,
        Some(&vec![Action::MoveFocusOrTab {
            direction: Direction::Left
        }]),
        "Second keybinding successfully defined in config"
    );
}

#[test]
fn can_define_series_of_actions_for_same_keybinding() {
    let config_contents = r#"
        keybinds {
            pane {
                bind "z" { TogglePaneFrames; SwitchToMode "Normal"; }
            }
        }
    "#;
    let config = Config::from_kdl(config_contents, None).unwrap();
    let z_in_pane_mode = config
        .keybinds
        .get_actions_for_key_in_mode(&InputMode::Pane, &KeyWithModifier::new(BareKey::Char('z')));
    assert_eq!(
        z_in_pane_mode,
        Some(&vec![
            Action::TogglePaneFrames,
            Action::SwitchToMode {
                input_mode: InputMode::Normal
            }
        ]),
        "Action series successfully defined"
    );
}

#[test]
fn keybindings_bind_order_is_preserved() {
    let config_contents = r#"
        keybinds {
            pane {
                bind "z" { TogglePaneFrames; SwitchToMode "Normal"; }
                bind "z" { SwitchToMode "Resize"; }
            }
        }
    "#;
    let config = Config::from_kdl(config_contents, None).unwrap();
    let z_in_pane_mode = config
        .keybinds
        .get_actions_for_key_in_mode(&InputMode::Pane, &KeyWithModifier::new(BareKey::Char('z')));
    assert_eq!(
        z_in_pane_mode,
        Some(&vec![Action::SwitchToMode {
            input_mode: InputMode::Resize
        }]),
        "Second keybinding was applied"
    );
}

#[test]
fn uppercase_and_lowercase_keybindings_are_distinct() {
    let config_contents = r#"
        keybinds {
            pane {
                bind "z" { TogglePaneFrames; SwitchToMode "Normal"; }
                bind "Z" { SwitchToMode "Resize"; }
            }
        }
    "#;
    let config = Config::from_kdl(config_contents, None).unwrap();
    let z_in_pane_mode = config
        .keybinds
        .get_actions_for_key_in_mode(&InputMode::Pane, &KeyWithModifier::new(BareKey::Char('z')));
    let uppercase_z_in_pane_mode = config
        .keybinds
        .get_actions_for_key_in_mode(&InputMode::Pane, &KeyWithModifier::new(BareKey::Char('Z')));
    assert_eq!(
        z_in_pane_mode,
        Some(&vec![
            Action::TogglePaneFrames,
            Action::SwitchToMode {
                input_mode: InputMode::Normal
            }
        ]),
        "Lowercase z successfully bound"
    );
    assert_eq!(
        uppercase_z_in_pane_mode,
        Some(&vec![Action::SwitchToMode {
            input_mode: InputMode::Resize
        }]),
        "Uppercase z successfully bound"
    );
}

#[test]
fn can_override_keybindings() {
    let default_config_contents = r#"
        keybinds {
            pane {
                bind "z" { TogglePaneFrames; SwitchToMode "Normal"; }
            }
        }
    "#;
    let config_contents = r#"
        keybinds {
            pane {
                bind "z" { SwitchToMode "Resize"; }
            }
        }
    "#;
    let default_config = Config::from_kdl(default_config_contents, None).unwrap();
    let config = Config::from_kdl(config_contents, Some(default_config)).unwrap();
    let z_in_pane_mode = config
        .keybinds
        .get_actions_for_key_in_mode(&InputMode::Pane, &KeyWithModifier::new(BareKey::Char('z')));
    assert_eq!(
        z_in_pane_mode,
        Some(&vec![Action::SwitchToMode {
            input_mode: InputMode::Resize
        }]),
        "Keybinding from config overrode keybinding from default config"
    );
}

#[test]
fn can_add_to_default_keybindings() {
    // this test just makes sure keybindings defined in a custom config are added to different
    // keybindings defined in the default config
    let default_config_contents = r#"
        keybinds {
            pane {
                bind "z" { TogglePaneFrames; SwitchToMode "Normal"; }
            }
        }
    "#;
    let config_contents = r#"
        keybinds {
            pane {
                bind "r" { SwitchToMode "Resize"; }
            }
        }
    "#;
    let default_config = Config::from_kdl(default_config_contents, None).unwrap();
    let config = Config::from_kdl(config_contents, Some(default_config)).unwrap();
    let z_in_pane_mode = config
        .keybinds
        .get_actions_for_key_in_mode(&InputMode::Pane, &KeyWithModifier::new(BareKey::Char('z')));
    let r_in_pane_mode = config
        .keybinds
        .get_actions_for_key_in_mode(&InputMode::Pane, &KeyWithModifier::new(BareKey::Char('r')));
    assert_eq!(
        z_in_pane_mode,
        Some(&vec![
            Action::TogglePaneFrames,
            Action::SwitchToMode {
                input_mode: InputMode::Normal
            }
        ]),
        "Keybinding from default config bound"
    );
    assert_eq!(
        r_in_pane_mode,
        Some(&vec![Action::SwitchToMode {
            input_mode: InputMode::Resize
        }]),
        "Keybinding from custom config bound as well"
    );
}

#[test]
fn can_clear_default_keybindings() {
    let default_config_contents = r#"
        keybinds {
            normal {
                bind "Ctrl g" { SwitchToMode "Locked"; }
            }
            pane {
                bind "z" { TogglePaneFrames; SwitchToMode "Normal"; }
            }
        }
    "#;
    let config_contents = r#"
        keybinds clear-defaults=true {
            normal {
                bind "Ctrl r" { SwitchToMode "Locked"; }
            }
            pane {
                bind "r" { SwitchToMode "Resize"; }
            }
        }
    "#;
    let default_config = Config::from_kdl(default_config_contents, None).unwrap();
    let config = Config::from_kdl(config_contents, Some(default_config)).unwrap();
    let ctrl_g_normal_mode_action = config.keybinds.get_actions_for_key_in_mode(
        &InputMode::Normal,
        &KeyWithModifier::new(BareKey::Char('g')).with_ctrl_modifier(),
    );
    let z_in_pane_mode = config
        .keybinds
        .get_actions_for_key_in_mode(&InputMode::Pane, &KeyWithModifier::new(BareKey::Char('z')));
    let ctrl_r_in_normal_mode = config.keybinds.get_actions_for_key_in_mode(
        &InputMode::Normal,
        &KeyWithModifier::new(BareKey::Char('r')).with_ctrl_modifier(),
    );
    let r_in_pane_mode = config
        .keybinds
        .get_actions_for_key_in_mode(&InputMode::Pane, &KeyWithModifier::new(BareKey::Char('r')));
    assert_eq!(
        ctrl_g_normal_mode_action, None,
        "Keybinding from normal mode in default config cleared"
    );
    assert_eq!(
        z_in_pane_mode, None,
        "Keybinding from pane mode in default config cleared"
    );
    assert_eq!(
        r_in_pane_mode,
        Some(&vec![Action::SwitchToMode {
            input_mode: InputMode::Resize
        }]),
        "Keybinding from pane mode in custom config still bound"
    );
    assert_eq!(
        ctrl_r_in_normal_mode,
        Some(&vec![Action::SwitchToMode {
            input_mode: InputMode::Locked
        }]),
        "Keybinding from normal mode in custom config still bound"
    );
}

#[test]
fn can_clear_default_keybindings_per_single_mode() {
    let default_config_contents = r#"
        keybinds {
            normal {
                bind "Ctrl g" { SwitchToMode "Locked"; }
            }
            pane {
                bind "z" { TogglePaneFrames; SwitchToMode "Normal"; }
            }
        }
    "#;
    let config_contents = r#"
        keybinds {
            pane clear-defaults=true {
                bind "r" { SwitchToMode "Resize"; }
            }
        }
    "#;
    let default_config = Config::from_kdl(default_config_contents, None).unwrap();
    let config = Config::from_kdl(config_contents, Some(default_config)).unwrap();
    let ctrl_g_normal_mode_action = config.keybinds.get_actions_for_key_in_mode(
        &InputMode::Normal,
        &KeyWithModifier::new(BareKey::Char('g')).with_ctrl_modifier(),
    );
    let z_in_pane_mode = config
        .keybinds
        .get_actions_for_key_in_mode(&InputMode::Pane, &KeyWithModifier::new(BareKey::Char('z')));
    let r_in_pane_mode = config
        .keybinds
        .get_actions_for_key_in_mode(&InputMode::Pane, &KeyWithModifier::new(BareKey::Char('r')));
    assert_eq!(
        ctrl_g_normal_mode_action,
        Some(&vec![Action::SwitchToMode {
            input_mode: InputMode::Locked
        }]),
        "Keybind in different mode from default config not cleared"
    );
    assert_eq!(
        z_in_pane_mode, None,
        "Keybinding from pane mode in default config cleared"
    );
    assert_eq!(
        r_in_pane_mode,
        Some(&vec![Action::SwitchToMode {
            input_mode: InputMode::Resize
        }]),
        "Keybinding from pane mode in custom config still bound"
    );
}

#[test]
fn can_unbind_multiple_keys_globally() {
    let default_config_contents = r#"
        keybinds {
            normal {
                bind "Ctrl g" { SwitchToMode "Locked"; }
            }
            pane {
                bind "Ctrl g" { SwitchToMode "Locked"; }
                bind "z" { TogglePaneFrames; SwitchToMode "Normal"; }
                bind "r" { TogglePaneFrames; }
            }
        }
    "#;
    let config_contents = r#"
        keybinds {
            unbind "Ctrl g" "z"
            pane {
                bind "t" { SwitchToMode "Tab"; }
            }
        }
    "#;
    let default_config = Config::from_kdl(default_config_contents, None).unwrap();
    let config = Config::from_kdl(config_contents, Some(default_config)).unwrap();
    let ctrl_g_normal_mode_action = config.keybinds.get_actions_for_key_in_mode(
        &InputMode::Normal,
        &KeyWithModifier::new(BareKey::Char('g')).with_ctrl_modifier(),
    );
    let ctrl_g_pane_mode_action = config.keybinds.get_actions_for_key_in_mode(
        &InputMode::Pane,
        &KeyWithModifier::new(BareKey::Char('g')).with_ctrl_modifier(),
    );
    let r_in_pane_mode = config
        .keybinds
        .get_actions_for_key_in_mode(&InputMode::Pane, &KeyWithModifier::new(BareKey::Char('r')));
    let z_in_pane_mode = config
        .keybinds
        .get_actions_for_key_in_mode(&InputMode::Pane, &KeyWithModifier::new(BareKey::Char('z')));
    let t_in_pane_mode = config
        .keybinds
        .get_actions_for_key_in_mode(&InputMode::Pane, &KeyWithModifier::new(BareKey::Char('t')));
    assert_eq!(
        ctrl_g_normal_mode_action, None,
        "First keybind uncleared in one mode"
    );
    assert_eq!(
        ctrl_g_pane_mode_action, None,
        "First keybind uncleared in another mode"
    );
    assert_eq!(z_in_pane_mode, None, "Second keybind cleared as well");
    assert_eq!(
        r_in_pane_mode,
        Some(&vec![Action::TogglePaneFrames]),
        "Unrelated keybinding in default config still bound"
    );
    assert_eq!(
        t_in_pane_mode,
        Some(&vec![Action::SwitchToMode {
            input_mode: InputMode::Tab
        }]),
        "Keybinding from custom config still bound"
    );
}

#[test]
fn can_unbind_multiple_keys_per_single_mode() {
    let default_config_contents = r#"
        keybinds {
            normal {
                bind "Ctrl g" { SwitchToMode "Locked"; }
            }
            pane {
                bind "Ctrl g" { SwitchToMode "Locked"; }
                bind "z" { TogglePaneFrames; SwitchToMode "Normal"; }
                bind "r" { TogglePaneFrames; }
            }
        }
    "#;
    let config_contents = r#"
        keybinds {
            pane {
                unbind "Ctrl g" "z"
                bind "t" { SwitchToMode "Tab"; }
            }
        }
    "#;
    let default_config = Config::from_kdl(default_config_contents, None).unwrap();
    let config = Config::from_kdl(config_contents, Some(default_config)).unwrap();
    let ctrl_g_normal_mode_action = config.keybinds.get_actions_for_key_in_mode(
        &InputMode::Normal,
        &KeyWithModifier::new(BareKey::Char('g')).with_ctrl_modifier(),
    );
    let ctrl_g_pane_mode_action = config.keybinds.get_actions_for_key_in_mode(
        &InputMode::Pane,
        &KeyWithModifier::new(BareKey::Char('g')).with_ctrl_modifier(),
    );
    let r_in_pane_mode = config
        .keybinds
        .get_actions_for_key_in_mode(&InputMode::Pane, &KeyWithModifier::new(BareKey::Char('r')));
    let z_in_pane_mode = config
        .keybinds
        .get_actions_for_key_in_mode(&InputMode::Pane, &KeyWithModifier::new(BareKey::Char('z')));
    let t_in_pane_mode = config
        .keybinds
        .get_actions_for_key_in_mode(&InputMode::Pane, &KeyWithModifier::new(BareKey::Char('t')));
    assert_eq!(
        ctrl_g_normal_mode_action,
        Some(&vec![Action::SwitchToMode {
            input_mode: InputMode::Locked
        }]),
        "Keybind in different mode not cleared"
    );
    assert_eq!(
        ctrl_g_pane_mode_action, None,
        "First Keybind cleared in its mode"
    );
    assert_eq!(
        z_in_pane_mode, None,
        "Second keybind cleared in its mode as well"
    );
    assert_eq!(
        r_in_pane_mode,
        Some(&vec![Action::TogglePaneFrames]),
        "Unrelated keybinding in default config still bound"
    );
    assert_eq!(
        t_in_pane_mode,
        Some(&vec![Action::SwitchToMode {
            input_mode: InputMode::Tab
        }]),
        "Keybinding from custom config still bound"
    );
}

#[test]
fn can_define_shared_keybinds_for_all_modes() {
    let config_contents = r#"
        keybinds {
            shared {
                bind "Ctrl g" { SwitchToMode "Locked"; }
            }
        }
    "#;
    let config = Config::from_kdl(config_contents, None).unwrap();
    for mode in InputMode::iter() {
        let action_in_mode = config.keybinds.get_actions_for_key_in_mode(
            &mode,
            &KeyWithModifier::new(BareKey::Char('g')).with_ctrl_modifier(),
        );
        assert_eq!(
            action_in_mode,
            Some(&vec![Action::SwitchToMode {
                input_mode: InputMode::Locked
            }]),
            "Keybind bound in mode"
        );
    }
}

#[test]
fn can_define_shared_keybinds_with_exclusion() {
    let config_contents = r#"
        keybinds {
            shared_except "locked" {
                bind "Ctrl g" { SwitchToMode "Locked"; }
            }
        }
    "#;
    let config = Config::from_kdl(config_contents, None).unwrap();
    for mode in InputMode::iter() {
        let action_in_mode = config.keybinds.get_actions_for_key_in_mode(
            &mode,
            &KeyWithModifier::new(BareKey::Char('g')).with_ctrl_modifier(),
        );
        if mode == InputMode::Locked {
            assert_eq!(action_in_mode, None, "Keybind unbound in excluded mode");
        } else {
            assert_eq!(
                action_in_mode,
                Some(&vec![Action::SwitchToMode {
                    input_mode: InputMode::Locked
                }]),
                "Keybind bound in mode"
            );
        }
    }
}

#[test]
fn can_define_shared_keybinds_with_inclusion() {
    let config_contents = r#"
        keybinds {
            shared_among "normal" "resize" "pane" {
                bind "Ctrl g" { SwitchToMode "Locked"; }
            }
        }
    "#;
    let config = Config::from_kdl(config_contents, None).unwrap();
    for mode in InputMode::iter() {
        let action_in_mode = config.keybinds.get_actions_for_key_in_mode(
            &mode,
            &KeyWithModifier::new(BareKey::Char('g')).with_ctrl_modifier(),
        );
        if mode == InputMode::Normal || mode == InputMode::Resize || mode == InputMode::Pane {
            assert_eq!(
                action_in_mode,
                Some(&vec![Action::SwitchToMode {
                    input_mode: InputMode::Locked
                }]),
                "Keybind bound in included mode"
            );
        } else {
            assert_eq!(action_in_mode, None, "Keybind unbound in other modes");
        }
    }
}

#[test]
fn keybindings_unbinds_happen_after_binds() {
    let config_contents = r#"
        keybinds {
            pane {
                unbind "z"
                bind "z" { SwitchToMode "Resize"; }
            }
        }
    "#;
    let config = Config::from_kdl(config_contents, None).unwrap();
    let z_in_pane_mode = config
        .keybinds
        .get_actions_for_key_in_mode(&InputMode::Pane, &KeyWithModifier::new(BareKey::Char('z')));
    assert_eq!(z_in_pane_mode, None, "Key was ultimately unbound");
}

#[test]
fn error_received_on_unknown_input_mode() {
    let config_contents = r#"
        keybinds {
            i_do_not_exist {
                bind "z" { SwitchToMode "Resize"; }
            }
        }
    "#;
    let config_error = Config::from_kdl(config_contents, None).unwrap_err();
    assert_snapshot!(format!("{:?}", config_error));
}

#[test]
fn error_received_on_unknown_key_instruction() {
    let config_contents = r#"
        keybinds {
            pane {
                i_am_not_bind_or_unbind
                bind "z" { SwitchToMode "Resize"; }
            }
        }
    "#;
    let config_error = Config::from_kdl(config_contents, None).unwrap_err();
    assert_snapshot!(format!("{:?}", config_error));
}
