use crate::parse::{FlagCheck, Token, WordSet};

static YARN_READ_ONLY: WordSet =
    WordSet::new(&["--version", "info", "list", "ls", "why"]);

static NPM_READ_ONLY: WordSet = WordSet::new(&[
    "--version", "audit", "doctor", "explain", "fund", "info", "list", "ls",
    "outdated", "prefix", "root", "test", "view", "why",
]);

static NPX_SAFE: WordSet =
    WordSet::new(&["@herb-tools/linter", "eslint", "karma"]);

static NPX_FLAGS_NO_ARG: WordSet =
    WordSet::new(&["--ignore-existing", "--no", "--quiet", "--yes", "-q", "-y"]);

static PNPM_READ_ONLY: WordSet =
    WordSet::new(&["--version", "audit", "list", "ls", "outdated", "why"]);

static BUN_SAFE: WordSet =
    WordSet::new(&["--version", "outdated", "test"]);

static BUN_MULTI: &[(&str, WordSet)] =
    &[("pm", WordSet::new(&["bin", "cache", "hash", "ls"]))];

static BUNX_FLAGS_NO_ARG: WordSet =
    WordSet::new(&["--bun", "--no-install", "--silent", "--verbose"]);

static DENO_SAFE: WordSet =
    WordSet::new(&["--version", "check", "doc", "info", "lint", "test"]);

static DENO_FMT: FlagCheck =
    FlagCheck::new(&["--check"], &[]);

static TSC_CHECK: FlagCheck =
    FlagCheck::new(&["--noEmit"], &[]);

static NVM_SAFE: WordSet =
    WordSet::new(&["--version", "current", "list", "ls", "ls-remote", "version", "which"]);

static FNM_SAFE: WordSet =
    WordSet::new(&["--version", "current", "default", "list", "ls-remote"]);

static VOLTA_SAFE: WordSet =
    WordSet::new(&["--version", "list", "which"]);

pub fn is_safe_yarn(tokens: &[Token]) -> bool {
    if tokens.len() < 2 {
        return false;
    }
    if YARN_READ_ONLY.contains(&tokens[1]) {
        return true;
    }
    tokens[1] == "test" || tokens[1].starts_with("test:")
}

pub fn is_safe_npm(tokens: &[Token]) -> bool {
    if tokens.len() < 2 {
        return false;
    }
    if NPM_READ_ONLY.contains(&tokens[1]) {
        return true;
    }
    if tokens[1] == "config" {
        return tokens
            .get(2)
            .is_some_and(|a| a == "list" || a == "get");
    }
    if tokens[1] == "run" || tokens[1] == "run-script" {
        return tokens
            .get(2)
            .is_some_and(|a| a == "test" || a.starts_with("test:"));
    }
    false
}

fn find_runner_package_index(
    tokens: &[Token],
    start: usize,
    flags: &WordSet,
) -> Option<usize> {
    let mut i = start;
    while i < tokens.len() {
        if tokens[i] == "--package" || tokens[i] == "-p" {
            i += 2;
            continue;
        }
        if flags.contains(&tokens[i]) {
            i += 1;
            continue;
        }
        if tokens[i] == "--" {
            return Some(i + 1);
        }
        if tokens[i].starts_with("-") {
            return None;
        }
        return Some(i);
    }
    None
}

fn is_safe_runner_package(tokens: &[Token], pkg_idx: usize) -> bool {
    if pkg_idx >= tokens.len() {
        return false;
    }
    if NPX_SAFE.contains(&tokens[pkg_idx]) {
        return true;
    }
    if tokens[pkg_idx] == "tsc" {
        return TSC_CHECK.is_safe(&tokens[pkg_idx + 1..]);
    }
    false
}

pub fn is_safe_npx(tokens: &[Token]) -> bool {
    if tokens.len() < 2 {
        return false;
    }
    if tokens.len() == 2 && tokens[1] == "--version" {
        return true;
    }
    find_runner_package_index(tokens, 1, &NPX_FLAGS_NO_ARG)
        .is_some_and(|idx| is_safe_runner_package(tokens, idx))
}

pub fn is_safe_bunx(tokens: &[Token]) -> bool {
    if tokens.len() < 2 {
        return false;
    }
    if tokens.len() == 2 && tokens[1] == "--version" {
        return true;
    }
    find_runner_package_index(tokens, 1, &BUNX_FLAGS_NO_ARG)
        .is_some_and(|idx| is_safe_runner_package(tokens, idx))
}

pub fn is_safe_pnpm(tokens: &[Token]) -> bool {
    tokens.len() >= 2 && PNPM_READ_ONLY.contains(&tokens[1])
}

pub fn is_safe_bun(tokens: &[Token]) -> bool {
    if tokens.len() >= 2 && tokens[1] == "x" {
        return find_runner_package_index(tokens, 2, &BUNX_FLAGS_NO_ARG)
            .is_some_and(|idx| is_safe_runner_package(tokens, idx));
    }
    super::is_safe_subcmd(tokens, &BUN_SAFE, BUN_MULTI)
}

pub fn is_safe_deno(tokens: &[Token]) -> bool {
    if tokens.len() < 2 {
        return false;
    }
    if DENO_SAFE.contains(&tokens[1]) {
        return true;
    }
    if tokens[1] == "fmt" {
        return DENO_FMT.is_safe(&tokens[2..]);
    }
    false
}

pub fn is_safe_nvm(tokens: &[Token]) -> bool {
    tokens.len() >= 2 && NVM_SAFE.contains(&tokens[1])
}

pub fn is_safe_fnm(tokens: &[Token]) -> bool {
    tokens.len() >= 2 && FNM_SAFE.contains(&tokens[1])
}

pub fn is_safe_volta(tokens: &[Token]) -> bool {
    tokens.len() >= 2 && VOLTA_SAFE.contains(&tokens[1])
}

pub fn command_docs() -> Vec<crate::docs::CommandDoc> {
    use crate::docs::{CommandDoc, DocBuilder, doc, doc_multi, wordset_items};
    vec![
        CommandDoc::handler("npm",
            doc(&NPM_READ_ONLY)
                .section("Guarded: config (list/get only), run/run-script (test/test:* only).")
                .build()),
        CommandDoc::handler("yarn",
            doc(&YARN_READ_ONLY)
                .section("Also allowed: test, test:*.")
                .build()),
        CommandDoc::wordset("pnpm", &PNPM_READ_ONLY),
        CommandDoc::handler("bun",
            doc_multi(&BUN_SAFE, BUN_MULTI)
                .section("x delegates to bunx logic.")
                .build()),
        CommandDoc::handler("bunx",
            DocBuilder::new()
                .section(format!("Allowed packages: {}.", wordset_items(&NPX_SAFE)))
                .section("Guarded: tsc (requires --noEmit).")
                .section("Skips flags: --bun/--no-install/--package/-p.")
                .build()),
        CommandDoc::handler("deno",
            doc(&DENO_SAFE)
                .section("Guarded: fmt (requires --check).")
                .build()),
        CommandDoc::handler("npx",
            DocBuilder::new()
                .section(format!("Allowed packages: {}.", wordset_items(&NPX_SAFE)))
                .section("Guarded: tsc (requires --noEmit).")
                .section("Skips flags: --yes/-y/--no/--package/-p.")
                .build()),
        CommandDoc::wordset("nvm", &NVM_SAFE),
        CommandDoc::wordset("fnm", &FNM_SAFE),
        CommandDoc::wordset("volta", &VOLTA_SAFE),
    ]
}

#[cfg(test)]
mod tests {
    use crate::is_safe_command;

    fn check(cmd: &str) -> bool {
        is_safe_command(cmd)
    }

    #[test]
    fn yarn_list() {
        assert!(check("yarn list --depth=0"));
        assert!(check("yarn ls bootstrap"));
    }

    #[test]
    fn yarn_info() {
        assert!(check("yarn info react"));
    }

    #[test]
    fn yarn_why() {
        assert!(check("yarn why lodash"));
    }

    #[test]
    fn yarn_version() {
        assert!(check("yarn --version"));
    }

    #[test]
    fn yarn_test() {
        assert!(check("yarn test"));
    }

    #[test]
    fn yarn_test_watch() {
        assert!(check("yarn test:watch"));
    }

    #[test]
    fn yarn_test_with_args() {
        assert!(check("yarn test --testPathPattern=Foo"));
    }

    #[test]
    fn yarn_install_denied() {
        assert!(!check("yarn install"));
    }

    #[test]
    fn yarn_add_denied() {
        assert!(!check("yarn add react"));
    }

    #[test]
    fn yarn_remove_denied() {
        assert!(!check("yarn remove lodash"));
    }

    #[test]
    fn yarn_upgrade_denied() {
        assert!(!check("yarn upgrade"));
    }

    #[test]
    fn npm_view() {
        assert!(check("npm view react version"));
    }

    #[test]
    fn npm_info() {
        assert!(check("npm info lodash"));
    }

    #[test]
    fn npm_list() {
        assert!(check("npm list --depth=0"));
    }

    #[test]
    fn npm_ls() {
        assert!(check("npm ls"));
    }

    #[test]
    fn npm_test() {
        assert!(check("npm test"));
    }

    #[test]
    fn npm_audit() {
        assert!(check("npm audit"));
    }

    #[test]
    fn npm_outdated() {
        assert!(check("npm outdated"));
    }

    #[test]
    fn npm_explain() {
        assert!(check("npm explain lodash"));
    }

    #[test]
    fn npm_why() {
        assert!(check("npm why lodash"));
    }

    #[test]
    fn npm_fund() {
        assert!(check("npm fund"));
    }

    #[test]
    fn npm_prefix() {
        assert!(check("npm prefix"));
    }

    #[test]
    fn npm_root() {
        assert!(check("npm root"));
    }

    #[test]
    fn npm_doctor() {
        assert!(check("npm doctor"));
    }

    #[test]
    fn npm_config_list() {
        assert!(check("npm config list"));
    }

    #[test]
    fn npm_config_get() {
        assert!(check("npm config get registry"));
    }

    #[test]
    fn npm_run_test() {
        assert!(check("npm run test"));
    }

    #[test]
    fn npm_run_test_colon() {
        assert!(check("npm run test:unit"));
    }

    #[test]
    fn npm_version() {
        assert!(check("npm --version"));
    }

    #[test]
    fn npm_install_denied() {
        assert!(!check("npm install react"));
    }

    #[test]
    fn npm_uninstall_denied() {
        assert!(!check("npm uninstall lodash"));
    }

    #[test]
    fn npm_run_build_denied() {
        assert!(!check("npm run build"));
    }

    #[test]
    fn npm_run_start_denied() {
        assert!(!check("npm run start"));
    }

    #[test]
    fn npm_config_set_denied() {
        assert!(!check("npm config set registry https://example.com"));
    }

    #[test]
    fn npx_herb_linter() {
        assert!(check("npx @herb-tools/linter app/views/foo.html.erb"));
    }

    #[test]
    fn npx_eslint() {
        assert!(check("npx eslint src/"));
    }

    #[test]
    fn npx_karma() {
        assert!(check("npx karma start"));
    }


    #[test]
    fn npx_yes_flag() {
        assert!(check("npx --yes eslint src/"));
    }

    #[test]
    fn npx_y_flag() {
        assert!(check("npx -y @herb-tools/linter ."));
    }

    #[test]
    fn npx_package_flag() {
        assert!(check(
            "npx --package @herb-tools/linter @herb-tools/linter ."
        ));
    }

    #[test]
    fn npx_double_dash() {
        assert!(check("npx -- eslint src/"));
    }

    #[test]
    fn npx_version() {
        assert!(check("npx --version"));
    }

    #[test]
    fn npx_react_scripts_denied() {
        assert!(!check("npx react-scripts start"));
    }

    #[test]
    fn npx_cowsay_denied() {
        assert!(!check("npx cowsay hello"));
    }

    #[test]
    fn bare_npx_denied() {
        assert!(!check("npx"));
    }

    #[test]
    fn npx_only_flags_denied() {
        assert!(!check("npx --yes"));
    }

    #[test]
    fn pnpm_list() {
        assert!(check("pnpm list"));
    }

    #[test]
    fn pnpm_why() {
        assert!(check("pnpm why lodash"));
    }

    #[test]
    fn pnpm_audit() {
        assert!(check("pnpm audit"));
    }

    #[test]
    fn pnpm_outdated() {
        assert!(check("pnpm outdated"));
    }

    #[test]
    fn pnpm_version() {
        assert!(check("pnpm --version"));
    }

    #[test]
    fn pnpm_install_denied() {
        assert!(!check("pnpm install"));
    }

    #[test]
    fn pnpm_add_denied() {
        assert!(!check("pnpm add react"));
    }

    #[test]
    fn pnpm_run_denied() {
        assert!(!check("pnpm run build"));
    }

    #[test]
    fn bun_version() {
        assert!(check("bun --version"));
    }

    #[test]
    fn bun_test() {
        assert!(check("bun test"));
    }

    #[test]
    fn bun_pm_ls() {
        assert!(check("bun pm ls"));
    }

    #[test]
    fn bun_pm_hash() {
        assert!(check("bun pm hash"));
    }

    #[test]
    fn bun_pm_cache() {
        assert!(check("bun pm cache"));
    }

    #[test]
    fn bun_pm_bin() {
        assert!(check("bun pm bin"));
    }

    #[test]
    fn bun_outdated() {
        assert!(check("bun outdated"));
    }

    #[test]
    fn bun_x_eslint() {
        assert!(check("bun x eslint src/"));
    }

    #[test]
    fn bun_x_tsc_noemit() {
        assert!(check("bun x tsc --noEmit"));
    }

    #[test]
    fn bun_x_tsc_denied() {
        assert!(!check("bun x tsc"));
    }

    #[test]
    fn bun_x_cowsay_denied() {
        assert!(!check("bun x cowsay hello"));
    }

    #[test]
    fn bun_install_denied() {
        assert!(!check("bun install"));
    }

    #[test]
    fn bun_run_denied() {
        assert!(!check("bun run build"));
    }

    #[test]
    fn bun_add_denied() {
        assert!(!check("bun add react"));
    }

    #[test]
    fn deno_version() {
        assert!(check("deno --version"));
    }

    #[test]
    fn deno_info() {
        assert!(check("deno info"));
    }

    #[test]
    fn deno_doc() {
        assert!(check("deno doc mod.ts"));
    }

    #[test]
    fn deno_lint() {
        assert!(check("deno lint"));
    }

    #[test]
    fn deno_check() {
        assert!(check("deno check main.ts"));
    }

    #[test]
    fn deno_test() {
        assert!(check("deno test"));
    }

    #[test]
    fn deno_fmt_check() {
        assert!(check("deno fmt --check"));
    }

    #[test]
    fn deno_fmt_denied() {
        assert!(!check("deno fmt"));
    }

    #[test]
    fn deno_run_denied() {
        assert!(!check("deno run main.ts"));
    }

    #[test]
    fn deno_install_denied() {
        assert!(!check("deno install"));
    }

    #[test]
    fn deno_compile_denied() {
        assert!(!check("deno compile main.ts"));
    }

    #[test]
    fn nvm_ls() {
        assert!(check("nvm ls"));
    }

    #[test]
    fn nvm_list() {
        assert!(check("nvm list"));
    }

    #[test]
    fn nvm_current() {
        assert!(check("nvm current"));
    }

    #[test]
    fn nvm_which() {
        assert!(check("nvm which 18"));
    }

    #[test]
    fn nvm_version() {
        assert!(check("nvm version"));
    }

    #[test]
    fn nvm_ls_remote() {
        assert!(check("nvm ls-remote"));
    }

    #[test]
    fn nvm_install_denied() {
        assert!(!check("nvm install 18"));
    }

    #[test]
    fn nvm_use_denied() {
        assert!(!check("nvm use 18"));
    }

    #[test]
    fn fnm_list() {
        assert!(check("fnm list"));
    }

    #[test]
    fn fnm_current() {
        assert!(check("fnm current"));
    }

    #[test]
    fn fnm_default() {
        assert!(check("fnm default"));
    }

    #[test]
    fn fnm_version() {
        assert!(check("fnm --version"));
    }

    #[test]
    fn fnm_install_denied() {
        assert!(!check("fnm install 18"));
    }

    #[test]
    fn fnm_use_denied() {
        assert!(!check("fnm use 18"));
    }

    #[test]
    fn volta_list() {
        assert!(check("volta list"));
    }

    #[test]
    fn volta_which() {
        assert!(check("volta which node"));
    }

    #[test]
    fn volta_version() {
        assert!(check("volta --version"));
    }

    #[test]
    fn volta_install_denied() {
        assert!(!check("volta install node@18"));
    }

    #[test]
    fn volta_pin_denied() {
        assert!(!check("volta pin node@18"));
    }

    #[test]
    fn bunx_eslint() {
        assert!(check("bunx eslint src/"));
    }

    #[test]
    fn bunx_tsc_noemit() {
        assert!(check("bunx tsc --noEmit"));
    }

    #[test]
    fn bunx_tsc_project_noemit() {
        assert!(check("bunx tsc --project tsconfig.json --noEmit"));
    }

    #[test]
    fn bunx_bun_flag() {
        assert!(check("bunx --bun eslint src/"));
    }

    #[test]
    fn bunx_no_install_flag() {
        assert!(check("bunx --no-install eslint ."));
    }

    #[test]
    fn bunx_package_flag() {
        assert!(check("bunx --package eslint eslint src/"));
    }

    #[test]
    fn bunx_double_dash() {
        assert!(check("bunx -- eslint src/"));
    }

    #[test]
    fn bunx_version() {
        assert!(check("bunx --version"));
    }

    #[test]
    fn bunx_tsc_without_noemit_denied() {
        assert!(!check("bunx tsc"));
    }

    #[test]
    fn bunx_tsc_with_other_flags_denied() {
        assert!(!check("bunx tsc --pretty"));
    }

    #[test]
    fn bunx_cowsay_denied() {
        assert!(!check("bunx cowsay hello"));
    }

    #[test]
    fn bare_bunx_denied() {
        assert!(!check("bunx"));
    }

    #[test]
    fn npx_tsc_noemit() {
        assert!(check("npx tsc --noEmit"));
    }

    #[test]
    fn npx_tsc_without_noemit_denied() {
        assert!(!check("npx tsc"));
    }
}
