mod command_bar;
mod commands;
mod lib_tab;
mod lib_tree;
mod mode;
mod pdf_viewer;
mod pub_detail;
mod pub_list;
mod tab_bar;
mod tauri;

use std::{
    cell::{Cell, RefCell},
    collections::HashMap,
    rc::Rc,
};

use brittle_keymap::{actions, Key, KeyCode, KeymapState, Outcome, default_bindings};
use command_bar::{CommandBar, provide_search_query};
use leptos::prelude::*;
use lib_tab::LibTab;
use mode::{AppMode, provide_mode};
use pdf_viewer::PdfViewer;
use tab_bar::TabBar;
use web_sys::KeyboardEvent;

fn main() {
    leptos::mount::mount_to_body(App);
}

// ── Action event ──────────────────────────────────────────────────────────────

/// A dispatched keymap action.
///
/// The `seq` field increments monotonically so that firing the same action
/// twice in succession produces a distinct signal value and triggers
/// reactive updates both times.
#[derive(Clone, PartialEq, Eq)]
pub struct ActionEvent {
    pub name: String,
    pub count: u32,
    seq: u32,
}

thread_local! {
    static ACTION_SEQ: Cell<u32> = const { Cell::new(0) };
}

pub fn next_seq() -> u32 {
    ACTION_SEQ.with(|s| {
        let n = s.get();
        s.set(n.wrapping_add(1));
        n
    })
}

/// Context handle giving components read access to the last dispatched action.
#[derive(Clone, Copy)]
pub struct KeymapAction(pub ReadSignal<Option<ActionEvent>>);

// ── Tab types ─────────────────────────────────────────────────────────────────

/// A single open tab in the application.
#[derive(Clone, PartialEq, Eq)]
pub enum AppTab {
    /// The persistent library / reference browser tab (always index 0).
    Library,
    /// A PDF viewer tab for a specific reference.
    Pdf {
        /// UUID string of the reference whose PDF is being viewed.
        ref_id: String,
        /// Short display name shown in the tab (typically the cite key).
        title: String,
    },
}

// ── Theme context ─────────────────────────────────────────────────────────────

/// Context handle for the current colour theme (`"dark"` or `"light"`).
///
/// Writable so `CommandBar` can update it when the user types `:theme …`.
#[derive(Clone, Copy)]
pub struct ThemeContext(pub RwSignal<String>);

fn provide_theme() {
    let theme = RwSignal::new("dark".to_string());
    provide_context(ThemeContext(theme));

    // Reactively apply the `data-theme` attribute to `<html>`.
    Effect::new(move |_| {
        let t = theme.get();
        if let Some(doc) = web_sys::window().and_then(|w| w.document()) {
            if let Some(el) = doc.document_element() {
                let _ = el.set_attribute("data-theme", &t);
            }
        }
    });

    // Load the persisted theme from backend config; replace the default if
    // found.  Runs after the effect is live, so the DOM is updated immediately
    // once the IPC call returns.
    leptos::task::spawn_local(async move {
        if let Ok(t) = crate::tauri::get_theme().await {
            theme.set(t);
        }
    });
}

// ── Reload trigger ────────────────────────────────────────────────────────────

/// Incrementing counter that components watch to know when to reload their data.
///
/// Incremented whenever the open repository changes (`:open <path>`).
#[derive(Clone, Copy)]
pub struct ReloadTrigger(pub RwSignal<u32>);

// ── PDF open context ──────────────────────────────────────────────────────────

/// Request to open (or switch to) a PDF tab, posted by child components.
#[derive(Clone, PartialEq, Eq)]
pub struct PdfOpenRequest {
    pub ref_id: String,
    pub title: String,
}

/// Context handle that child components write to when they want to open a PDF tab.
#[derive(Clone, Copy)]
pub struct OpenPdfContext(pub RwSignal<Option<PdfOpenRequest>>);

// ── Keymap provider ────────────────────────────────────────────────────────────

/// Per-tab PDF viewer state (zoom + scroll) shared across the app.
#[derive(Clone, Copy)]
pub struct PdfStatesContext(
    pub RwSignal<HashMap<String, crate::tauri::PdfTabState>>,
);

/// Viewer state update posted by a PDF viewer iframe when zoom/scroll settle.
#[derive(serde::Deserialize)]
struct IframeViewerStateMsg {
    #[serde(rename = "type")]
    kind: String,
    #[serde(rename = "refId")]
    ref_id: String,
    zoom: f64,
    #[serde(rename = "scrollTop")]
    scroll_top: f64,
}

/// Keydown message forwarded from a child iframe (e.g. the PDF viewer).
#[derive(serde::Deserialize)]
struct IframeKeyMsg {
    #[serde(rename = "type")]
    kind: String,
    key: String,
    #[serde(rename = "ctrlKey",  default)] ctrl:  bool,
    #[serde(rename = "shiftKey", default)] shift: bool,
    #[serde(rename = "altKey",   default)] alt:   bool,
    #[serde(rename = "metaKey",  default)] meta:  bool,
}

async fn save_session_async(
    tabs: Vec<AppTab>,
    active_tab: usize,
    pdf_states: HashMap<String, crate::tauri::PdfTabState>,
) {
    let open_tabs = tabs
        .iter()
        .filter_map(|t| match t {
            AppTab::Pdf { ref_id, title } => Some(crate::tauri::PdfTabEntry {
                ref_id: ref_id.clone(),
                title:  title.clone(),
            }),
            AppTab::Library => None,
        })
        .collect();
    let session = crate::tauri::SessionConfig { open_tabs, active_tab, pdf_states };
    let _ = crate::tauri::save_session(&session).await;
}

fn provide_keymap() {
    let state: Rc<RefCell<KeymapState>> =
        Rc::new(RefCell::new(KeymapState::new(default_bindings())));

    let (action_read, action_write) = signal::<Option<ActionEvent>>(None);
    provide_context(KeymapAction(action_read));

    let state_for_listener = state.clone();
    let listener = window_event_listener(leptos::ev::keydown, move |ev: KeyboardEvent| {
        if is_input_focused() {
            return;
        }
        if let Some(key) = key_from_event(&ev) {
            ev.prevent_default();
            let outcome = state_for_listener.borrow_mut().process(key);
            if let Outcome::Action { name, count } = outcome {
                action_write.set(Some(ActionEvent { name, count, seq: next_seq() }));
            }
        }
    });

    // When a child iframe (PDF viewer) has focus, its keydown events don't
    // bubble to the outer window.  The PDF viewer forwards them via postMessage
    // so global keybindings continue to work.
    let state_for_msg = state.clone();
    {
        use wasm_bindgen::{closure::Closure, JsCast as _};
        let cb: Closure<dyn Fn(web_sys::MessageEvent)> =
            Closure::new(move |ev: web_sys::MessageEvent| {
                let Ok(msg) =
                    serde_wasm_bindgen::from_value::<IframeKeyMsg>(ev.data())
                else {
                    return;
                };
                if msg.kind != "brittle:keydown" {
                    return;
                }
                if let Some(key) =
                    key_from_parts(&msg.key, msg.ctrl, msg.shift, msg.alt, msg.meta)
                {
                    let outcome = state_for_msg.borrow_mut().process(key);
                    if let Outcome::Action { name, count } = outcome {
                        action_write.set(Some(ActionEvent {
                            name,
                            count,
                            seq: next_seq(),
                        }));
                    }
                }
            });
        if let Some(win) = web_sys::window() {
            let _ = win
                .add_event_listener_with_callback("message", cb.as_ref().unchecked_ref());
        }
        cb.forget(); // listener lives for the lifetime of the app
    }

    on_cleanup(move || listener.remove());

    // Asynchronously load user keybinding overrides and hot-swap the state.
    // Runs after the listener is already live, so the app is usable with
    // defaults while the async IPC call is in flight.
    let state_for_overrides = state.clone();
    leptos::task::spawn_local(async move {
        let overrides = match crate::tauri::get_keybindings().await {
            Ok(map) if !map.is_empty() => map,
            _ => return, // no config or error — keep defaults
        };

        let mut bindings = default_bindings();
        // Config keys use snake_case; action names use dot.notation.
        let pairs: Vec<(String, String)> = overrides
            .into_iter()
            .map(|(k, v)| (action_key_to_name(&k), v))
            .collect();
        bindings.apply_overrides(pairs.iter().map(|(k, v)| (k.as_str(), v.as_str())));
        *state_for_overrides.borrow_mut() = KeymapState::new(bindings);
    });
}

/// Convert a config keybinding key (snake_case) to an action name (dot.notation).
///
/// ```
/// # use brittle_ui::action_key_to_name; // won't compile as-is, just illustrating
/// assert_eq!(action_key_to_name("tab_next"), "tab.next");
/// assert_eq!(action_key_to_name("nav_page_down"), "nav.page.down");
/// ```
fn action_key_to_name(key: &str) -> String {
    key.replace('_', ".")
}

// ── Helpers ────────────────────────────────────────────────────────────────────

fn is_input_focused() -> bool {
    let Some(doc) = web_sys::window().and_then(|w| w.document()) else {
        return false;
    };
    let Some(el) = doc.active_element() else {
        return false;
    };
    let tag = el.tag_name().to_uppercase();
    if tag == "INPUT" || tag == "TEXTAREA" {
        return true;
    }
    el.get_attribute("contenteditable")
        .map(|v| v != "false")
        .unwrap_or(false)
}

fn key_from_event(ev: &KeyboardEvent) -> Option<Key> {
    key_from_parts(&ev.key(), ev.ctrl_key(), ev.shift_key(), ev.alt_key(), ev.meta_key())
}

fn key_from_parts(key_str: &str, ctrl: bool, shift: bool, alt: bool, meta: bool) -> Option<Key> {
    let code = match key_str {
        "Enter" => KeyCode::Enter,
        "Escape" => KeyCode::Escape,
        "Tab" => KeyCode::Tab,
        "Backspace" => KeyCode::Backspace,
        "Delete" => KeyCode::Delete,
        " " => KeyCode::Space,
        "ArrowUp" => KeyCode::ArrowUp,
        "ArrowDown" => KeyCode::ArrowDown,
        "ArrowLeft" => KeyCode::ArrowLeft,
        "ArrowRight" => KeyCode::ArrowRight,
        "Home" => KeyCode::Home,
        "End" => KeyCode::End,
        "PageUp" => KeyCode::PageUp,
        "PageDown" => KeyCode::PageDown,
        s if s.starts_with('F') && s.len() > 1 => KeyCode::F(s[1..].parse::<u8>().ok()?),
        s if s.chars().count() == 1 => KeyCode::Char(s.chars().next().unwrap()),
        _ => return None,
    };

    // For Char keys the character already encodes the shift state:
    // ':' is the shifted ';', 'G' is the shifted 'g', etc.
    // Including shift: true would produce <S-:> which never matches a binding.
    // For special keys (Tab, arrows, …) shift must be preserved (<S-Tab>).
    let shift = match code {
        KeyCode::Char(_) => false,
        _ => shift,
    };

    Some(Key { code, ctrl, shift, alt, meta })
}

// ── Tab provider ───────────────────────────────────────────────────────────────

/// Set up tab state and related contexts, following the same pattern as
/// `provide_keymap()` and `provide_theme()`.
///
/// Provides: [`OpenPdfContext`], [`ReloadTrigger`].
/// Returns: `(tabs, active_tab)` signals for the view layer.
fn provide_tabs() -> (RwSignal<Vec<AppTab>>, RwSignal<usize>) {
    // The Library tab is always present at index 0 and cannot be closed.
    let tabs       = RwSignal::new(vec![AppTab::Library]);
    let active_tab = RwSignal::new(0usize);

    // Per-tab PDF viewer state (zoom + scroll) keyed by ref_id.
    let pdf_states =
        RwSignal::new(HashMap::<String, crate::tauri::PdfTabState>::new());
    provide_context(PdfStatesContext(pdf_states));

    // Provide the open-PDF context so LibTab can post open requests.
    let open_pdf_req = RwSignal::new(Option::<PdfOpenRequest>::None);
    provide_context(OpenPdfContext(open_pdf_req));

    // Reload trigger: increment when a repository is opened.
    let reload_trigger = RwSignal::new(0u32);
    provide_context(ReloadTrigger(reload_trigger));

    // Auto-open the last repository from the previous session.
    leptos::task::spawn_local(async move {
        if let Ok(Some(path)) = crate::tauri::get_last_project().await {
            if crate::tauri::open_repository(&path).await.is_ok() {
                reload_trigger.update(|n| *n += 1);
            }
        }
    });

    // When a repository opens (or changes), reset tab state and restore the
    // saved session.  `prev.is_none()` on the first reactive run means no repo
    // has been opened yet — skip that case.
    Effect::new(move |prev: Option<()>| {
        let _ = reload_trigger.get(); // subscribe
        let Some(()) = prev else { return; };

        // Reset to just the Library tab before restoring.
        tabs.set(vec![AppTab::Library]);
        active_tab.set(0);
        pdf_states.set(HashMap::new());

        leptos::task::spawn_local(async move {
            let Ok(session) = crate::tauri::get_session().await else { return; };
            // Set pdf_states first so initial zoom/scroll is available when
            // PdfViewer components are created by the For loop below.
            pdf_states.set(session.pdf_states);
            let mut new_tabs = vec![AppTab::Library];
            for entry in &session.open_tabs {
                new_tabs.push(AppTab::Pdf {
                    ref_id: entry.ref_id.clone(),
                    title:  entry.title.clone(),
                });
            }
            let count = new_tabs.len();
            tabs.set(new_tabs);
            active_tab.set(session.active_tab.min(count.saturating_sub(1)));
        });
    });

    // Reactively save session whenever the tab list or active tab changes.
    // Guard on reload_trigger > 0 to skip saves before any repo is open.
    Effect::new(move |_| {
        let _ = tabs.get();       // subscribe
        let _ = active_tab.get(); // subscribe
        if reload_trigger.get_untracked() == 0 { return; }
        let tabs_val = tabs.get_untracked();
        let active   = active_tab.get_untracked();
        let states   = pdf_states.get_untracked();
        leptos::task::spawn_local(async move {
            let _ = save_session_async(tabs_val, active, states).await;
        });
    });

    // Tab keymap effects (cycling and closing).
    let keymap_action = use_context::<KeymapAction>().unwrap().0;
    Effect::new(move |_| {
        let Some(ev) = keymap_action.get() else { return };
        match ev.name.as_str() {
            actions::TAB_NEXT => {
                let len = tabs.with_untracked(Vec::len);
                if len > 1 {
                    active_tab.update(|i| *i = (*i + 1) % len);
                }
            }
            actions::TAB_PREV => {
                let len = tabs.with_untracked(Vec::len);
                if len > 1 {
                    active_tab.update(|i| *i = if *i == 0 { len - 1 } else { *i - 1 });
                }
            }
            // Close the active tab (Library tab is immortal).
            actions::TAB_CLOSE => {
                let idx = active_tab.get_untracked();
                if idx > 0 {
                    tabs.update(|t| { t.remove(idx); });
                    active_tab.update(|i| *i = i.saturating_sub(1));
                }
            }
            _ => {}
        }
    });

    // Open-PDF request handler: watches for requests from LibTab and opens or
    // activates the appropriate PDF tab.
    Effect::new(move |_| {
        let Some(req) = open_pdf_req.get() else { return };

        let existing = tabs.with_untracked(|t| {
            t.iter().position(|tab| {
                matches!(tab, AppTab::Pdf { ref_id: r, .. } if *r == req.ref_id)
            })
        });

        if let Some(idx) = existing {
            active_tab.set(idx);
        } else {
            let new_idx = tabs.with_untracked(Vec::len);
            tabs.update(|t| t.push(AppTab::Pdf {
                ref_id: req.ref_id.clone(),
                title:  req.title.clone(),
            }));
            active_tab.set(new_idx);
        }

        // Clear so the same ref_id can re-trigger (e.g. switch away, then re-open).
        open_pdf_req.set(None);
    });

    (tabs, active_tab)
}

// ── Layout context ────────────────────────────────────────────────────────────

/// Context handle for the current layout configuration.
#[derive(Clone, Copy)]
pub struct LayoutContext(pub ReadSignal<crate::tauri::LayoutConfig>);

fn provide_layout() {
    let (layout, set_layout) = signal(crate::tauri::LayoutConfig {
        left_pane_fraction: 0.20,
        right_pane_fraction: 0.35,
        left_pane_min: 120,
        left_pane_max: 600,
        right_pane_min: 150,
        right_pane_max: 600,
        center_pane_min: 200,
    });
    provide_context(LayoutContext(layout));

    // Load layout config from backend.
    leptos::task::spawn_local(async move {
        if let Ok(l) = crate::tauri::get_layout_config().await {
            set_layout.set(l);
        }
    });
}

// ── Root component ─────────────────────────────────────────────────────────────

#[component]
fn App() -> impl IntoView {
    provide_keymap();
    provide_theme();
    provide_layout();
    let mode = provide_mode();
    provide_search_query();
    let (tabs, active_tab) = provide_tabs();

    // Access contexts provided by provide_tabs().
    let pdf_states     = use_context::<PdfStatesContext>().unwrap().0;
    let reload_trigger = use_context::<ReloadTrigger>().unwrap().0;

    // Listen for viewer state updates (zoom + scroll) posted by PDF viewer
    // iframes and persist them immediately to the project session file.
    {
        use wasm_bindgen::{closure::Closure, JsCast as _};
        let cb: Closure<dyn Fn(web_sys::MessageEvent)> =
            Closure::new(move |ev: web_sys::MessageEvent| {
                let Ok(msg) =
                    serde_wasm_bindgen::from_value::<IframeViewerStateMsg>(ev.data())
                else {
                    return;
                };
                if msg.kind != "brittle:viewer-state" {
                    return;
                }
                pdf_states.update(|m| {
                    m.insert(
                        msg.ref_id.clone(),
                        crate::tauri::PdfTabState {
                            zoom:       msg.zoom,
                            scroll_top: msg.scroll_top,
                        },
                    );
                });
                if reload_trigger.get_untracked() > 0 {
                    let tabs_val = tabs.get_untracked();
                    let active   = active_tab.get_untracked();
                    let states   = pdf_states.get_untracked();
                    leptos::task::spawn_local(async move {
                        let _ = save_session_async(tabs_val, active, states).await;
                    });
                }
            });
        if let Some(win) = web_sys::window() {
            let _ =
                win.add_event_listener_with_callback("message", cb.as_ref().unchecked_ref());
        }
        cb.forget();
    }

    // Mode-switching keymap effect (tab actions are handled inside provide_tabs).
    let keymap_action = use_context::<KeymapAction>().unwrap().0;
    Effect::new(move |_| {
        let Some(ev) = keymap_action.get() else { return };
        match ev.name.as_str() {
            actions::MODE_COMMAND => mode.set(AppMode::Command),
            actions::MODE_SEARCH => mode.set(AppMode::Search),
            actions::MODE_NORMAL => mode.set(AppMode::Normal),
            _ => {}
        }
    });

    // ── View ──────────────────────────────────────────────────────────────────
    view! {
        <div class="app">
            <TabBar tabs=tabs active_tab=active_tab />
            <div class="app-body">
                // Library tab — always mounted; hidden when a PDF tab is active.
                <div style=move || if active_tab.get() == 0 { "height:100%" } else { "display:none" }>
                    <LibTab />
                </div>
                // PDF tabs — each mounted once (keyed by ref_id) and hidden
                // when not active, so iframe state is preserved across switches.
                <For
                    each=move || {
                        tabs.get()
                            .into_iter()
                            .enumerate()
                            .filter_map(|(i, tab)| match tab {
                                AppTab::Pdf { ref_id, .. } => Some((i, ref_id)),
                                AppTab::Library => None,
                            })
                            .collect::<Vec<_>>()
                    }
                    key=|(_, ref_id)| ref_id.clone()
                    children=move |(i, ref_id)| {
                        // Capture saved state at creation time; the iframe URL
                        // is static so these values only matter on first mount.
                        let initial_zoom =
                            pdf_states.with_untracked(|m| m.get(&ref_id).map(|s| s.zoom));
                        let initial_scroll_top =
                            pdf_states.with_untracked(|m| m.get(&ref_id).map(|s| s.scroll_top));

                        // Derive visibility reactively: look up the live tabs
                        // so the style updates correctly after tab close/reorder.
                        let ref_id_vis = ref_id.clone();
                        let is_visible = Memo::new(move |_| {
                            let active = active_tab.get();
                            tabs.with(|t| {
                                t.get(active)
                                    .map(|tab| matches!(
                                        tab,
                                        AppTab::Pdf { ref_id: r, .. } if *r == ref_id_vis
                                    ))
                                    .unwrap_or(false)
                            })
                        });
                        // `i` at creation time is used as a fallback for the
                        // close button in TabBar; here we only need visibility.
                        let _ = i;
                        view! {
                            <div style=move || if is_visible.get() { "height:100%" } else { "display:none" }>
                                <PdfViewer
                                    ref_id=ref_id
                                    is_active=Signal::derive(move || is_visible.get())
                                    initial_zoom=initial_zoom
                                    initial_scroll_top=initial_scroll_top
                                />
                            </div>
                        }
                    }
                />
            </div>
            <CommandBar mode />
        </div>
    }
}

// ── Tests ──────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::{action_key_to_name, key_from_parts};
    use brittle_keymap::{Key, KeyCode};

    #[test]
    fn single_segment_unchanged() {
        assert_eq!(action_key_to_name("quit"), "quit");
    }

    #[test]
    fn two_segment_conversion() {
        assert_eq!(action_key_to_name("tab_next"), "tab.next");
        assert_eq!(action_key_to_name("tab_prev"), "tab.prev");
        assert_eq!(action_key_to_name("tab_close"), "tab.close");
    }

    #[test]
    fn three_segment_conversion() {
        assert_eq!(action_key_to_name("nav_page_down"), "nav.page.down");
        assert_eq!(action_key_to_name("nav_page_up"), "nav.page.up");
    }

    #[test]
    fn focus_actions() {
        assert_eq!(action_key_to_name("focus_left"), "focus.left");
        assert_eq!(action_key_to_name("focus_center"), "focus.center");
        assert_eq!(action_key_to_name("focus_right"), "focus.right");
    }

    #[test]
    fn mode_actions() {
        assert_eq!(action_key_to_name("mode_command"), "mode.command");
        assert_eq!(action_key_to_name("mode_normal"), "mode.normal");
    }

    // ── key_from_parts ───────────────────────────────────────────────────────

    #[test]
    fn char_key_basic() {
        let k = key_from_parts("g", false, false, false, false).unwrap();
        assert_eq!(k, Key { code: KeyCode::Char('g'), ctrl: false, shift: false, alt: false, meta: false });
    }

    #[test]
    fn char_key_shift_suppressed() {
        // 'G' already encodes the shift; shift flag must be suppressed to avoid <S-G>
        let k = key_from_parts("G", false, true, false, false).unwrap();
        assert_eq!(k.code, KeyCode::Char('G'));
        assert!(!k.shift, "shift must be false for Char keys");
    }

    #[test]
    fn char_key_colon_shift_suppressed() {
        // ':' is the shifted ';' on many keyboards
        let k = key_from_parts(":", false, true, false, false).unwrap();
        assert_eq!(k.code, KeyCode::Char(':'));
        assert!(!k.shift);
    }

    #[test]
    fn char_key_ctrl_preserved() {
        let k = key_from_parts("a", true, false, false, false).unwrap();
        assert_eq!(k.code, KeyCode::Char('a'));
        assert!(k.ctrl);
        assert!(!k.shift);
    }

    #[test]
    fn special_key_shift_preserved() {
        let k = key_from_parts("Tab", false, true, false, false).unwrap();
        assert_eq!(k.code, KeyCode::Tab);
        assert!(k.shift, "shift must be preserved for special keys like Tab");
    }

    #[test]
    fn arrow_keys_shift_preserved() {
        let k = key_from_parts("ArrowUp", false, true, false, false).unwrap();
        assert_eq!(k.code, KeyCode::ArrowUp);
        assert!(k.shift);

        let k = key_from_parts("ArrowDown", false, true, false, false).unwrap();
        assert_eq!(k.code, KeyCode::ArrowDown);
        assert!(k.shift);
    }

    #[test]
    fn enter_escape_backspace() {
        assert_eq!(key_from_parts("Enter", false, false, false, false).unwrap().code, KeyCode::Enter);
        assert_eq!(key_from_parts("Escape", false, false, false, false).unwrap().code, KeyCode::Escape);
        assert_eq!(key_from_parts("Backspace", false, false, false, false).unwrap().code, KeyCode::Backspace);
    }

    #[test]
    fn function_keys() {
        assert_eq!(key_from_parts("F1", false, false, false, false).unwrap().code, KeyCode::F(1));
        assert_eq!(key_from_parts("F12", false, false, false, false).unwrap().code, KeyCode::F(12));
    }

    #[test]
    fn unknown_key_returns_none() {
        assert!(key_from_parts("Dead", false, false, false, false).is_none());
        assert!(key_from_parts("Unidentified", false, false, false, false).is_none());
        assert!(key_from_parts("AudioVolumeUp", false, false, false, false).is_none());
    }

    #[test]
    fn space_key() {
        let k = key_from_parts(" ", false, false, false, false).unwrap();
        assert_eq!(k.code, KeyCode::Space);
    }
}
