use color_eyre::eyre::Result;
use crossterm::event::{KeyCode, KeyEvent, MouseEventKind};
use indexmap::IndexMap;
use ratatui::{prelude::*, widgets::*};
use symbols::scrollbar;
use tokio::sync::mpsc::UnboundedSender;

use super::{Component, Frame};
use crate::{
  action::{Action, MenuItemKind, MenuPreview, MenuTarget},
  app::AppState,
  config::Config,
  database::Rows,
  focus::Focus,
};

#[derive(Debug, Clone)]
struct MenuViewItem {
  name: String,
  materialized: bool,
}

#[derive(Debug, Clone, Default)]
struct MenuSchemaItems {
  tables: Vec<String>,
  views: Vec<MenuViewItem>,
  functions: Vec<String>,
}

#[derive(Debug, Clone)]
struct MenuItem {
  name: String,
  kind: MenuItemKind,
}

#[derive(Debug, Clone)]
enum MenuEntry {
  Header(String),
  Item(MenuItem),
}

#[derive(Debug, Clone, Eq, PartialEq)]
enum CopiedMenuTarget {
  Schema(String),
  Item { schema: String, name: String, kind: MenuItemKind },
}

#[derive(Debug, Clone, Default, Eq, PartialEq)]
pub enum MenuFocus {
  #[default]
  Schema,
  Tables,
}

pub trait SettableTableList<'a> {
  fn set_table_list(&mut self, data: Option<Result<Rows>>);
}

pub trait MenuComponent<'a>: Component + SettableTableList<'a> {}

impl<'a, T> MenuComponent<'a> for T where T: Component + SettableTableList<'a> {}

#[derive(Debug, Clone)]
pub struct Menu {
  command_tx: Option<UnboundedSender<Action>>,
  config: Config,
  table_map: IndexMap<String, MenuSchemaItems>,
  schema_index: usize,
  list_state: ListState,
  menu_focus: MenuFocus,
  search: Option<String>,
  search_focused: bool,
  copied_target: Option<CopiedMenuTarget>,
  loading: bool,
}

impl Menu {
  pub fn new() -> Self {
    Menu {
      command_tx: None,
      config: Config::default(),
      table_map: IndexMap::new(),
      schema_index: 0,
      list_state: ListState::default(),
      menu_focus: MenuFocus::default(),
      search: None,
      search_focused: false,
      copied_target: None,
      loading: true,
    }
  }

  fn draw_loading_skeleton(&self, f: &mut Frame<'_>, area: Rect, focused: bool, focus_hint: &str) {
    let border_style = if focused { Style::default().fg(Color::Green) } else { Style::new().dim() };
    let block = Block::default()
      .title(format!(" 󰦄  loading...{focus_hint} (schema) "))
      .borders(Borders::ALL)
      .border_style(border_style)
      .padding(Padding { left: 0, right: 1, top: 0, bottom: 0 });
    let skeleton = Text::from(vec![
      Line::styled("─ Tables", Style::default().fg(Color::DarkGray)),
      Line::styled(" █████████████", Style::default().fg(Color::DarkGray).dim()),
      Line::styled(" ██████████", Style::default().fg(Color::DarkGray).dim()),
      Line::styled(" ███████████████", Style::default().fg(Color::DarkGray).dim()),
      Line::from(""),
      Line::styled("─ Views", Style::default().fg(Color::DarkGray)),
      Line::styled(" ███████████", Style::default().fg(Color::DarkGray).dim()),
      Line::styled(" ██████████████", Style::default().fg(Color::DarkGray).dim()),
    ]);
    f.render_widget(Paragraph::new(skeleton).block(block), area);
  }

  pub fn change_focus(&mut self, new_focus: MenuFocus) {
    if self.menu_focus != new_focus && self.table_map.keys().len() > 1 {
      match new_focus {
        MenuFocus::Schema => {
          self.list_state = ListState::default();
        },
        MenuFocus::Tables => {
          let entries = self.filtered_entries();
          self.list_state =
            ListState::default().with_selected(Self::first_selectable_index(&entries));
        },
      }
      self.menu_focus = new_focus;
    }
  }

  pub fn scroll_down(&mut self) {
    match self.menu_focus {
      MenuFocus::Tables => {
        let entries = self.filtered_entries();
        if entries.is_empty() {
          self.list_state = ListState::default();
          return;
        }
        let next = match self.list_state.selected() {
          Some(i) => Self::next_selectable_index(&entries, i)
            .or_else(|| Self::first_selectable_index(&entries)),
          None => Self::first_selectable_index(&entries),
        };
        self.list_state = ListState::default().with_selected(next);
      },
      MenuFocus::Schema => {
        self.schema_index = self
          .schema_index
          .saturating_add(1)
          .clamp(0, self.table_map.keys().len().saturating_sub(1))
      },
    }
  }

  pub fn scroll_up(&mut self) {
    match self.menu_focus {
      MenuFocus::Tables => {
        let entries = self.filtered_entries();
        if entries.is_empty() {
          self.list_state = ListState::default();
          return;
        }
        let prev = match self.list_state.selected() {
          Some(i) => Self::previous_selectable_index(&entries, i)
            .or_else(|| Self::last_selectable_index(&entries)),
          None => Self::last_selectable_index(&entries),
        };
        self.list_state = ListState::default().with_selected(prev);
      },
      MenuFocus::Schema => self.schema_index = self.schema_index.saturating_sub(1),
    }
  }

  pub fn scroll_bottom(&mut self) {
    match self.menu_focus {
      MenuFocus::Tables => {
        let entries = self.filtered_entries();
        let last = Self::last_selectable_index(&entries);
        self.list_state = ListState::default().with_selected(last);
      },
      MenuFocus::Schema => {
        self.schema_index = self.table_map.keys().len().saturating_sub(1);
      },
    }
  }

  pub fn scroll_top(&mut self) {
    match self.menu_focus {
      MenuFocus::Tables => {
        let entries = self.filtered_entries();
        let first = Self::first_selectable_index(&entries);
        self.list_state = ListState::default().with_selected(first);
      },
      MenuFocus::Schema => self.schema_index = 0,
    }
  }

  pub fn reset_search(&mut self) {
    self.search = None;
    self.search_focused = false;
    let entries = self.filtered_entries();
    self.list_state = ListState::default().with_selected(Self::first_selectable_index(&entries));
  }

  fn filtered_entries(&self) -> Vec<MenuEntry> {
    let Some((_, items)) = self.table_map.get_index(self.schema_index) else {
      return vec![];
    };
    let matches_search = |name: &str, search: &Option<String>| {
      if let Some(search) = search.as_ref() {
        name.to_lowercase().contains(search.to_lowercase().trim())
      } else {
        true
      }
    };

    let tables: Vec<MenuEntry> = items
      .tables
      .iter()
      .filter(|t| matches_search(t.as_str(), &self.search))
      .cloned()
      .map(|name| MenuEntry::Item(MenuItem { name, kind: MenuItemKind::Table }))
      .collect();

    let views: Vec<MenuEntry> = items
      .views
      .iter()
      .filter(|v| matches_search(v.name.as_str(), &self.search))
      .map(|v| {
        MenuEntry::Item(MenuItem {
          name: v.name.clone(),
          kind: MenuItemKind::View { materialized: v.materialized },
        })
      })
      .collect();

    let functions: Vec<MenuEntry> = items
      .functions
      .iter()
      .filter(|f| matches_search(f.as_str(), &self.search))
      .cloned()
      .map(|name| MenuEntry::Item(MenuItem { name, kind: MenuItemKind::Function }))
      .collect();

    let mut entries = Vec::new();
    if !tables.is_empty() {
      entries.push(MenuEntry::Header("Tables".to_owned()));
      entries.extend(tables);
    }
    if !views.is_empty() {
      entries.push(MenuEntry::Header("Views".to_owned()));
      entries.extend(views);
    }
    if !functions.is_empty() {
      entries.push(MenuEntry::Header("Functions".to_owned()));
      entries.extend(functions);
    }
    entries
  }

  fn first_selectable_index(entries: &[MenuEntry]) -> Option<usize> {
    entries.iter().position(|entry| matches!(entry, MenuEntry::Item(_)))
  }

  fn last_selectable_index(entries: &[MenuEntry]) -> Option<usize> {
    entries.iter().rposition(|entry| matches!(entry, MenuEntry::Item(_)))
  }

  fn next_selectable_index(entries: &[MenuEntry], current: usize) -> Option<usize> {
    entries.iter().enumerate().skip(current + 1).find_map(|(index, entry)| {
      if matches!(entry, MenuEntry::Item(_)) { Some(index) } else { None }
    })
  }

  fn previous_selectable_index(entries: &[MenuEntry], current: usize) -> Option<usize> {
    entries
      .iter()
      .enumerate()
      .take(current)
      .rfind(|(_, entry)| matches!(entry, MenuEntry::Item(_)))
      .map(|(index, _)| index)
  }

  fn selected_item(&self) -> Option<MenuItem> {
    let entries = self.filtered_entries();
    let selected = self.list_state.selected()?;
    match entries.get(selected) {
      Some(MenuEntry::Item(item)) => Some(item.clone()),
      _ => None,
    }
  }

  fn clear_copied_target(&mut self) {
    self.copied_target = None;
  }

  fn copy_focused_name(&mut self) -> Result<()> {
    match self.menu_focus {
      MenuFocus::Schema => {
        if let Some((schema, _)) = self.table_map.get_index(self.schema_index) {
          self.command_tx.as_ref().unwrap().send(Action::CopyData(schema.clone()))?;
          self.copied_target = Some(CopiedMenuTarget::Schema(schema.clone()));
        }
      },
      MenuFocus::Tables => {
        if let Some(item) = self.selected_item()
          && let Some((schema, _)) = self.table_map.get_index(self.schema_index)
        {
          self.command_tx.as_ref().unwrap().send(Action::CopyData(item.name.clone()))?;
          self.copied_target = Some(CopiedMenuTarget::Item {
            schema: schema.clone(),
            name: item.name,
            kind: item.kind,
          });
        }
      },
    }
    Ok(())
  }

  fn copied_schema_name(&self, schema: &str) -> bool {
    matches!(self.copied_target.as_ref(), Some(CopiedMenuTarget::Schema(copied_schema)) if copied_schema == schema)
  }

  fn copied_item_name(&self, schema: &str, item: &MenuItem) -> bool {
    matches!(
      self.copied_target.as_ref(),
      Some(CopiedMenuTarget::Item {
        schema: copied_schema,
        name: copied_name,
        kind: copied_kind,
      }) if copied_schema == schema && copied_name == &item.name && copied_kind == &item.kind
    )
  }
}

impl SettableTableList<'_> for Menu {
  fn set_table_list(&mut self, data: Option<Result<Rows>>) {
    log::info!("setting menu table list");
    match data {
      Some(Ok(rows)) => {
        self.loading = false;
        let mut table_map: IndexMap<String, MenuSchemaItems> = IndexMap::new();
        rows.rows.iter().for_each(|row| {
          let schema = row.first().cloned().unwrap_or_default();
          let name = row.get(1).cloned().unwrap_or_default();
          let kind =
            row.get(2).map(|value| value.to_lowercase()).unwrap_or_else(|| "table".to_owned());
          let entry = table_map.entry(schema.clone()).or_default();
          match kind.as_str() {
            "view" => entry.views.push(MenuViewItem { name, materialized: false }),
            "materialized_view" | "materialized view" | "mview" => {
              entry.views.push(MenuViewItem { name, materialized: true })
            },
            "function" => entry.functions.push(name),
            _ => entry.tables.push(name),
          }
        });
        self.table_map = table_map;
        if self.table_map.keys().len() == 1 {
          self.menu_focus = MenuFocus::Tables;
          let entries = self.filtered_entries();
          self.list_state =
            ListState::default().with_selected(Self::first_selectable_index(&entries));
        } else {
          self.menu_focus = MenuFocus::Schema;
          self.list_state = ListState::default();
        }
      },
      Some(Err(e)) => {
        self.loading = false;
        log::error!("{e}");
      },
      None => self.loading = true,
    }
  }
}

impl Default for Menu {
  fn default() -> Self {
    Self::new()
  }
}

impl Component for Menu {
  fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
    self.command_tx = Some(tx);
    Ok(())
  }

  fn register_config_handler(&mut self, config: Config) -> Result<()> {
    self.config = config;
    Ok(())
  }

  fn handle_mouse_events(
    &mut self,
    mouse: crossterm::event::MouseEvent,
    app_state: &AppState,
  ) -> Result<Option<Action>> {
    if app_state.focus != Focus::Menu || self.loading {
      return Ok(None);
    }
    self.clear_copied_target();
    match mouse.kind {
      MouseEventKind::ScrollDown => self.scroll_down(),
      MouseEventKind::ScrollUp => self.scroll_up(),
      _ => {},
    };
    Ok(None)
  }

  fn handle_key_events(&mut self, key: KeyEvent, app_state: &AppState) -> Result<Option<Action>> {
    if app_state.focus != Focus::Menu {
      return Ok(None);
    }
    if self.loading {
      if key.code == KeyCode::Char('R') {
        self.command_tx.as_ref().unwrap().send(Action::LoadMenu)?;
      }
      return Ok(None);
    }
    self.clear_copied_target();
    match key.code {
      KeyCode::Right => self.change_focus(MenuFocus::Tables),
      KeyCode::Left => self.change_focus(MenuFocus::Schema),
      KeyCode::Down => self.scroll_down(),
      KeyCode::Up => self.scroll_up(),
      KeyCode::Char(c) => {
        if self.search.is_some() && self.search_focused {
          if let Some(search) = self.search.as_mut() {
            search.push(c);
            let entries = self.filtered_entries();
            self.list_state =
              ListState::default().with_selected(Self::first_selectable_index(&entries));
          }
        } else {
          match key.code {
            KeyCode::Char('/') => {
              self.search_focused = true;
              if self.search.is_none() {
                self.search = Some("".to_owned())
              }
            },
            KeyCode::Char('l') => self.change_focus(MenuFocus::Tables),
            KeyCode::Char('h') => self.change_focus(MenuFocus::Schema),
            KeyCode::Char('j') => self.scroll_down(),
            KeyCode::Char('k') => self.scroll_up(),
            KeyCode::Char('g') => self.scroll_top(),
            KeyCode::Char('G') => self.scroll_bottom(),
            KeyCode::Char('R') => self.command_tx.as_ref().unwrap().send(Action::LoadMenu)?,
            KeyCode::Char('y') => self.copy_focused_name()?,
            KeyCode::Char('1') | KeyCode::Char('2') | KeyCode::Char('3') | KeyCode::Char('4') => {
              if let Some(item) = self.selected_item()
                && let Some((schema, _)) = self.table_map.get_index(self.schema_index)
              {
                let preview = match (key.code, item.kind.clone()) {
                  (KeyCode::Char('1'), MenuItemKind::Table)
                  | (KeyCode::Char('1'), MenuItemKind::View { .. }) => Some(MenuPreview::Columns),
                  (KeyCode::Char('2'), MenuItemKind::View { .. }) => Some(MenuPreview::Definition),
                  (KeyCode::Char('2'), MenuItemKind::Table) => Some(MenuPreview::Constraints),
                  (KeyCode::Char('3'), MenuItemKind::Table) => Some(MenuPreview::Indexes),
                  (KeyCode::Char('4'), MenuItemKind::Table) => Some(MenuPreview::Policies),
                  _ => None,
                };
                if let Some(preview) = preview {
                  self.command_tx.as_ref().unwrap().send(Action::MenuPreview(
                    preview,
                    MenuTarget {
                      schema: schema.clone(),
                      name: item.name.clone(),
                      kind: item.kind.clone(),
                    },
                  ))?;
                }
              }
            },
            _ => {},
          }
        }
      },
      KeyCode::Enter => {
        if self.search.is_some() && self.search_focused {
          self.search_focused = false;
        } else if self.menu_focus == MenuFocus::Schema {
          self.change_focus(MenuFocus::Tables);
        } else if let Some(item) = self.selected_item()
          && let Some((schema, _)) = self.table_map.get_index(self.schema_index)
        {
          let preview = match item.kind.clone() {
            MenuItemKind::Function => MenuPreview::Definition,
            _ => MenuPreview::Rows,
          };
          self.command_tx.as_ref().unwrap().send(Action::MenuPreview(
            preview,
            MenuTarget { schema: schema.clone(), name: item.name.clone(), kind: item.kind.clone() },
          ))?;
        }
      },
      KeyCode::Esc => self.reset_search(),
      KeyCode::Backspace => {
        if self.search.is_some() && self.search_focused {
          if let Some(search) = self.search.as_mut() {
            if !search.is_empty() {
              search.pop();
              let entries = self.filtered_entries();
              self.list_state =
                ListState::default().with_selected(Self::first_selectable_index(&entries));
            } else {
              self.reset_search();
            }
          }
        } else if self.menu_focus == MenuFocus::Tables {
          self.change_focus(MenuFocus::Schema);
        }
      },
      _ => {},
    }
    Ok(None)
  }

  fn handle_paste_events(&mut self, text: &str, app_state: &AppState) -> Result<Option<Action>> {
    if app_state.focus != Focus::Menu || self.loading || !self.search_focused {
      return Ok(None);
    }
    if let Some(search) = self.search.as_mut() {
      search.push_str(text);
      let entries = self.filtered_entries();
      self.list_state = ListState::default().with_selected(Self::first_selectable_index(&entries));
    }
    Ok(None)
  }

  fn draw(&mut self, f: &mut Frame<'_>, area: Rect, app_state: &AppState) -> Result<()> {
    let focused = app_state.focus == Focus::Menu;
    let focus_hint = self
      .config
      .keybindings
      .hint_for_action(app_state.focus, &Action::FocusMenu, "<alt+1>")
      .map_or_else(String::new, |hint| format!(" {hint}"));
    if self.loading {
      self.draw_loading_skeleton(f, area, focused, &focus_hint);
      return Ok(());
    }
    let parent_block = Block::default();
    let schema_keys: Vec<String> = self.table_map.keys().cloned().collect();
    let mut constraints: Vec<Constraint> = schema_keys
      .iter()
      .enumerate()
      .map(|(i, _)| match i {
        x if x == self.schema_index => Constraint::Min(5),
        _ => Constraint::Length(1),
      })
      .collect();
    if let Some(search) = self.search.as_ref() {
      constraints.insert(0, Constraint::Length(1));
    }
    let layout = Layout::default()
      .constraints(constraints)
      .direction(Direction::Vertical)
      .split(parent_block.inner(area));
    if let Some(search) = self.search.as_ref() {
      f.render_widget(
        Text::styled(
          "/ ".to_owned() + search.to_owned().as_str(),
          if !focused {
            Style::new().dim()
          } else if self.search_focused {
            Style::default().fg(Color::Yellow)
          } else {
            Style::default()
          },
        ),
        layout[0],
      )
    }
    schema_keys.iter().enumerate().for_each(|(i, k)| {
      let layout_index = if self.search.is_some() { i + 1 } else { i };
      let schema_label =
        if self.copied_schema_name(k) { format!("  {k}") } else { k.to_owned() };
      match i {
        x if x == self.schema_index => {
          let block = Block::default()
            .title(format!(" 󰦄  {schema_label}{focus_hint} (schema) "))
            .borders(Borders::ALL)
            .border_style(if focused && self.menu_focus == MenuFocus::Schema {
              Style::default().fg(Color::Green)
            } else if focused {
              Style::default()
            } else {
              Style::new().dim()
            })
            .padding(Padding { left: 0, right: 1, top: 0, bottom: 0 });
          let block_margin = layout[layout_index].inner(Margin { vertical: 1, horizontal: 0 });
          let entries = self.filtered_entries();
          let entry_length = entries.len();
          let available_height = block.inner(parent_block.inner(area)).height as usize;
          let selected_index = self.list_state.selected();
          let entries_items: Vec<ListItem> = entries
            .iter()
            .enumerate()
            .map(|(i, entry)| match entry {
              MenuEntry::Header(title) => ListItem::new(Text::styled(
                format!("─ {title}"),
                Style::default().fg(Color::DarkGray),
              )),
              MenuEntry::Item(item) => {
                let is_copied = self.copied_item_name(k, item);
                let item_label = match &item.kind {
                  MenuItemKind::View { materialized: true } => {
                    format!("{} (materialized)", item.name)
                  },
                  _ => item.name.clone(),
                };
                let display_name = match &item.kind {
                  MenuItemKind::View { .. } | MenuItemKind::Table | MenuItemKind::Function => {
                    format!("{}{}", if is_copied { "   " } else { " " }, item_label)
                  },
                };
                let is_selected = selected_index == Some(i);
                if is_selected && focused && !self.search_focused {
                  match &item.kind {
                    MenuItemKind::Table => ListItem::new(Text::from(vec![
                      Line::from(display_name),
                      Line::from(if app_state.query_task_running {
                        " ├[...] rows"
                      } else {
                        " ├[<enter>] rows"
                      }),
                      Line::from(if app_state.query_task_running {
                        " ├[...] columns"
                      } else {
                        " ├[1] columns"
                      }),
                      Line::from(if app_state.query_task_running {
                        " ├[...] constraints"
                      } else {
                        " ├[2] constraints"
                      }),
                      Line::from(if app_state.query_task_running {
                        " ├[...] indexes"
                      } else {
                        " ├[3] indexes"
                      }),
                      Line::from(if app_state.query_task_running {
                        " └[...] rls policies"
                      } else {
                        " └[4] rls policies"
                      }),
                    ])),
                    MenuItemKind::View { .. } => ListItem::new(Text::from(vec![
                      Line::from(display_name),
                      Line::from(if app_state.query_task_running {
                        " ├[...] rows"
                      } else {
                        " ├[<enter>] rows"
                      }),
                      Line::from(if app_state.query_task_running {
                        " ├[...] columns"
                      } else {
                        " ├[1] columns"
                      }),
                      Line::from(if app_state.query_task_running {
                        " └[...] schema definition"
                      } else {
                        " └[2] schema definition"
                      }),
                    ])),
                    MenuItemKind::Function => ListItem::new(Text::from(vec![
                      Line::from(display_name),
                      Line::from(if app_state.query_task_running {
                        " └[...] schema definition"
                      } else {
                        " └[<enter>] schema definition"
                      }),
                    ])),
                  }
                } else {
                  ListItem::new(display_name)
                }
              },
            })
            .collect();
          let list = List::default().items(entries_items).block(block).highlight_style(
            Style::default()
              .fg(if focused && !self.search_focused && self.menu_focus == MenuFocus::Tables {
                Color::Green
              } else {
                Color::Gray
              })
              .add_modifier(if focused { Modifier::BOLD } else { Modifier::REVERSED }),
          );
          f.render_stateful_widget(list, layout[layout_index], &mut self.list_state);
          let vertical_scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
            .symbols(scrollbar::VERTICAL)
            .style(if focused && !self.search_focused && self.menu_focus == MenuFocus::Tables {
              Style::default().fg(Color::Green)
            } else {
              Style::default()
            });
          let mut vertical_scrollbar_state =
            ScrollbarState::new(entry_length.saturating_sub(available_height))
              .position(self.list_state.offset());
          f.render_stateful_widget(vertical_scrollbar, block_margin, &mut vertical_scrollbar_state);
        },
        x if x == self.table_map.keys().len().saturating_sub(1) => {
          f.render_widget(
            Text::styled(
              format!("└ {schema_label}"),
              if focused { Style::default() } else { Style::new().dim() },
            ),
            layout[layout_index],
          );
        },
        0 => {
          f.render_widget(
            Text::styled(
              format!("┌ {schema_label}"),
              if focused { Style::default() } else { Style::new().dim() },
            ),
            layout[layout_index],
          );
        },
        _ => f.render_widget(
          Text::styled(
            format!("├ {schema_label}"),
            if focused { Style::default() } else { Style::new().dim() },
          ),
          layout[layout_index],
        ),
      };
    });

    f.render_widget(parent_block, area);
    Ok(())
  }
}

#[cfg(test)]
mod tests {
  use super::*;
  use crate::{components::app_state_with_focus, tui::Event};

  #[test]
  fn loading_skeleton_is_visible_before_the_menu_loads() {
    let mut menu = Menu::new();
    let backend = ratatui::backend::TestBackend::new(40, 12);
    let mut terminal = ratatui::Terminal::new(backend).unwrap();

    terminal
      .draw(|frame| {
        menu.draw(frame, frame.area(), &app_state_with_focus(Focus::Menu)).unwrap();
      })
      .unwrap();

    let rendered: String =
      terminal.backend().buffer().content.iter().map(|cell| cell.symbol()).collect();
    assert!(rendered.contains("loading..."));
    assert!(rendered.contains("██████████"));
  }

  #[test]
  fn loading_skeleton_uses_configured_focus_hint() {
    let mut menu = Menu::new();
    let config: Config = toml::from_str(
      r#"
        [keybindings.Menu]
        "<Alt-1>" = ""
        "<Ctrl-h>" = "FocusMenu"
      "#,
    )
    .unwrap();
    menu.register_config_handler(config).unwrap();
    let backend = ratatui::backend::TestBackend::new(40, 12);
    let mut terminal = ratatui::Terminal::new(backend).unwrap();

    terminal
      .draw(|frame| {
        menu.draw(frame, frame.area(), &app_state_with_focus(Focus::Menu)).unwrap();
      })
      .unwrap();

    let rendered: String =
      terminal.backend().buffer().content.iter().map(|cell| cell.symbol()).collect();
    assert!(rendered.contains("<ctrl+h>"));
    assert!(!rendered.contains("<alt+1>"));
  }

  #[test]
  fn failed_refresh_hides_skeleton_and_preserves_existing_menu() {
    let mut menu = Menu::new();
    menu.set_table_list(Some(Ok(Rows {
      headers: Vec::new(),
      rows: vec![vec!["public".into(), "users".into(), "table".into()]],
      rows_affected: None,
    })));

    menu.set_table_list(None);
    assert!(menu.loading);
    menu.set_table_list(Some(Err(color_eyre::eyre::eyre!("refresh failed"))));

    assert!(!menu.loading);
    assert_eq!(menu.table_map["public"].tables, vec!["users"]);
  }

  #[test]
  fn loading_menu_ignores_hidden_inputs_except_refresh() {
    let mut menu = Menu::new();
    menu.set_table_list(Some(Ok(Rows {
      headers: Vec::new(),
      rows: vec![
        vec!["public".into(), "users".into(), "table".into()],
        vec!["public".into(), "orders".into(), "table".into()],
      ],
      rows_affected: None,
    })));
    let selected = menu.list_state.selected();
    let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
    menu.register_action_handler(tx).unwrap();
    menu.set_table_list(None);

    for code in [KeyCode::Down, KeyCode::Char('/'), KeyCode::Enter, KeyCode::Char('1')] {
      menu
        .handle_key_events(
          KeyEvent::new(code, crossterm::event::KeyModifiers::NONE),
          &app_state_with_focus(Focus::Menu),
        )
        .unwrap();
    }
    menu
      .handle_mouse_events(
        crossterm::event::MouseEvent {
          kind: MouseEventKind::ScrollDown,
          column: 0,
          row: 0,
          modifiers: crossterm::event::KeyModifiers::NONE,
        },
        &app_state_with_focus(Focus::Menu),
      )
      .unwrap();

    assert_eq!(menu.list_state.selected(), selected);
    assert!(menu.search.is_none());
    assert!(rx.try_recv().is_err());

    menu
      .handle_key_events(
        KeyEvent::new(KeyCode::Char('R'), crossterm::event::KeyModifiers::NONE),
        &app_state_with_focus(Focus::Menu),
      )
      .unwrap();
    assert_eq!(rx.try_recv().unwrap(), Action::LoadMenu);
  }

  #[test]
  fn loading_menu_ignores_paste_into_hidden_search() {
    let mut menu = Menu::new();
    menu.search = Some("user".to_owned());
    menu.search_focused = true;

    menu.handle_paste_events("s", &app_state_with_focus(Focus::Menu)).unwrap();

    assert_eq!(menu.search.as_deref(), Some("user"));
  }

  #[test]
  fn paste_appends_to_focused_search_and_selects_first_match() {
    let mut menu = Menu::new();
    menu.loading = false;
    menu.menu_focus = MenuFocus::Tables;
    menu.search = Some("user".to_string());
    menu.search_focused = true;
    menu.table_map.insert(
      "public".to_string(),
      MenuSchemaItems {
        tables: vec!["users".to_string(), "orders".to_string()],
        ..MenuSchemaItems::default()
      },
    );

    menu
      .handle_events(
        Some(Event::Paste("s\n".to_string())),
        Vec::new(),
        &app_state_with_focus(Focus::Menu),
      )
      .unwrap();

    assert_eq!(menu.search.as_deref(), Some("users\n"));
    assert_eq!(menu.list_state.selected(), Some(1));
  }

  #[test]
  fn paste_is_ignored_when_search_is_not_focused() {
    let mut menu = Menu::new();
    menu.loading = false;
    menu.search = Some("user".to_string());

    menu
      .handle_events(
        Some(Event::Paste("s".to_string())),
        Vec::new(),
        &app_state_with_focus(Focus::Menu),
      )
      .unwrap();

    assert_eq!(menu.search.as_deref(), Some("user"));
  }
}
