mod capture;
mod cdp;
mod clipboard;
mod cookies;
mod coverage;
mod download;
mod emulate;
mod frame;
mod interaction;
mod misc;
mod mouse;
mod navigation;
mod query;
mod route;
mod session_cmd;
mod state;
mod storage;
mod tab;
mod touch;
mod ws;

use night_fury_core::BrowserSession;

use night_fury_cli_core::output::{output_err, OutputMode};

/// Resolved top-level CLI command. One variant per handler call site in
/// [`dispatch`]. Keep this enum and [`resolve_command`] in sync — the dispatch
/// match is exhaustive over this enum.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub(crate) enum CommandKind {
    Go,
    Back,
    Forward,
    Reload,
    Url,
    Title,
    Snap,
    Screenshot,
    Eval,
    Pdf,
    Click,
    Type,
    Scroll,
    Hover,
    Select,
    Check,
    Uncheck,
    Wait,
    Cookies,
    Tab,
    Get,
    Is,
    Session,
    Frame,
    Storage,
    Clipboard,
    Route,
    Emulate,
    Mouse,
    Touch,
    Download,
    Ws,
    Coverage,
    Cdp,
    Viewport,
    Headers,
    Auth,
    Help,
    Quit,
    Unknown,
}

/// Map a raw first-word command token to a [`CommandKind`]. Case-insensitive.
pub(crate) fn resolve_command(cmd: &str) -> CommandKind {
    match cmd.to_lowercase().as_str() {
        "go" | "nav" | "navigate" => CommandKind::Go,
        "back" => CommandKind::Back,
        "forward" => CommandKind::Forward,
        "reload" => CommandKind::Reload,
        "url" => CommandKind::Url,
        "title" => CommandKind::Title,
        "snap" | "s" | "snapshot" => CommandKind::Snap,
        "screenshot" | "ss" => CommandKind::Screenshot,
        "eval" | "js" => CommandKind::Eval,
        "pdf" => CommandKind::Pdf,
        "click" | "c" => CommandKind::Click,
        "type" | "t" => CommandKind::Type,
        "scroll" => CommandKind::Scroll,
        "hover" => CommandKind::Hover,
        "select" => CommandKind::Select,
        "check" => CommandKind::Check,
        "uncheck" => CommandKind::Uncheck,
        "wait" | "sleep" => CommandKind::Wait,
        "cookies" | "cookie" => CommandKind::Cookies,
        "tab" => CommandKind::Tab,
        "get" => CommandKind::Get,
        "is" => CommandKind::Is,
        "session" => CommandKind::Session,
        "frame" => CommandKind::Frame,
        "storage" => CommandKind::Storage,
        "clipboard" => CommandKind::Clipboard,
        "route" => CommandKind::Route,
        "emulate" => CommandKind::Emulate,
        "mouse" => CommandKind::Mouse,
        "touch" => CommandKind::Touch,
        "download" => CommandKind::Download,
        "ws" => CommandKind::Ws,
        "coverage" => CommandKind::Coverage,
        "cdp" => CommandKind::Cdp,
        "viewport" => CommandKind::Viewport,
        "headers" => CommandKind::Headers,
        "auth" => CommandKind::Auth,
        "help" | "?" => CommandKind::Help,
        "quit" | "exit" | "q" => CommandKind::Quit,
        _ => CommandKind::Unknown,
    }
}

pub async fn dispatch(session: &BrowserSession, line: &str, mode: OutputMode) -> bool {
    let parts: Vec<&str> = line.splitn(4, ' ').collect();
    match resolve_command(parts[0]) {
        CommandKind::Go => navigation::go(session, &parts[1..], mode).await,
        CommandKind::Back => navigation::back(session, mode).await,
        CommandKind::Forward => navigation::forward(session, mode).await,
        CommandKind::Reload => navigation::reload(session, mode).await,
        CommandKind::Url => navigation::url(session, mode).await,
        CommandKind::Title => navigation::title(session, mode).await,

        CommandKind::Snap => capture::snap(session, mode).await,
        CommandKind::Screenshot => capture::screenshot(session, &parts[1..], mode).await,
        CommandKind::Eval => capture::eval(session, line, mode).await,
        CommandKind::Pdf => capture::pdf(session, &parts[1..], mode).await,

        CommandKind::Click => interaction::click(session, &parts[1..], mode).await,
        CommandKind::Type => interaction::type_text(session, line, &parts[1..], mode).await,
        CommandKind::Scroll => interaction::scroll(session, &parts[1..], mode).await,
        CommandKind::Hover => interaction::hover(session, &parts[1..], mode).await,
        CommandKind::Select => interaction::select(session, &parts[1..], mode).await,
        CommandKind::Check => interaction::check(session, &parts[1..], mode).await,
        CommandKind::Uncheck => interaction::uncheck(session, &parts[1..], mode).await,
        CommandKind::Wait => interaction::wait(session, line, &parts[1..], mode).await,

        CommandKind::Cookies => cookies::dispatch(session, &parts[1..], mode).await,
        CommandKind::Tab => tab::dispatch(session, &parts[1..], mode).await,
        CommandKind::Get => query::dispatch(session, &parts[1..], mode).await,
        CommandKind::Is => state::dispatch(session, &parts[1..], mode).await,
        CommandKind::Session => session_cmd::dispatch(session, &parts[1..], mode).await,
        CommandKind::Frame => frame::dispatch(session, &parts[1..], mode).await,
        CommandKind::Storage => storage::dispatch(session, &parts[1..], mode).await,
        CommandKind::Clipboard => clipboard::dispatch(session, &parts[1..], mode).await,
        CommandKind::Route => route::dispatch(session, &parts[1..], mode).await,
        CommandKind::Emulate => emulate::dispatch(session, &parts[1..], mode).await,
        CommandKind::Mouse => mouse::dispatch(session, &parts[1..], mode).await,
        CommandKind::Touch => touch::dispatch(session, &parts[1..], mode).await,
        CommandKind::Download => download::dispatch(session, &parts[1..], mode).await,
        CommandKind::Ws => ws::dispatch(session, &parts[1..], mode).await,
        CommandKind::Coverage => coverage::dispatch(session, &parts[1..], mode).await,
        CommandKind::Cdp => cdp::run(session, &parts[1..], mode).await,
        CommandKind::Viewport => misc::viewport(session, &parts[1..], mode).await,
        CommandKind::Headers => misc::headers(session, &parts[1..], mode).await,
        CommandKind::Auth => misc::auth(session, &parts[1..], mode).await,

        CommandKind::Help => print_help(mode),
        CommandKind::Quit => return true,

        CommandKind::Unknown => output_err(
            mode,
            &format!(
                "Unknown command: {}. Type 'help' for commands.",
                parts[0].to_lowercase()
            ),
        ),
    }
    false
}

fn print_help(mode: OutputMode) {
    match mode {
        OutputMode::Json => {
            let help = serde_json::json!({
                "ok": true,
                "data": {
                    "groups": [
                        {
                            "name": "Navigation",
                            "commands": [
                                {"cmd": "go <url>", "desc": "Navigate to URL", "aliases": ["nav"]},
                                {"cmd": "back", "desc": "History back"},
                                {"cmd": "forward", "desc": "History forward"},
                                {"cmd": "reload", "desc": "Reload page"},
                                {"cmd": "url", "desc": "Print current URL"},
                                {"cmd": "title", "desc": "Print page title"}
                            ]
                        },
                        {
                            "name": "Interaction",
                            "commands": [
                                {"cmd": "click <sel>", "desc": "Click element", "aliases": ["c"]},
                                {"cmd": "type <sel> <text>", "desc": "Type text", "aliases": ["t"]},
                                {"cmd": "scroll [px]", "desc": "Scroll (default: 600)"},
                                {"cmd": "hover <sel>", "desc": "Hover element"},
                                {"cmd": "select <sel> <value>", "desc": "Select dropdown option"},
                                {"cmd": "check <sel>", "desc": "Check checkbox"},
                                {"cmd": "uncheck <sel>", "desc": "Uncheck checkbox"},
                                {"cmd": "wait [ms]", "desc": "Wait (default: 1000)", "aliases": ["sleep"]}
                            ]
                        },
                        {
                            "name": "Capture",
                            "commands": [
                                {"cmd": "snap", "desc": "Page snapshot", "aliases": ["s", "snapshot"]},
                                {"cmd": "screenshot [file]", "desc": "Save PNG", "aliases": ["ss"]},
                                {"cmd": "pdf [file]", "desc": "Export PDF"},
                                {"cmd": "eval <js>", "desc": "Evaluate JavaScript", "aliases": ["js"]}
                            ]
                        },
                        {
                            "name": "Tabs",
                            "commands": [
                                {"cmd": "tab new [url]", "desc": "Open new tab"},
                                {"cmd": "tab switch <idx>", "desc": "Switch to tab"},
                                {"cmd": "tab list", "desc": "List tabs"},
                                {"cmd": "tab close [idx]", "desc": "Close tab"}
                            ]
                        },
                        {
                            "name": "Queries",
                            "commands": [
                                {"cmd": "get text <sel>", "desc": "Element text content"},
                                {"cmd": "get attr <sel> <name>", "desc": "Element attribute"},
                                {"cmd": "get html <sel>", "desc": "Element innerHTML"},
                                {"cmd": "get value <sel>", "desc": "Input value"},
                                {"cmd": "get count <sel>", "desc": "Count elements"},
                                {"cmd": "get box <sel>", "desc": "Bounding box"}
                            ]
                        },
                        {
                            "name": "State",
                            "commands": [
                                {"cmd": "is visible <sel>", "desc": "Check visibility"},
                                {"cmd": "is checked <sel>", "desc": "Check state"},
                                {"cmd": "is enabled <sel>", "desc": "Check enabled"}
                            ]
                        },
                        {
                            "name": "Cookies",
                            "commands": [
                                {"cmd": "cookies get", "desc": "List cookies"},
                                {"cmd": "cookies set <file>", "desc": "Inject from file"},
                                {"cmd": "cookies delete <n> <d>", "desc": "Delete cookie"},
                                {"cmd": "cookies clear", "desc": "Clear all"}
                            ]
                        },
                        {
                            "name": "Session",
                            "commands": [
                                {"cmd": "session save [file]", "desc": "Save session"},
                                {"cmd": "session load <file>", "desc": "Load session"}
                            ]
                        },
                        {
                            "name": "Routes",
                            "commands": [
                                {"cmd": "route block <pat>", "desc": "Block requests"},
                                {"cmd": "route fulfill <p> <s> <b>", "desc": "Mock response"},
                                {"cmd": "route capture <pat>", "desc": "Capture responses"},
                                {"cmd": "route list", "desc": "Show captured"},
                                {"cmd": "route clear", "desc": "Remove routes"}
                            ]
                        },
                        {
                            "name": "Frames",
                            "commands": [
                                {"cmd": "frame enter <sel>", "desc": "Switch to iframe"},
                                {"cmd": "frame main", "desc": "Back to main"}
                            ]
                        },
                        {
                            "name": "Storage",
                            "commands": [
                                {"cmd": "storage get <local|session> [key]", "desc": "Read value or dump store"},
                                {"cmd": "storage set <local|session> <key> <value>", "desc": "Write value"},
                                {"cmd": "storage clear <local|session>", "desc": "Clear all"}
                            ]
                        },
                        {
                            "name": "Clipboard",
                            "commands": [
                                {"cmd": "clipboard read", "desc": "Read clipboard"},
                                {"cmd": "clipboard write <txt>", "desc": "Write clipboard"}
                            ]
                        },
                        {
                            "name": "Mouse",
                            "commands": [
                                {"cmd": "mouse move <x> <y>", "desc": "Move cursor"},
                                {"cmd": "mouse wheel <delta>", "desc": "Scroll wheel"},
                                {"cmd": "mouse down [btn]", "desc": "Press button"},
                                {"cmd": "mouse up [btn]", "desc": "Release button"}
                            ]
                        },
                        {
                            "name": "Touch",
                            "commands": [
                                {"cmd": "touch tap <x> <y>", "desc": "Tap"},
                                {"cmd": "touch swipe <x1> <y1> <x2> <y2>", "desc": "Swipe"}
                            ]
                        },
                        {
                            "name": "Emulation",
                            "commands": [
                                {"cmd": "emulate tz <tz>", "desc": "Set timezone"},
                                {"cmd": "emulate locale <loc>", "desc": "Set locale"},
                                {"cmd": "emulate media <f> <v>", "desc": "Set media feature"},
                                {"cmd": "emulate geo <lat> <lng>", "desc": "Set geolocation"}
                            ]
                        },
                        {
                            "name": "Download",
                            "commands": [
                                {"cmd": "download path <dir>", "desc": "Set download dir"},
                                {"cmd": "download wait [sec]", "desc": "Wait for download"}
                            ]
                        },
                        {
                            "name": "WebSocket",
                            "commands": [
                                {"cmd": "ws start", "desc": "Start WS capture"},
                                {"cmd": "ws messages", "desc": "Get messages"}
                            ]
                        },
                        {
                            "name": "Coverage",
                            "commands": [
                                {"cmd": "coverage start", "desc": "Start coverage"},
                                {"cmd": "coverage stop", "desc": "Stop & report"}
                            ]
                        },
                        {
                            "name": "Other",
                            "commands": [
                                {"cmd": "cdp <method> [json]", "desc": "Raw CDP command"},
                                {"cmd": "viewport <w> <h>", "desc": "Resize viewport"},
                                {"cmd": "headers <json>", "desc": "Set extra headers"},
                                {"cmd": "auth <user> <pass>", "desc": "Set HTTP auth"},
                                {"cmd": "help", "desc": "Show this help"},
                                {"cmd": "quit", "desc": "Exit", "aliases": ["q"]}
                            ]
                        }
                    ],
                    "selector_hint": "Selectors: @e3 (ref), css #id, text Sign in, or bare CSS"
                }
            });
            println!(
                "{}",
                serde_json::to_string_pretty(&help).unwrap_or_default()
            );
        }
        OutputMode::Human => {
            println!(
                r#"Night Fury CLI — commands:

  Navigation:
    go <url>              Navigate to URL  (alias: nav)
    back                  History back
    forward               History forward
    reload                Reload page
    url                   Print current URL
    title                 Print page title

  Interaction:
    click <sel>           Click element  (alias: c)
    type <sel> <text>     Type text  (alias: t)
    scroll [px]           Scroll (default: 600)
    hover <sel>           Hover element
    select <sel> <value>  Select dropdown option
    check <sel>           Check checkbox
    uncheck <sel>         Uncheck checkbox
    wait [ms]             Wait (default: 1000)

  Capture:
    snap                  Page snapshot  (alias: s)
    screenshot [file]     Save PNG  (alias: ss)
    pdf [file]            Export PDF
    eval <js>             Evaluate JavaScript  (alias: js)

  Tabs:
    tab new [url]         Open new tab
    tab switch <idx>      Switch to tab
    tab list              List tabs
    tab close [idx]       Close tab

  Queries:
    get text <sel>        Element text content
    get attr <sel> <name> Element attribute
    get html <sel>        Element innerHTML
    get value <sel>       Input value
    get count <sel>       Count elements
    get box <sel>         Bounding box

  State:
    is visible <sel>      Check visibility
    is checked <sel>      Check state
    is enabled <sel>      Check enabled

  Cookies:
    cookies get           List cookies
    cookies set <file>    Inject from file
    cookies delete <n> <d> Delete cookie
    cookies clear         Clear all

  Session:
    session save [file]   Save session
    session load <file>   Load session

  Routes:
    route block <pat>     Block requests
    route fulfill <p> <s> <b>  Mock response
    route capture <pat>   Capture responses
    route list            Show captured
    route clear           Remove routes

  Frames:
    frame enter <sel>     Switch to iframe
    frame main            Back to main

  Storage:
    storage get <local|session> [key]       Read value or dump store
    storage set <local|session> <key> <value> Write value
    storage clear <local|session>           Clear all

  Clipboard:
    clipboard read        Read clipboard
    clipboard write <txt> Write clipboard

  Mouse:
    mouse move <x> <y>    Move cursor
    mouse wheel <delta>   Scroll wheel
    mouse down [btn]      Press button
    mouse up [btn]        Release button

  Touch:
    touch tap <x> <y>     Tap
    touch swipe <x1> <y1> <x2> <y2>  Swipe

  Emulation:
    emulate tz <tz>       Set timezone
    emulate locale <loc>  Set locale
    emulate media <f> <v> Set media feature
    emulate geo <lat> <lng> Set geolocation

  Download:
    download path <dir>   Set download dir
    download wait [sec]   Wait for download

  WebSocket:
    ws start              Start WS capture
    ws messages           Get messages

  Coverage:
    coverage start        Start coverage
    coverage stop         Stop & report

  Other:
    cdp <method> [json]   Raw CDP command
    viewport <w> <h>      Resize viewport
    headers <json>        Set extra headers
    auth <user> <pass>    Set HTTP auth
    help                  Show this help
    quit                  Exit  (alias: q)

Selectors: @e3 (ref), css #id, text Sign in, or bare CSS"#
            );
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{resolve_command, CommandKind};

    // ---- Happy-path resolution: one check per handler ----

    #[test]
    fn resolves_navigation_aliases() {
        assert_eq!(resolve_command("go"), CommandKind::Go);
        assert_eq!(resolve_command("nav"), CommandKind::Go);
        assert_eq!(resolve_command("navigate"), CommandKind::Go);
        assert_eq!(resolve_command("back"), CommandKind::Back);
        assert_eq!(resolve_command("forward"), CommandKind::Forward);
        assert_eq!(resolve_command("reload"), CommandKind::Reload);
        assert_eq!(resolve_command("url"), CommandKind::Url);
        assert_eq!(resolve_command("title"), CommandKind::Title);
    }

    #[test]
    fn resolves_capture_aliases() {
        assert_eq!(resolve_command("snap"), CommandKind::Snap);
        assert_eq!(resolve_command("s"), CommandKind::Snap);
        assert_eq!(resolve_command("snapshot"), CommandKind::Snap);
        assert_eq!(resolve_command("screenshot"), CommandKind::Screenshot);
        assert_eq!(resolve_command("ss"), CommandKind::Screenshot);
        assert_eq!(resolve_command("eval"), CommandKind::Eval);
        assert_eq!(resolve_command("js"), CommandKind::Eval);
        assert_eq!(resolve_command("pdf"), CommandKind::Pdf);
    }

    #[test]
    fn resolves_interaction_aliases() {
        assert_eq!(resolve_command("click"), CommandKind::Click);
        assert_eq!(resolve_command("c"), CommandKind::Click);
        assert_eq!(resolve_command("type"), CommandKind::Type);
        assert_eq!(resolve_command("t"), CommandKind::Type);
        assert_eq!(resolve_command("scroll"), CommandKind::Scroll);
        assert_eq!(resolve_command("hover"), CommandKind::Hover);
        assert_eq!(resolve_command("select"), CommandKind::Select);
        assert_eq!(resolve_command("check"), CommandKind::Check);
        assert_eq!(resolve_command("uncheck"), CommandKind::Uncheck);
    }

    #[test]
    fn resolves_wait_and_sleep() {
        assert_eq!(resolve_command("wait"), CommandKind::Wait);
        assert_eq!(resolve_command("sleep"), CommandKind::Wait);
    }

    #[test]
    fn resolves_cookies_singular_and_plural() {
        assert_eq!(resolve_command("cookies"), CommandKind::Cookies);
        assert_eq!(resolve_command("cookie"), CommandKind::Cookies);
    }

    #[test]
    fn resolves_sub_dispatcher_groups() {
        assert_eq!(resolve_command("tab"), CommandKind::Tab);
        assert_eq!(resolve_command("get"), CommandKind::Get);
        assert_eq!(resolve_command("is"), CommandKind::Is);
        assert_eq!(resolve_command("session"), CommandKind::Session);
        assert_eq!(resolve_command("frame"), CommandKind::Frame);
        assert_eq!(resolve_command("storage"), CommandKind::Storage);
        assert_eq!(resolve_command("clipboard"), CommandKind::Clipboard);
        assert_eq!(resolve_command("route"), CommandKind::Route);
        assert_eq!(resolve_command("emulate"), CommandKind::Emulate);
        assert_eq!(resolve_command("mouse"), CommandKind::Mouse);
        assert_eq!(resolve_command("touch"), CommandKind::Touch);
        assert_eq!(resolve_command("download"), CommandKind::Download);
        assert_eq!(resolve_command("ws"), CommandKind::Ws);
        assert_eq!(resolve_command("coverage"), CommandKind::Coverage);
    }

    #[test]
    fn resolves_raw_and_misc() {
        assert_eq!(resolve_command("cdp"), CommandKind::Cdp);
        assert_eq!(resolve_command("viewport"), CommandKind::Viewport);
        assert_eq!(resolve_command("headers"), CommandKind::Headers);
        assert_eq!(resolve_command("auth"), CommandKind::Auth);
    }

    #[test]
    fn resolves_help_aliases() {
        assert_eq!(resolve_command("help"), CommandKind::Help);
        assert_eq!(resolve_command("?"), CommandKind::Help);
    }

    #[test]
    fn resolves_quit_aliases() {
        assert_eq!(resolve_command("quit"), CommandKind::Quit);
        assert_eq!(resolve_command("exit"), CommandKind::Quit);
        assert_eq!(resolve_command("q"), CommandKind::Quit);
    }

    // ---- Case-insensitivity ----

    #[test]
    fn resolve_is_case_insensitive() {
        assert_eq!(resolve_command("GO"), CommandKind::Go);
        assert_eq!(resolve_command("Snapshot"), CommandKind::Snap);
        assert_eq!(resolve_command("QUIT"), CommandKind::Quit);
        assert_eq!(resolve_command("Click"), CommandKind::Click);
    }

    // ---- Unknown ----

    #[test]
    fn unknown_command_returns_unknown() {
        assert_eq!(resolve_command("launch-the-missiles"), CommandKind::Unknown);
        assert_eq!(resolve_command(""), CommandKind::Unknown);
        assert_eq!(resolve_command("navigat"), CommandKind::Unknown); // typo — no partial match
        assert_eq!(resolve_command("clickk"), CommandKind::Unknown);
    }

    // ---- Edge cases ----

    #[test]
    fn single_char_aliases_dont_collide() {
        // s / ss / c / t / q are all single/double char — make sure they route distinctly
        assert_eq!(resolve_command("s"), CommandKind::Snap);
        assert_eq!(resolve_command("ss"), CommandKind::Screenshot);
        assert_eq!(resolve_command("c"), CommandKind::Click);
        assert_eq!(resolve_command("t"), CommandKind::Type);
        assert_eq!(resolve_command("q"), CommandKind::Quit);
    }

    #[test]
    fn whitespace_token_is_unknown() {
        // caller should trim — resolver does not
        assert_eq!(resolve_command(" go"), CommandKind::Unknown);
        assert_eq!(resolve_command("go "), CommandKind::Unknown);
    }

    #[test]
    fn every_variant_has_a_keyword() {
        // Smoke-check that adding a CommandKind variant forces at least one
        // keyword to map to it. If someone adds a variant without wiring
        // resolve_command, this test won't catch it directly, but the
        // exhaustive match in `dispatch` will fail to compile — this test
        // is a belt-and-suspenders that the current mapping is total.
        let kinds = [
            CommandKind::Go,
            CommandKind::Back,
            CommandKind::Forward,
            CommandKind::Reload,
            CommandKind::Url,
            CommandKind::Title,
            CommandKind::Snap,
            CommandKind::Screenshot,
            CommandKind::Eval,
            CommandKind::Pdf,
            CommandKind::Click,
            CommandKind::Type,
            CommandKind::Scroll,
            CommandKind::Hover,
            CommandKind::Select,
            CommandKind::Check,
            CommandKind::Uncheck,
            CommandKind::Wait,
            CommandKind::Cookies,
            CommandKind::Tab,
            CommandKind::Get,
            CommandKind::Is,
            CommandKind::Session,
            CommandKind::Frame,
            CommandKind::Storage,
            CommandKind::Clipboard,
            CommandKind::Route,
            CommandKind::Emulate,
            CommandKind::Mouse,
            CommandKind::Touch,
            CommandKind::Download,
            CommandKind::Ws,
            CommandKind::Coverage,
            CommandKind::Cdp,
            CommandKind::Viewport,
            CommandKind::Headers,
            CommandKind::Auth,
            CommandKind::Help,
            CommandKind::Quit,
            CommandKind::Unknown,
        ];
        // 40 variants — one per dispatch arm, plus Help / Quit / Unknown.
        assert_eq!(kinds.len(), 40);
    }

    // ---- Property tests ----

    use proptest::prelude::*;

    proptest! {
        /// `resolve_command` must never panic on arbitrary input, including
        /// empty strings, huge strings, random unicode, whitespace-only,
        /// control characters. Worst case it returns `CommandKind::Unknown`.
        #[test]
        fn resolve_command_never_panics(input in ".{0,500}") {
            let _ = resolve_command(&input);
        }

        /// Case-insensitivity property: for any known keyword, any
        /// mixed-case version of it resolves to the same variant.
        #[test]
        fn known_keywords_are_case_insensitive(
            word_idx in 0usize..8,
        ) {
            const KNOWN: &[&str] = &[
                "navigate", "click", "snap", "quit", "cookies", "help", "scroll", "wait",
            ];
            let word = KNOWN[word_idx];
            let expected = resolve_command(word);
            let upper = word.to_uppercase();
            let mixed = {
                let mut s = String::new();
                for (i, c) in word.chars().enumerate() {
                    if i % 2 == 0 { s.push(c.to_ascii_uppercase()); } else { s.push(c); }
                }
                s
            };
            prop_assert_eq!(resolve_command(&upper), expected,
                "uppercase form must match lowercase: {}", word);
            prop_assert_eq!(resolve_command(&mixed), expected,
                "mixed case form must match: {} -> {}", word, mixed);
        }
    }
}
