use assert_cmd::assert::Assert;
use assert_cmd::cargo;
use std::env;
use std::process::Command;

use assert_cmd::prelude::*;
use predicates::prelude::*;
use predicates::str::contains;

#[test]
fn complete_word_completer() {
    assert_cmd("basic.usage.kdl", &["plugins", "install", "pl"])
        .stdout("plugin-1\nplugin-2\nplugin-3\n");
    assert_cmd("basic.usage.kdl", &["plugins", "install_desc", "pl"])
        .stdout("plugin-1\tdesc\nplugin-2\tdesc\nplugin-3\tdesc\n");
}

#[test]
fn complete_word_subcommands() {
    assert_cmd("basic.usage.kdl", &["plugins", "install"]).stdout(contains("install"));
}

#[test]
fn complete_word_cword() {
    assert_cmd("basic.usage.kdl", &["--cword=3", "plugins", "install"])
        .stdout(contains("plugin-2"));
}

#[test]
fn complete_word_long_flag() {
    assert_cmd("basic.usage.kdl", &["--", "plugins", "install", "--"]).stdout("--dir\n--global\n");
    assert_cmd("basic.usage.kdl", &["--", "plugins", "install", "--g"]).stdout("--global\n");
    assert_cmd(
        "basic.usage.kdl",
        &["--", "plugins", "install", "--global", "pl"],
    )
    .stdout(contains("plugin-2"));
}

#[test]
fn complete_word_long_flag_val() {
    assert_cmd(
        "basic.usage.kdl",
        &["--", "plugins", "install", "--dir", ""],
    )
    .stdout(contains("src").and(contains("tests")));
}

#[test]
fn complete_word_short_flag() {
    assert_cmd("basic.usage.kdl", &["--", "plugins", "install", "-"])
        .stdout("-d\n-g\n--dir\n--global\n");
    assert_cmd("basic.usage.kdl", &["--", "plugins", "install", "-g"]).stdout("-g\n");
    assert_cmd("basic.usage.kdl", &["--", "plugins", "install", "-g", "pl"])
        .stdout(contains("plugin-2"));
}

#[test]
fn complete_word_kitchen_sink() {
    assert_cmd("kitchen-sink.usage.kdl", &["--", "install", "--"])
        .stdout("--dir\n--force\n--global\n--no-force\n");
    assert_cmd("kitchen-sink.usage.kdl", &["--", "--shell", ""]).stdout("bash\nzsh\nfish\n");
}

#[test]
fn complete_word_choices() {
    assert_cmd("mise.usage.kdl", &["--", "env", "--shell", ""])
        .stdout("bash\nelvish\nfish\nnu\nxonsh\nzsh\npwsh\n");
}

#[test]
fn complete_word_choices_from_env() {
    cmd("env-choices.usage.kdl", Some("fish"))
        .env("DEPLOY_ENVS", "foo,bar baz")
        .args(["--", "--env", ""])
        .assert()
        .success()
        .stdout("foo\nbar\nbaz\n");
}

#[test]
fn complete_word_choices_from_env_unset_returns_empty() {
    cmd("env-choices.usage.kdl", Some("fish"))
        .env_remove("DEPLOY_ENVS")
        .args(["--", "--env", ""])
        .assert()
        .success()
        .stdout("");
}

#[test]
fn complete_word_default_subcommand_choices_do_not_block_root_file_fallback() {
    assert_cmd("default-subcommand-root-fallback.usage.kdl", &["--", "C"])
        .stdout(contains("Cargo.toml"));
}

#[test]
fn complete_word_shebang() {
    assert_cmd("example.sh", &["--", "-"])
        .stdout("--bar\tOption value\n--defaulted\tDefaulted value\n--foo\tFlag value\n");
}

#[test]
fn complete_word_arg_completer() {
    assert_cmd("example.sh", &["--", "v"]).stdout("val-1\nval-2\nval-3\n");
}

#[test]
fn complete_word_mounted() {
    let mut path = env::split_paths(&env::var("PATH").unwrap()).collect::<Vec<_>>();
    path.insert(
        0,
        env::current_dir()
            .unwrap()
            .join("..")
            .join("target")
            .join("debug"),
    );
    path.insert(0, env::current_dir().unwrap().join("..").join("examples"));
    env::set_var("PATH", env::join_paths(path).unwrap());
    assert_cmd("mounted.sh", &["--", "-"]).stdout("--mount\tDisplay kdl spec for mounted tasks\n");
    assert_cmd("mounted.sh", &["--", ""]).stdout("exec-task\n");
    assert_cmd("mounted.sh", &["--", "exec-task", ""]).stdout("task-a\ntask-b\n");
}

#[test]
fn complete_word_mounted_with_global_flags() {
    let mut path = env::split_paths(&env::var("PATH").unwrap()).collect::<Vec<_>>();
    path.insert(
        0,
        env::current_dir()
            .unwrap()
            .join("..")
            .join("target")
            .join("debug"),
    );
    path.insert(0, env::current_dir().unwrap().join("..").join("examples"));
    env::set_var("PATH", env::join_paths(path).unwrap());

    // Without --dir flag, should get default tasks
    assert_cmd("mounted-global-flags.sh", &["--", "run", ""])
        .stdout("task-a\tTask from default dir\ntask-b\tTask from default dir\n");

    // With --dir=dir2 flag, should get dir2 tasks
    assert_cmd(
        "mounted-global-flags.sh",
        &["--", "--dir", "dir2", "run", ""],
    )
    .stdout("task-bar\tTask from dir2\ntask-foo\tTask from dir2\n");

    // Edge case: embedded value (--dir=dir2) should also work
    assert_cmd("mounted-global-flags.sh", &["--", "--dir=dir2", "run", ""])
        .stdout("task-bar\tTask from dir2\ntask-foo\tTask from dir2\n");

    // Edge case: multiple flags with embedded values
    assert_cmd("mounted-global-flags.sh", &["--", "--dir=dir2", "run", ""])
        .stdout("task-bar\tTask from dir2\ntask-foo\tTask from dir2\n");

    // Edge case: short flag (-d) should work the same as long flag
    assert_cmd("mounted-global-flags.sh", &["--", "-d", "dir2", "run", ""])
        .stdout("task-bar\tTask from dir2\ntask-foo\tTask from dir2\n");
}

#[test]
fn complete_word_mounted_global_flag_choices() {
    // Regression for the parser-side root cause referenced by jdx/mise#10069:
    // a value-taking global flag placed before a mounted subcommand must not leak its
    // consumed tokens into the mounted task's `choices` positional arg.
    let mut path = env::split_paths(&env::var("PATH").unwrap()).collect::<Vec<_>>();
    path.insert(
        0,
        env::current_dir()
            .unwrap()
            .join("..")
            .join("target")
            .join("debug"),
    );
    path.insert(0, env::current_dir().unwrap().join("..").join("examples"));
    env::set_var("PATH", env::join_paths(path).unwrap());

    // Baseline: no global flag prefix. The mount sees no `usage_cd`, so it returns the
    // default choices. This must complete (not error) and not be polluted by stray tokens.
    assert_cmd(
        "mounted-global-flags-choices.sh",
        &["--", "run", "sample:run", ""],
    )
    .stdout("one\ntwo\n");

    // Value-taking global flag before the mounted subcommand (the failing case). The choices
    // switch to the dir2 set, which also proves `usage_cd=dir2` propagated to the mount even
    // though `run` re-declares `-C/--cd` as non-global.
    assert_cmd(
        "mounted-global-flags-choices.sh",
        &["--", "-C", "dir2", "run", "sample:run", ""],
    )
    .stdout("alpha\nbeta\ngamma\n");

    // Long form.
    assert_cmd(
        "mounted-global-flags-choices.sh",
        &["--", "--cd", "dir2", "run", "sample:run", ""],
    )
    .stdout("alpha\nbeta\ngamma\n");

    // Embedded-value form.
    assert_cmd(
        "mounted-global-flags-choices.sh",
        &["--", "--cd=dir2", "run", "sample:run", ""],
    )
    .stdout("alpha\nbeta\ngamma\n");
}

#[test]
fn complete_word_mounted_orphan_short_flag_choices() {
    // Follow-up to jdx/mise#10069: a long-only global flag re-declared as a non-global flag
    // with an ADDED short (`-r --raw`, `-S --silent`) must keep the orphan short recognized.
    // Completing a mounted task with the short in front must return the task's choices rather
    // than bailing with "unexpected word" / "Invalid choice" (which mise worked around by
    // promoting the short back to global).
    let mut path = env::split_paths(&env::var("PATH").unwrap()).collect::<Vec<_>>();
    path.insert(
        0,
        env::current_dir()
            .unwrap()
            .join("..")
            .join("target")
            .join("debug"),
    );
    path.insert(0, env::current_dir().unwrap().join("..").join("examples"));
    env::set_var("PATH", env::join_paths(path).unwrap());

    // Orphan short `-r` before the mounted task (the failing case).
    assert_cmd(
        "mounted-orphan-short-flags.sh",
        &["--", "run", "-r", "sample:run", ""],
    )
    .stdout("alpha\nbeta\ngamma\n");

    // A different orphan short (`-S`) on the same subcommand.
    assert_cmd(
        "mounted-orphan-short-flags.sh",
        &["--", "run", "-S", "sample:run", ""],
    )
    .stdout("alpha\nbeta\ngamma\n");

    // The nested `tasks run` path exercises a second descent level.
    assert_cmd(
        "mounted-orphan-short-flags.sh",
        &["--", "tasks", "run", "-r", "sample:run", ""],
    )
    .stdout("alpha\nbeta\ngamma\n");

    // The original long alias still works after the merge.
    assert_cmd(
        "mounted-orphan-short-flags.sh",
        &["--", "run", "--raw", "sample:run", ""],
    )
    .stdout("alpha\nbeta\ngamma\n");
}

#[test]
fn complete_word_boolean_flags_dont_consume_subcommands() {
    let mut path = env::split_paths(&env::var("PATH").unwrap()).collect::<Vec<_>>();
    path.insert(
        0,
        env::current_dir()
            .unwrap()
            .join("..")
            .join("target")
            .join("debug"),
    );
    path.insert(0, env::current_dir().unwrap().join("..").join("examples"));
    env::set_var("PATH", env::join_paths(path).unwrap());

    // Boolean flag --verbose before subcommand 'run' should not consume 'run'
    assert_cmd("test-boolean-flags.sh", &["--", "--verbose", "run", ""])
        .stdout("task-verbose\tTask with verbose\n");

    // Multiple boolean flags before subcommand
    assert_cmd(
        "test-boolean-flags.sh",
        &["--", "--verbose", "--debug", "run", ""],
    )
    .stdout("task-verbose-debug\tTask with verbose and debug\n");

    // Edge case: short boolean flags should work
    assert_cmd("test-boolean-flags.sh", &["--", "-v", "run", ""])
        .stdout("task-verbose\tTask with verbose\n");

    // Edge case: mixed short and long boolean flags
    assert_cmd("test-boolean-flags.sh", &["--", "-v", "-d", "run", ""])
        .stdout("task-verbose-debug\tTask with verbose and debug\n");
}

#[test]
fn complete_word_non_global_flags_stop_search() {
    let mut path = env::split_paths(&env::var("PATH").unwrap()).collect::<Vec<_>>();
    path.insert(
        0,
        env::current_dir()
            .unwrap()
            .join("..")
            .join("target")
            .join("debug"),
    );
    path.insert(0, env::current_dir().unwrap().join("..").join("examples"));
    env::set_var("PATH", env::join_paths(path).unwrap());

    // Non-global flag --local should stop subcommand search
    // The parser will fail to recognize 'run' as a subcommand and error
    let mut cmd = cmd("test-boolean-flags.sh", Some("fish"));
    cmd.args(["--", "--local", "run", ""]);
    cmd.assert().failure().stderr(contains("unexpected word"));
}

#[test]
fn complete_word_mixed_global_flags() {
    let mut path = env::split_paths(&env::var("PATH").unwrap()).collect::<Vec<_>>();
    path.insert(
        0,
        env::current_dir()
            .unwrap()
            .join("..")
            .join("target")
            .join("debug"),
    );
    path.insert(0, env::current_dir().unwrap().join("..").join("examples"));
    env::set_var("PATH", env::join_paths(path).unwrap());

    // Mix of boolean and valued flags, long and short, with embedded values
    // This test uses the test-boolean-flags fixture but we only care about
    // verifying that the flags are parsed correctly (not the actual completion)
    // Note: This won't actually change the tasks since test-boolean-flags doesn't
    // have a --dir flag, but it tests that the parser handles the combination
    assert_cmd("test-boolean-flags.sh", &["--", "-v", "--debug", "run", ""])
        .stdout("task-verbose-debug\tTask with verbose and debug\n");
}

#[test]
fn complete_word_fallback_to_files() {
    // Use a minimal spec with no args or subcommands, so any argument is unknown
    let mut cmd = Command::new(cargo::cargo_bin!("usage"));
    cmd.args([
        "cw",
        "--shell",
        "fish",
        "-f",
        "../examples/basic.usage.kdl",
        "mycli",
        "plugins",
        "install",
        "foo",
        "",
    ]);
    // Assert for files always present in the project root
    cmd.assert()
        .success()
        .stdout(contains("Cargo.toml").and(contains("src")));
}

#[test]
fn complete_word_subcommands_without_shell() {
    let mut cmd = cmd("basic.usage.kdl", None);
    cmd.args(["plugins", "install"]);
    cmd.assert().success().stdout(contains("install"));
}

#[test]
fn complete_word_escaped_colons_in_completions() {
    // When completions contain colons, they are escaped as \: in the name:description format.
    // Prefix matching should work against the unescaped names.

    // Typing "test:" should match "test:unit" and "test:integration" (unescaped)
    assert_cmd("colon-in-completions.usage.kdl", &["--", "run", "test:"])
        .stdout("test:unit\tRun unit tests\ntest:integration\tRun integration tests\n");

    // Typing "test" should also match (prefix of the unescaped name)
    assert_cmd("colon-in-completions.usage.kdl", &["--", "run", "test"])
        .stdout("test:unit\tRun unit tests\ntest:integration\tRun integration tests\n");

    // Typing "build" should only match "build"
    assert_cmd("colon-in-completions.usage.kdl", &["--", "run", "build"])
        .stdout("build\tBuild the project\n");

    // Empty input should match all completions
    assert_cmd("colon-in-completions.usage.kdl", &["--", "run", ""])
        .stdout("test:unit\tRun unit tests\ntest:integration\tRun integration tests\nbuild\tBuild the project\n");
}

#[test]
fn complete_word_zsh_three_columns_with_descriptions() {
    // zsh output is three tab-separated columns per line: raw value, raw
    // description, shell-quoted insert. No `\:`/`\(`/`\[` escaping is needed
    // because the generated completion script builds the menu display from
    // the value/description columns directly (no `_describe`).
    // See: https://github.com/jdx/usage/issues/558
    let mut c = cmd("parens-in-descriptions.usage.kdl", Some("zsh"));
    c.args(["--", "run", ""]);
    c.assert().success().stdout(
        "connect:server\tConnect server (Hot Reload)\tconnect:server\n\
         test:unit\tRun tests [fast]\ttest:unit\n\
         build\tBuild project\tbuild\n",
    );
}

#[test]
fn complete_word_zsh_three_columns_without_descriptions() {
    // Description column is empty when no description is set, but the three
    // tab-separated columns are still emitted so the receiving template can
    // parse uniformly with `read -r value desc insert`.
    let mut c = cmd("zsh-colons-without-descriptions.usage.kdl", Some("zsh"));
    c.args(["--", "run", ""]);
    c.assert()
        .success()
        .stdout("test:git\t\ttest:git\ntest:nvim\t\ttest:nvim\n");
}

fn cmd(example: &str, shell: Option<&str>) -> Command {
    let mut cmd = Command::new(cargo::cargo_bin!("usage"));
    cmd.args(["cw"]);
    if let Some(shell) = shell {
        cmd.args(["--shell", shell]);
    }
    cmd.args(["-f", &format!("../examples/{example}"), "mycli"]);
    cmd
}

fn assert_cmd(example: &str, args: &[&str]) -> Assert {
    cmd(example, Some("fish")).args(args).assert().success()
}
