use std::{collections::HashMap, path::PathBuf};

use color_eyre::eyre::{self, Result};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use derive_deref::{Deref, DerefMut};
use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
use ratatui::style::{Color, Modifier, Style};
use serde::{Deserialize, de, de::Deserializer};

use crate::{action::Action, cli::Driver, focus::Focus, keyring::Password};

// percent encoding for passwords in connection strings
const FRAGMENT: &AsciiSet = &CONTROLS
  .add(b' ')
  .add(b'"')
  .add(b'<')
  .add(b'>')
  .add(b'`')
  .add(b'#')
  .add(b'{')
  .add(b'}')
  .add(b'|')
  .add(b'^')
  .add(b'\\')
  .add(b'[')
  .add(b']')
  .add(b'$')
  .add(b'&')
  .add(b'(')
  .add(b')')
  .add(b':')
  .add(b';')
  .add(b'=')
  .add(b'?')
  .add(b'@')
  .add(b'!')
  .add(b'~')
  .add(b'\'')
  .add(b'*')
  .add(b'+')
  .add(b',')
  .add(b'/');

const CONFIG: &str = include_str!("../.config/rainfrog_config.toml");
const CONFIG_FILE_CANDIDATES: [(&str, config::FileFormat); 5] = [
  ("rainfrog_config.json5", config::FileFormat::Json5),
  ("rainfrog_config.json", config::FileFormat::Json),
  ("rainfrog_config.yaml", config::FileFormat::Yaml),
  ("rainfrog_config.toml", config::FileFormat::Toml),
  ("rainfrog_config.ini", config::FileFormat::Ini),
];
const PREFERRED_CONFIG_FILENAME: &str = "rainfrog_config.toml";

#[derive(Clone, Debug, Deserialize, Default)]
pub struct AppConfig {
  #[serde(default)]
  pub _data_dir: PathBuf,
  #[serde(default)]
  pub _config_dir: PathBuf,
  #[serde(default)]
  pub _favorites_dir: PathBuf,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(untagged)]
pub enum ConnectionString {
  Raw {
    connection_string: String,
  },
  Structured {
    #[serde(flatten)]
    details: StructuredConnection,
  },
}

#[derive(Clone, Debug, Deserialize)]
pub struct StructuredConnection {
  pub host: String,
  pub port: u32,
  pub database: String,
  pub username: String,
}

#[derive(Clone, Debug, Deserialize)]
pub struct DatabaseConnection {
  pub driver: Driver,
  #[serde(flatten)]
  pub connection: ConnectionString,
  #[serde(default)]
  pub default: bool,
  #[serde(default)]
  pub enable_cleartext_plugin: Option<bool>,
}

#[derive(Clone, Debug, Default, Deserialize)]
pub struct Config {
  #[serde(default, flatten)]
  pub config: AppConfig,
  #[serde(default)]
  pub keybindings: KeyBindings,
  #[serde(default)]
  pub styles: Styles,
  #[serde(default)]
  pub settings: Settings,
  #[serde(default)]
  pub db: HashMap<String, DatabaseConnection>,
}

impl StructuredConnection {
  pub fn connection_string(&self, driver: Driver, password: Password) -> Result<String> {
    let encoded_password = utf8_percent_encode(password.as_ref(), FRAGMENT);
    match driver {
      Driver::Postgres => Ok(format!(
        "postgresql://{}:{}@{}:{}/{}",
        self.username, encoded_password, self.host, self.port, self.database
      )),
      Driver::MySql => Ok(format!(
        "mysql://{}:{}@{}:{}/{}",
        self.username, encoded_password, self.host, self.port, self.database
      )),
      Driver::Sqlite => Err(eyre::Report::msg("Sqlite only supports raw connection strings")),
      #[cfg(feature = "oracle")]
      Driver::Oracle => Ok(format!(
        "jdbc:oracle:thin:{}/{}@//{}:{}/{}",
        self.username, encoded_password, self.host, self.port, self.database
      )),
      #[cfg(feature = "duckdb")]
      Driver::DuckDb => Err(eyre::Report::msg("DuckDb only supports raw connection strings")),
    }
  }
}

impl Config {
  pub fn new() -> Result<Self, config::ConfigError> {
    let default_config: Config = toml::from_str(CONFIG).unwrap();
    let data_dir = crate::utils::get_data_dir();
    let config_dir = crate::utils::get_config_dir();
    let favorites_dir = crate::utils::get_favorites_dir();
    let mut builder = config::Config::builder()
      .set_default("_data_dir", data_dir.to_str().unwrap())?
      .set_default("_config_dir", config_dir.to_str().unwrap())?
      .set_default("_favorites_dir", favorites_dir.to_str().unwrap())?;

    let mut found_config = false;
    for (file, format) in &CONFIG_FILE_CANDIDATES {
      builder = builder
        .add_source(config::File::from(config_dir.join(file)).format(*format).required(false));
      if config_dir.join(file).exists() {
        found_config = true
      }
    }
    if !found_config {
      log::error!("No configuration file found. Application may not behave as expected");
    }

    let mut cfg: Self = builder.build()?.try_deserialize()?;

    cfg.keybindings.merge_defaults(&default_config.keybindings);
    for (focus, default_styles) in default_config.styles.iter() {
      let user_styles = cfg.styles.entry(*focus).or_default();
      for (style_key, style) in default_styles.iter() {
        user_styles.entry(style_key.clone()).or_insert_with(|| *style);
      }
    }
    match cfg.settings.mouse_mode {
      Some(_) => {},
      None => {
        cfg.settings.mouse_mode = default_config.settings.mouse_mode;
      },
    };
    match cfg.settings.data_compact_columns {
      Some(_) => {},
      None => {
        cfg.settings.data_compact_columns = default_config.settings.data_compact_columns;
      },
    };
    match cfg.settings.data_row_spacer {
      Some(_) => {},
      None => {
        cfg.settings.data_row_spacer = default_config.settings.data_row_spacer;
      },
    };
    if cfg.settings.autocomplete_enabled.is_none() {
      cfg.settings.autocomplete_enabled = default_config.settings.autocomplete_enabled;
    }
    if cfg.settings.autocomplete_debounce_ms.is_none() {
      cfg.settings.autocomplete_debounce_ms = default_config.settings.autocomplete_debounce_ms;
    }
    if cfg.settings.autocomplete_trigger_len.is_none() {
      cfg.settings.autocomplete_trigger_len = default_config.settings.autocomplete_trigger_len;
    }
    if cfg.settings.autopairs_enabled.is_none() {
      cfg.settings.autopairs_enabled = default_config.settings.autopairs_enabled;
    }

    Ok(cfg)
  }
}

pub fn preferred_config_path() -> PathBuf {
  crate::utils::get_config_dir().join(PREFERRED_CONFIG_FILENAME)
}

pub fn existing_config_path() -> Option<PathBuf> {
  let config_dir = crate::utils::get_config_dir();
  CONFIG_FILE_CANDIDATES.iter().map(|(name, _)| config_dir.join(name)).find(|path| path.exists())
}

pub fn default_config_contents() -> &'static str {
  CONFIG
}

#[derive(Clone, Debug, Default, Deref, DerefMut)]
pub struct KeyBindings(pub HashMap<Focus, HashMap<Vec<KeyEvent>, Action>>);

impl KeyBindings {
  fn merge_defaults(&mut self, defaults: &Self) {
    for (focus, default_bindings) in defaults.iter() {
      let user_bindings = self.entry(*focus).or_default();
      for (key, action) in default_bindings {
        user_bindings.entry(key.clone()).or_insert_with(|| action.clone());
      }
    }
  }

  pub fn hint_for_action(
    &self,
    focus: Focus,
    action: &Action,
    preferred_hint: &str,
  ) -> Option<String> {
    let hints = self.hints_for_action(focus, action);

    hints
      .iter()
      .find(|hint| hint.as_str() == preferred_hint)
      .cloned()
      .or_else(|| hints.into_iter().next())
  }

  pub fn hints_for_action(&self, focus: Focus, action: &Action) -> Vec<String> {
    let Some(bindings) = self.get(&focus) else {
      return Vec::new();
    };
    let mut hints = bindings
      .iter()
      .filter(|(_, configured_action)| *configured_action == action)
      .map(|(key_sequence, _)| key_sequence_to_hint(key_sequence))
      .collect::<Vec<_>>();
    hints.sort_unstable();
    hints
  }
}

impl<'de> Deserialize<'de> for KeyBindings {
  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  where
    D: Deserializer<'de>,
  {
    let parsed_map = HashMap::<Focus, HashMap<String, Action>>::deserialize(deserializer)?;

    let keybindings = parsed_map
      .into_iter()
      .map(|(focus, inner_map)| {
        let converted_inner_map = inner_map
          .into_iter()
          .map(|(key_str, cmd)| {
            // a typo in the config file must not abort the process
            parse_key_sequence(&key_str)
              .map(|keys| (keys, cmd))
              .map_err(|e| de::Error::custom(format!("invalid keybinding \"{key_str}\": {e}")))
          })
          .collect::<Result<HashMap<_, _>, D::Error>>()?;
        Ok((focus, converted_inner_map))
      })
      .collect::<Result<HashMap<_, _>, D::Error>>()?;

    Ok(KeyBindings(keybindings))
  }
}

fn parse_key_event(raw: &str) -> Result<KeyEvent, String> {
  let raw_lower = raw.to_ascii_lowercase();
  let (remaining, modifiers) = extract_modifiers(&raw_lower);
  parse_key_code_with_modifiers(remaining, modifiers)
}

fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) {
  let mut modifiers = KeyModifiers::empty();
  let mut current = raw;

  loop {
    match current {
      rest if rest.starts_with("ctrl-") => {
        modifiers.insert(KeyModifiers::CONTROL);
        current = &rest[5..];
      },
      rest if rest.starts_with("alt-") => {
        modifiers.insert(KeyModifiers::ALT);
        current = &rest[4..];
      },
      rest if rest.starts_with("shift-") => {
        modifiers.insert(KeyModifiers::SHIFT);
        current = &rest[6..];
      },
      _ => break, // break out of the loop if no known prefix is detected
    };
  }

  (current, modifiers)
}

fn parse_key_code_with_modifiers(
  raw: &str,
  mut modifiers: KeyModifiers,
) -> Result<KeyEvent, String> {
  let c = match raw {
    "esc" => KeyCode::Esc,
    "enter" => KeyCode::Enter,
    "left" => KeyCode::Left,
    "right" => KeyCode::Right,
    "up" => KeyCode::Up,
    "down" => KeyCode::Down,
    "home" => KeyCode::Home,
    "end" => KeyCode::End,
    "pageup" => KeyCode::PageUp,
    "pagedown" => KeyCode::PageDown,
    "backtab" => {
      modifiers.insert(KeyModifiers::SHIFT);
      KeyCode::BackTab
    },
    "backspace" => KeyCode::Backspace,
    "delete" => KeyCode::Delete,
    "insert" => KeyCode::Insert,
    "f1" => KeyCode::F(1),
    "f2" => KeyCode::F(2),
    "f3" => KeyCode::F(3),
    "f4" => KeyCode::F(4),
    "f5" => KeyCode::F(5),
    "f6" => KeyCode::F(6),
    "f7" => KeyCode::F(7),
    "f8" => KeyCode::F(8),
    "f9" => KeyCode::F(9),
    "f10" => KeyCode::F(10),
    "f11" => KeyCode::F(11),
    "f12" => KeyCode::F(12),
    "space" => KeyCode::Char(' '),
    "hyphen" => KeyCode::Char('-'),
    "minus" => KeyCode::Char('-'),
    "tab" => KeyCode::Tab,
    c if c.len() == 1 => {
      let mut c = c.chars().next().unwrap();
      if modifiers.contains(KeyModifiers::SHIFT) {
        c = c.to_ascii_uppercase();
      }
      KeyCode::Char(c)
    },
    _ => return Err(format!("Unable to parse {raw}")),
  };
  Ok(KeyEvent::new(c, modifiers))
}

pub fn key_event_to_string(key_event: &KeyEvent) -> String {
  let char;
  let key_code = match key_event.code {
    KeyCode::Backspace => "backspace",
    KeyCode::Enter => "enter",
    KeyCode::Left => "left",
    KeyCode::Right => "right",
    KeyCode::Up => "up",
    KeyCode::Down => "down",
    KeyCode::Home => "home",
    KeyCode::End => "end",
    KeyCode::PageUp => "pageup",
    KeyCode::PageDown => "pagedown",
    KeyCode::Tab => "tab",
    KeyCode::BackTab => "backtab",
    KeyCode::Delete => "delete",
    KeyCode::Insert => "insert",
    KeyCode::F(c) => {
      char = format!("f{c}");
      &char
    },
    KeyCode::Char(' ') => "space",
    KeyCode::Char(c) => {
      char = c.to_string();
      &char
    },
    KeyCode::Esc => "esc",
    KeyCode::Null => "",
    KeyCode::CapsLock => "",
    KeyCode::Menu => "",
    KeyCode::ScrollLock => "",
    KeyCode::Media(_) => "",
    KeyCode::NumLock => "",
    KeyCode::PrintScreen => "",
    KeyCode::Pause => "",
    KeyCode::KeypadBegin => "",
    KeyCode::Modifier(_) => "",
  };

  let mut modifiers = Vec::with_capacity(3);

  if key_event.modifiers.intersects(KeyModifiers::CONTROL) {
    modifiers.push("ctrl");
  }

  if key_event.modifiers.intersects(KeyModifiers::SHIFT) {
    modifiers.push("shift");
  }

  if key_event.modifiers.intersects(KeyModifiers::ALT) {
    modifiers.push("alt");
  }

  let mut key = modifiers.join("-");

  if !key.is_empty() {
    key.push('-');
  }
  key.push_str(key_code);

  key
}

fn key_sequence_to_hint(key_sequence: &[KeyEvent]) -> String {
  key_sequence
    .iter()
    .map(|key| {
      let hint = key_event_to_string(key)
        .replace("ctrl-", "ctrl+")
        .replace("shift-", "shift+")
        .replace("alt-", "alt+");
      format!("<{hint}>")
    })
    .collect()
}

pub fn parse_key_sequence(raw: &str) -> Result<Vec<KeyEvent>, String> {
  if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() {
    return Err(format!("Unable to parse `{raw}`"));
  }
  let raw = if !raw.contains("><") {
    let raw = raw.strip_prefix('<').unwrap_or(raw);

    raw.strip_prefix('>').unwrap_or(raw)
  } else {
    raw
  };
  let sequences = raw
    .split("><")
    .map(|seq| {
      if let Some(s) = seq.strip_prefix('<') {
        s
      } else if let Some(s) = seq.strip_suffix('>') {
        s
      } else {
        seq
      }
    })
    .collect::<Vec<_>>();

  sequences.into_iter().map(parse_key_event).collect()
}

#[derive(Clone, Debug, Default, Deserialize)]
pub struct Settings {
  pub mouse_mode: Option<bool>,
  pub data_compact_columns: Option<bool>,
  pub data_row_spacer: Option<bool>,
  pub autocomplete_enabled: Option<bool>,
  pub autocomplete_debounce_ms: Option<u64>,
  pub autocomplete_trigger_len: Option<usize>,
  pub autopairs_enabled: Option<bool>,
}

#[derive(Clone, Debug, Default, Deref, DerefMut)]
pub struct Styles(pub HashMap<Focus, HashMap<String, Style>>);

impl<'de> Deserialize<'de> for Styles {
  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  where
    D: Deserializer<'de>,
  {
    let parsed_map = HashMap::<Focus, HashMap<String, String>>::deserialize(deserializer)?;

    let styles = parsed_map
      .into_iter()
      .map(|(focus, inner_map)| {
        let converted_inner_map =
          inner_map.into_iter().map(|(str, style)| (str, parse_style(&style))).collect();
        (focus, converted_inner_map)
      })
      .collect();

    Ok(Styles(styles))
  }
}

pub fn parse_style(line: &str) -> Style {
  let (foreground, background) =
    line.split_at(line.to_lowercase().find("on ").unwrap_or(line.len()));
  let foreground = process_color_string(foreground);
  let background = process_color_string(&background.replace("on ", ""));

  let mut style = Style::default();
  if let Some(fg) = parse_color(&foreground.0) {
    style = style.fg(fg);
  }
  if let Some(bg) = parse_color(&background.0) {
    style = style.bg(bg);
  }
  style = style.add_modifier(foreground.1 | background.1);
  style
}

fn process_color_string(color_str: &str) -> (String, Modifier) {
  let color = color_str
    .replace("grey", "gray")
    .replace("bright ", "")
    .replace("bold ", "")
    .replace("underline ", "")
    .replace("inverse ", "");

  let mut modifiers = Modifier::empty();
  if color_str.contains("underline") {
    modifiers |= Modifier::UNDERLINED;
  }
  if color_str.contains("bold") {
    modifiers |= Modifier::BOLD;
  }
  if color_str.contains("inverse") {
    modifiers |= Modifier::REVERSED;
  }

  (color, modifiers)
}

fn parse_color(s: &str) -> Option<Color> {
  let s = s.trim_start();
  let s = s.trim_end();
  if s.contains("bright color") {
    let s = s.trim_start_matches("bright ");
    let c = s.trim_start_matches("color").parse::<u8>().unwrap_or_default();
    Some(Color::Indexed(c.wrapping_shl(8)))
  } else if s.contains("color") {
    let c = s.trim_start_matches("color").parse::<u8>().unwrap_or_default();
    Some(Color::Indexed(c))
  } else if s.contains("gray") {
    let c = 232 + s.trim_start_matches("gray").parse::<u8>().unwrap_or_default();
    Some(Color::Indexed(c))
  } else if s.contains("rgb") {
    let bytes = s.as_bytes();
    let digit_at = |i: usize| -> u8 {
      bytes.get(i).and_then(|b| (*b as char).to_digit(10)).unwrap_or_default() as u8
    };
    let red = digit_at(3);
    let green = digit_at(4);
    let blue = digit_at(5);
    let c = 16 + red * 36 + green * 6 + blue;
    Some(Color::Indexed(c))
  } else if s == "bold black" {
    Some(Color::Indexed(8))
  } else if s == "bold red" {
    Some(Color::Indexed(9))
  } else if s == "bold green" {
    Some(Color::Indexed(10))
  } else if s == "bold yellow" {
    Some(Color::Indexed(11))
  } else if s == "bold blue" {
    Some(Color::Indexed(12))
  } else if s == "bold magenta" {
    Some(Color::Indexed(13))
  } else if s == "bold cyan" {
    Some(Color::Indexed(14))
  } else if s == "bold white" {
    Some(Color::Indexed(15))
  } else if s == "black" {
    Some(Color::Indexed(0))
  } else if s == "red" {
    Some(Color::Indexed(1))
  } else if s == "green" {
    Some(Color::Indexed(2))
  } else if s == "yellow" {
    Some(Color::Indexed(3))
  } else if s == "blue" {
    Some(Color::Indexed(4))
  } else if s == "magenta" {
    Some(Color::Indexed(5))
  } else if s == "cyan" {
    Some(Color::Indexed(6))
  } else if s == "white" {
    Some(Color::Indexed(7))
  } else {
    None
  }
}

#[cfg(test)]
mod tests {
  use pretty_assertions::assert_eq;

  use super::*;

  #[test]
  fn test_parse_style_default() {
    let style = parse_style("");
    assert_eq!(style, Style::default());
  }

  #[test]
  fn test_parse_style_foreground() {
    let style = parse_style("red");
    assert_eq!(style.fg, Some(Color::Indexed(1)));
  }

  #[test]
  fn test_parse_style_background() {
    let style = parse_style("on blue");
    assert_eq!(style.bg, Some(Color::Indexed(4)));
  }

  #[test]
  fn test_parse_style_modifiers() {
    let style = parse_style("underline red on blue");
    assert_eq!(style.fg, Some(Color::Indexed(1)));
    assert_eq!(style.bg, Some(Color::Indexed(4)));
  }

  #[test]
  fn test_process_color_string() {
    let (color, modifiers) = process_color_string("underline bold inverse gray");
    assert_eq!(color, "gray");
    assert!(modifiers.contains(Modifier::UNDERLINED));
    assert!(modifiers.contains(Modifier::BOLD));
    assert!(modifiers.contains(Modifier::REVERSED));
  }

  #[test]
  fn test_parse_color_rgb() {
    let color = parse_color("rgb123");
    let expected = 16 + 36 + 2 * 6 + 3;
    assert_eq!(color, Some(Color::Indexed(expected)));
  }

  #[test]
  fn test_parse_color_rgb_malformed_short() {
    let color = parse_color("rgb");
    assert_eq!(color, Some(Color::Indexed(16)));
  }

  #[test]
  fn test_parse_color_rgb_malformed_one_digit() {
    let color = parse_color("rgb1");
    let expected = 16 + 36;
    assert_eq!(color, Some(Color::Indexed(expected)));
  }

  #[test]
  fn test_parse_color_unknown() {
    let color = parse_color("unknown");
    assert_eq!(color, None);
  }

  #[test]
  fn test_invalid_keybinding_is_an_error_not_a_panic() {
    let toml_src = "[keybindings.Editor]\n\"<bogus-key>\" = \"Quit\"\n";
    let err = toml::from_str::<Config>(toml_src).unwrap_err();
    assert!(err.to_string().contains("invalid keybinding \"<bogus-key>\""), "got: {err}");
  }

  #[test]
  fn test_invalid_non_empty_action_is_an_error() {
    let toml_src = "[keybindings.Editor]\n\"x\" = \"NotAnAction\"\n";
    let err = toml::from_str::<Config>(toml_src).unwrap_err();
    assert!(err.to_string().contains("NotAnAction"), "got: {err}");
  }

  #[test]
  fn test_valid_keybinding_still_parses() {
    let toml_src = "[keybindings.Editor]\n\"<Alt-e>\" = \"Quit\"\n";
    let c: Config = toml::from_str(toml_src).unwrap();
    assert_eq!(
      c.keybindings
        .get(&Focus::Editor)
        .unwrap()
        .get(&parse_key_sequence("<Alt-e>").unwrap())
        .unwrap(),
      &Action::Quit
    );
  }

  #[test]
  fn test_empty_action_disables_default_keybinding() {
    let toml_src = r#"
      [keybindings.Menu]
      "<Alt-1>" = ""
      "<Ctrl-h>" = "FocusMenu"
    "#;
    let mut config: Config = toml::from_str(toml_src).unwrap();
    let default_config: Config = toml::from_str(CONFIG).unwrap();

    config.keybindings.merge_defaults(&default_config.keybindings);

    let menu_bindings = config.keybindings.get(&Focus::Menu).unwrap();
    assert_eq!(menu_bindings.get(&parse_key_sequence("<Alt-1>").unwrap()), Some(&Action::NoOp));
    assert_eq!(
      menu_bindings.get(&parse_key_sequence("<Ctrl-h>").unwrap()),
      Some(&Action::FocusMenu)
    );
  }

  #[test]
  fn test_focus_hint_uses_replacement_for_disabled_preferred_binding() {
    let toml_src = r#"
      [keybindings.Menu]
      "<Alt-1>" = ""
      "<Ctrl-h>" = "FocusMenu"
    "#;
    let mut config: Config = toml::from_str(toml_src).unwrap();
    let default_config: Config = toml::from_str(CONFIG).unwrap();
    config.keybindings.merge_defaults(&default_config.keybindings);

    assert_eq!(
      config.keybindings.hint_for_action(Focus::Menu, &Action::FocusMenu, "<alt+1>"),
      Some("<ctrl+h>".to_owned())
    );
  }

  #[test]
  fn test_focus_hint_preserves_active_preferred_binding() {
    let config: Config = toml::from_str(CONFIG).unwrap();

    assert_eq!(
      config.keybindings.hint_for_action(Focus::Menu, &Action::FocusMenu, "<alt+1>"),
      Some("<alt+1>".to_owned())
    );
  }

  #[test]
  fn test_focus_hint_is_absent_without_an_active_binding() {
    let config: Config = toml::from_str(
      r#"
        [keybindings.Menu]
        "<Alt-1>" = ""
      "#,
    )
    .unwrap();

    assert_eq!(
      config.keybindings.hint_for_action(Focus::Menu, &Action::FocusMenu, "<alt+1>"),
      None
    );
  }

  #[test]
  fn test_config() -> Result<()> {
    let c: Config = toml::from_str(CONFIG)?;
    assert_eq!(
      c.keybindings
        .get(&Focus::Menu)
        .unwrap()
        .get(&parse_key_sequence("<q>").unwrap_or_default())
        .unwrap(),
      &Action::AbortQuery
    );
    assert_eq!(
      c.keybindings
        .get(&Focus::Editor)
        .unwrap()
        .get(&parse_key_sequence("<Alt-e>").unwrap_or_default())
        .unwrap(),
      &Action::RequestExternalEditor
    );
    assert_eq!(
      c.keybindings
        .get(&Focus::Editor)
        .unwrap()
        .get(&parse_key_sequence("<F6>").unwrap_or_default())
        .unwrap(),
      &Action::RequestExternalEditor
    );
    assert_eq!(c.settings.mouse_mode, Some(true));
    assert_eq!(c.settings.autocomplete_enabled, Some(true));
    assert_eq!(c.settings.autocomplete_debounce_ms, Some(100));
    assert_eq!(c.settings.autocomplete_trigger_len, Some(1));
    assert_eq!(c.settings.autopairs_enabled, Some(true));
    Ok(())
  }

  #[test]
  fn test_default_config_section_resize_bindings() {
    let c: Config = toml::from_str(CONFIG).unwrap();
    for focus in [Focus::Menu, Focus::Editor, Focus::History, Focus::Data, Focus::Favorites] {
      let keymap = c.keybindings.get(&focus).unwrap();
      assert_eq!(
        keymap.get(&parse_key_sequence("<Alt-minus>").unwrap()),
        Some(&Action::DecreaseSectionSize),
        "missing decrease binding for {focus:?}"
      );
      assert_eq!(
        keymap.get(&parse_key_sequence("<Alt-+>").unwrap()),
        Some(&Action::IncreaseSectionSize),
        "missing increase binding for {focus:?}"
      );
      assert_eq!(
        keymap.get(&parse_key_sequence("<Alt-shift-+>").unwrap()),
        Some(&Action::IncreaseSectionSize),
        "missing shifted increase binding for {focus:?}"
      );
    }
    let popup = c.keybindings.get(&Focus::PopUp).unwrap();
    assert!(
      !popup
        .values()
        .any(|a| matches!(a, Action::IncreaseSectionSize | Action::DecreaseSectionSize)),
      "popup must not have resize bindings"
    );
  }

  #[test]
  fn test_simple_keys() {
    assert_eq!(
      parse_key_event("a").unwrap(),
      KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())
    );

    assert_eq!(
      parse_key_event("enter").unwrap(),
      KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())
    );

    assert_eq!(parse_key_event("esc").unwrap(), KeyEvent::new(KeyCode::Esc, KeyModifiers::empty()));
  }

  #[test]
  fn test_with_modifiers() {
    assert_eq!(
      parse_key_event("ctrl-a").unwrap(),
      KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
    );

    assert_eq!(
      parse_key_event("alt-enter").unwrap(),
      KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)
    );

    assert_eq!(
      parse_key_event("shift-esc").unwrap(),
      KeyEvent::new(KeyCode::Esc, KeyModifiers::SHIFT)
    );
  }

  #[test]
  fn test_multiple_modifiers() {
    assert_eq!(
      parse_key_event("ctrl-alt-a").unwrap(),
      KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL | KeyModifiers::ALT)
    );

    assert_eq!(
      parse_key_event("ctrl-shift-enter").unwrap(),
      KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL | KeyModifiers::SHIFT)
    );
  }

  #[test]
  fn test_reverse_multiple_modifiers() {
    assert_eq!(
      key_event_to_string(&KeyEvent::new(
        KeyCode::Char('a'),
        KeyModifiers::CONTROL | KeyModifiers::ALT
      )),
      "ctrl-alt-a".to_string()
    );
  }

  #[test]
  fn test_function_key_formats_with_parseable_syntax() {
    let key = KeyEvent::new(KeyCode::F(2), KeyModifiers::empty());
    assert_eq!(key_event_to_string(&key), "f2");
    assert_eq!(parse_key_sequence("<f2>").unwrap(), vec![key]);
  }

  #[test]
  fn test_invalid_keys() {
    assert!(parse_key_event("invalid-key").is_err());
    assert!(parse_key_event("ctrl-invalid-key").is_err());
  }

  #[test]
  fn test_case_insensitivity() {
    assert_eq!(
      parse_key_event("CTRL-a").unwrap(),
      KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
    );

    assert_eq!(
      parse_key_event("AlT-eNtEr").unwrap(),
      KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT)
    );
  }

  #[test]
  fn database_connection_enable_cleartext_plugin_true() {
    let toml = r#"
      driver = "mysql"
      host = "localhost"
      port = 3306
      database = "mydb"
      username = "user"
      enable_cleartext_plugin = true
    "#;
    let conn: DatabaseConnection = toml::from_str(toml).unwrap();
    assert_eq!(conn.enable_cleartext_plugin, Some(true));
  }

  #[test]
  fn database_connection_enable_cleartext_plugin_false() {
    let toml = r#"
      driver = "mysql"
      host = "localhost"
      port = 3306
      database = "mydb"
      username = "user"
      enable_cleartext_plugin = false
    "#;
    let conn: DatabaseConnection = toml::from_str(toml).unwrap();
    assert_eq!(conn.enable_cleartext_plugin, Some(false));
  }

  #[test]
  fn database_connection_enable_cleartext_plugin_omitted() {
    let toml = r#"
      driver = "mysql"
      host = "localhost"
      port = 3306
      database = "mydb"
      username = "user"
    "#;
    let conn: DatabaseConnection = toml::from_str(toml).unwrap();
    assert_eq!(conn.enable_cleartext_plugin, None);
  }
}
