//! Completion integration tests for brush shell.

// For now, only compile this for Linux.
#![cfg(target_os = "linux")]
#![cfg(test)]
#![allow(clippy::panic_in_result_fn)]

use anyhow::Result;
use assert_fs::prelude::*;
use brush_builtins::ShellBuilderExt;
use std::path::PathBuf;

struct TestShellWithBashCompletion {
    shell: brush_core::Shell,
    temp_dir: assert_fs::TempDir,
}

const DEFAULT_BASH_COMPLETION_SCRIPT: &str = "/usr/share/bash-completion/bash_completion";

impl TestShellWithBashCompletion {
    async fn new() -> Result<Self> {
        let mut shell = brush_core::Shell::builder()
            .profile(brush_core::ProfileLoadBehavior::Skip)
            .rc(brush_core::RcLoadBehavior::Skip)
            .default_builtins(brush_builtins::BuiltinSet::BashMode)
            .build()
            .await?;

        let temp_dir = assert_fs::TempDir::new()?;
        let bash_completion_script_path = Self::find_bash_completion_script()?;

        let exec_params = shell.default_exec_params();
        let source_result = shell
            .source_script(
                bash_completion_script_path.as_path(),
                std::iter::empty::<String>(),
                &exec_params,
            )
            .await?;

        if !source_result.is_success() {
            return Err(anyhow::anyhow!("failed to source bash completion script"));
        }

        shell.set_working_dir(temp_dir.path())?;

        Ok(Self { shell, temp_dir })
    }

    fn find_bash_completion_script() -> Result<PathBuf> {
        // See if an environmental override was provided.
        let script_path = std::env::var("BASH_COMPLETION_PATH").map_or_else(
            |_| PathBuf::from(DEFAULT_BASH_COMPLETION_SCRIPT),
            PathBuf::from,
        );

        if script_path.exists() {
            Ok(script_path)
        } else {
            Err(anyhow::anyhow!(
                "bash completion script not found: {}",
                script_path.display()
            ))
        }
    }

    pub async fn complete_end_of_line(&mut self, line: &str) -> Result<Vec<String>> {
        self.complete(line, line.len()).await
    }

    pub async fn complete(&mut self, line: &str, pos: usize) -> Result<Vec<String>> {
        let completions = self.shell.complete(line, pos).await?;
        Ok(completions
            .candidates
            .into_iter()
            .collect::<indexmap::IndexSet<_>>()
            .into_iter()
            .collect())
    }

    pub fn set_var(&mut self, name: &str, value: &str) -> Result<()> {
        self.shell
            .env_mut()
            .set_global(name, brush_core::ShellVariable::new(value))?;
        Ok(())
    }
}

#[test_with::file(/usr/share/bash-completion/bash_completion)]
#[tokio::test(flavor = "multi_thread")]
async fn complete_relative_file_path() -> Result<()> {
    let mut test_shell = TestShellWithBashCompletion::new().await?;

    // Create file and dir.
    test_shell.temp_dir.child("item1").touch()?;
    test_shell.temp_dir.child("item2").create_dir_all()?;
    test_shell.temp_dir.child(".dot_item1").touch()?;
    test_shell.temp_dir.child("..dot_item2").touch()?;

    // Complete; expect to see the two files.
    let mut results = test_shell.complete_end_of_line("ls item").await?;

    assert_eq!(results, ["item1", "item2"]);

    results = test_shell.complete_end_of_line("ls .").await?;
    // Some versions of bash-completion filter out "." and ".." via
    // `-X '?(*/)@(.|..)'`; others don't. Accept either outcome.
    assert!(
        results == ["..dot_item2", ".dot_item1"]
            || results == [".", "..", "..dot_item2", ".dot_item1"],
        "unexpected completions for 'ls .': {results:?}"
    );

    results = test_shell.complete_end_of_line("ls ..").await?;
    assert_eq!(results, ["..", "..dot_item2"]);

    Ok(())
}

#[test_with::file(/usr/share/bash-completion/bash_completion)]
#[tokio::test(flavor = "multi_thread")]
async fn complete_relative_file_path_ignoring_case() -> Result<()> {
    let mut test_shell = TestShellWithBashCompletion::new().await?;
    test_shell
        .shell
        .options_mut()
        .case_insensitive_pathname_expansion = true;

    // Create file and dir.
    test_shell.temp_dir.child("ITEM1").touch()?;
    test_shell.temp_dir.child("item2").create_dir_all()?;

    // Complete; expect to see the two files.
    let results = test_shell.complete_end_of_line("ls item").await?;

    assert_eq!(results, ["ITEM1", "item2"]);

    Ok(())
}

#[test_with::file(/usr/share/bash-completion/bash_completion)]
#[tokio::test(flavor = "multi_thread")]
async fn complete_relative_dir_path() -> Result<()> {
    let mut test_shell = TestShellWithBashCompletion::new().await?;

    // Create file and dir.
    test_shell.temp_dir.child("item1").touch()?;
    test_shell.temp_dir.child("item2").create_dir_all()?;

    // Complete; expect to see just the dir.
    let results = test_shell.complete_end_of_line("cd item").await?;

    assert_eq!(results, ["item2"]);

    Ok(())
}

#[test_with::file(/usr/share/bash-completion/bash_completion)]
#[tokio::test(flavor = "multi_thread")]
async fn complete_under_empty_dir() -> Result<()> {
    let mut test_shell = TestShellWithBashCompletion::new().await?;

    // Create file and dir.
    test_shell.temp_dir.child("empty").create_dir_all()?;

    // Complete; expect to see nothing.
    let results = test_shell.complete_end_of_line("ls empty/").await?;

    assert_eq!(results, Vec::<String>::new());

    Ok(())
}

#[test_with::file(/usr/share/bash-completion/bash_completion)]
#[tokio::test(flavor = "multi_thread")]
async fn complete_nonexistent_relative_path() -> Result<()> {
    let mut test_shell = TestShellWithBashCompletion::new().await?;

    // Complete; expect to see nothing.
    let results = test_shell.complete_end_of_line("ls item").await?;

    assert_eq!(results, Vec::<String>::new());

    Ok(())
}

#[test_with::file(/usr/share/bash-completion/bash_completion)]
#[tokio::test(flavor = "multi_thread")]
async fn complete_absolute_paths() -> Result<()> {
    let mut test_shell = TestShellWithBashCompletion::new().await?;

    // Create file and dir.
    test_shell.temp_dir.child("item1").touch()?;
    test_shell.temp_dir.child("item2").create_dir_all()?;

    // Complete; expect to see just the dir.
    let input = std::format!("ls {}", test_shell.temp_dir.path().join("item").display());
    let results = test_shell.complete_end_of_line(input.as_str()).await?;

    assert_eq!(
        results,
        [
            test_shell
                .temp_dir
                .child("item1")
                .path()
                .display()
                .to_string(),
            test_shell
                .temp_dir
                .child("item2")
                .path()
                .display()
                .to_string(),
        ]
    );

    Ok(())
}

#[test_with::file(/usr/share/bash-completion/bash_completion)]
#[tokio::test(flavor = "multi_thread")]
async fn complete_path_with_var() -> Result<()> {
    let mut test_shell = TestShellWithBashCompletion::new().await?;

    // Create file and dir.
    test_shell.temp_dir.child("item1").touch()?;
    test_shell.temp_dir.child("item2").create_dir_all()?;

    // Complete; expect to see the two files.
    let results = test_shell.complete_end_of_line("ls $PWD/item").await?;

    assert_eq!(
        results,
        [
            test_shell
                .temp_dir
                .child("item1")
                .path()
                .display()
                .to_string(),
            test_shell
                .temp_dir
                .child("item2")
                .path()
                .display()
                .to_string(),
        ]
    );

    Ok(())
}

#[test_with::file(/usr/share/bash-completion/bash_completion)]
#[tokio::test(flavor = "multi_thread")]
async fn complete_path_with_tilde() -> Result<()> {
    let mut test_shell = TestShellWithBashCompletion::new().await?;

    // Set HOME to the temp dir so we can use ~ to reference it.
    test_shell.set_var(
        "HOME",
        test_shell
            .temp_dir
            .path()
            .to_string_lossy()
            .to_string()
            .as_str(),
    )?;

    // Create file and dir.
    test_shell.temp_dir.child("item1").touch()?;
    test_shell.temp_dir.child("item2").create_dir_all()?;

    // Complete; expect to see the two files.
    let results = test_shell.complete_end_of_line("ls ~/item").await?;

    assert_eq!(
        results,
        [
            test_shell
                .temp_dir
                .child("item1")
                .path()
                .display()
                .to_string(),
            test_shell
                .temp_dir
                .child("item2")
                .path()
                .display()
                .to_string(),
        ]
    );

    Ok(())
}

#[test_with::file(/usr/share/bash-completion/bash_completion)]
#[tokio::test(flavor = "multi_thread")]
async fn complete_variable_names() -> Result<()> {
    let mut test_shell = TestShellWithBashCompletion::new().await?;

    // Set a few vars.
    test_shell.set_var("TESTVAR1", "")?;
    test_shell.set_var("TESTVAR2", "")?;

    // Complete.
    let results = test_shell.complete_end_of_line("echo $TESTVAR").await?;
    assert_eq!(results, ["$TESTVAR1", "$TESTVAR2"]);

    Ok(())
}

#[test_with::file(/usr/share/bash-completion/bash_completion)]
#[tokio::test(flavor = "multi_thread")]
async fn complete_variable_names_with_braces() -> Result<()> {
    let mut test_shell = TestShellWithBashCompletion::new().await?;

    // Set a few vars.
    test_shell.set_var("TESTVAR1", "")?;
    test_shell.set_var("TESTVAR2", "")?;

    // Complete.
    let results = test_shell.complete_end_of_line("echo ${TESTVAR").await?;
    assert_eq!(results, ["${TESTVAR1}", "${TESTVAR2}"]);

    Ok(())
}

#[test_with::file(/usr/share/bash-completion/bash_completion)]
#[tokio::test(flavor = "multi_thread")]
async fn complete_help_topic() -> Result<()> {
    let mut test_shell = TestShellWithBashCompletion::new().await?;

    // Complete.
    let results = test_shell.complete_end_of_line("help expor").await?;
    assert_eq!(results, ["export"]);

    Ok(())
}

#[test_with::file(/usr/share/bash-completion/bash_completion)]
#[tokio::test(flavor = "multi_thread")]
async fn complete_command_option() -> Result<()> {
    let mut test_shell = TestShellWithBashCompletion::new().await?;

    // Complete.
    let results = test_shell.complete_end_of_line("ls --hel").await?;
    assert_eq!(results, ["--help"]);

    Ok(())
}

/// Tests completion with some well-known programs that have been good manual test cases
/// for us in the past.
#[test_with::file(/usr/share/bash-completion/bash_completion)]
#[tokio::test(flavor = "multi_thread")]
async fn complete_path_args_to_well_known_programs() -> Result<()> {
    let mut test_shell = TestShellWithBashCompletion::new().await?;

    // Create file and dir.
    test_shell.temp_dir.child("item1").touch()?;
    test_shell.temp_dir.child("item2").create_dir_all()?;

    // Complete.
    let results = test_shell.complete_end_of_line("tar tvf ./item").await?;

    assert_eq!(results, ["./item2"]);

    Ok(())
}

/// Tests some 'find' completion.
#[test_with::file(/usr/share/bash-completion/bash_completion)]
#[tokio::test(flavor = "multi_thread")]
async fn complete_find_command() -> Result<()> {
    let mut test_shell = TestShellWithBashCompletion::new().await?;

    // Complete.
    let results = test_shell.complete_end_of_line("find . -na").await?;

    assert_eq!(results, ["-name"]);

    Ok(())
}

#[test_with::file(/usr/share/bash-completion/bash_completion)]
#[tokio::test(flavor = "multi_thread")]
async fn complete_quoted_filenames() -> Result<()> {
    let mut test_shell = TestShellWithBashCompletion::new().await?;

    test_shell.temp_dir.child("item1 item2").touch()?;
    test_shell.temp_dir.child("item1'item2").touch()?;

    let mut results = test_shell.complete_end_of_line("ls item1\\ ").await?;
    assert_eq!(results, ["item1 item2"]);

    results = test_shell.complete_end_of_line("ls item1'").await?;
    assert_eq!(results, ["item1 item2", "item1'item2"]);

    results = test_shell.complete_end_of_line("ls item1").await?;
    assert_eq!(results, ["item1 item2", "item1'item2"]);

    results = test_shell.complete_end_of_line("ls 'item1 ").await?;
    assert_eq!(results, ["item1 item2"]);

    results = test_shell.complete_end_of_line("ls \"item1 ").await?;
    assert_eq!(results, ["item1 item2"]);

    Ok(())
}

/// Tests that interactive completion sets `COMP_KEY` and `COMP_TYPE` to 9 (TAB).
#[tokio::test(flavor = "multi_thread")]
async fn interactive_completion_sets_comp_key_and_comp_type() -> Result<()> {
    let mut shell = brush_core::Shell::builder()
        .profile(brush_core::ProfileLoadBehavior::Skip)
        .rc(brush_core::RcLoadBehavior::Skip)
        .default_builtins(brush_builtins::BuiltinSet::BashMode)
        .build()
        .await?;

    // Register a completion function that captures COMP_KEY and COMP_TYPE.
    let exec_params = shell.default_exec_params();
    let source_info = brush_core::SourceInfo::default();
    shell
        .run_string(
            r"
_test_comp() {
    CAPTURED_COMP_KEY=$COMP_KEY
    CAPTURED_COMP_TYPE=$COMP_TYPE
    COMPREPLY=(done)
}
complete -F _test_comp mycmd
"
            .to_string(),
            &source_info,
            &exec_params,
        )
        .await?;

    // Trigger interactive completion.
    let _completions = shell.complete("mycmd ", 6).await?;

    // Check the captured values.
    let comp_key = shell
        .env()
        .get("CAPTURED_COMP_KEY")
        .map(|(_, v)| v.value().to_cow_str(&shell).to_string());
    let comp_type = shell
        .env()
        .get("CAPTURED_COMP_TYPE")
        .map(|(_, v)| v.value().to_cow_str(&shell).to_string());

    assert_eq!(comp_key.as_deref(), Some("9"), "COMP_KEY should be 9 (TAB)");
    assert_eq!(
        comp_type.as_deref(),
        Some("9"),
        "COMP_TYPE should be 9 (TAB)"
    );

    Ok(())
}

// Tests for native completion fallback (without bash-completion installed)

struct TestShellNative {
    shell: brush_core::Shell,
    temp_dir: assert_fs::TempDir,
}

impl TestShellNative {
    async fn new() -> Result<Self> {
        let mut shell = brush_core::Shell::builder()
            .profile(brush_core::ProfileLoadBehavior::Skip)
            .rc(brush_core::RcLoadBehavior::Skip)
            .default_builtins(brush_builtins::BuiltinSet::BashMode)
            .build()
            .await?;

        let temp_dir = assert_fs::TempDir::new()?;
        shell.set_working_dir(temp_dir.path())?;

        Ok(Self { shell, temp_dir })
    }

    pub async fn complete_end_of_line_full(
        &mut self,
        line: &str,
    ) -> Result<brush_core::completion::Completions> {
        Ok(self.shell.complete(line, line.len()).await?)
    }

    pub fn set_var(&mut self, name: &str, value: &str) -> Result<()> {
        self.shell
            .env_mut()
            .set_global(name, brush_core::ShellVariable::new(value))?;
        Ok(())
    }
}

/// Tests native variable completion without braces (e.g., $VAR)
#[tokio::test(flavor = "multi_thread")]
async fn native_complete_variable_names() -> Result<()> {
    let mut test_shell = TestShellNative::new().await?;

    // Set test variables.
    test_shell.set_var("TESTVAR1", "value1")?;
    test_shell.set_var("TESTVAR2", "value2")?;

    // Complete.
    let completions = test_shell
        .complete_end_of_line_full("echo $TESTVAR")
        .await?;
    let results: Vec<String> = completions.candidates.into_iter().collect();
    assert_eq!(results, ["$TESTVAR1", "$TESTVAR2"]);

    // Variable completions should not be treated as filenames (to avoid escaping $)
    assert!(
        !completions.options.treat_as_filenames,
        "variable completions should not be treated as filenames"
    );

    Ok(())
}

/// Tests native variable completion with braces (e.g., ${VAR})
#[tokio::test(flavor = "multi_thread")]
async fn native_complete_variable_names_with_braces() -> Result<()> {
    let mut test_shell = TestShellNative::new().await?;

    // Set test variables.
    test_shell.set_var("TESTVAR1", "value1")?;
    test_shell.set_var("TESTVAR2", "value2")?;

    // Complete.
    let completions = test_shell
        .complete_end_of_line_full("echo ${TESTVAR")
        .await?;
    let results: Vec<String> = completions.candidates.into_iter().collect();
    assert_eq!(results, ["${TESTVAR1}", "${TESTVAR2}"]);

    // Variable completions should not be treated as filenames (to avoid escaping $)
    assert!(
        !completions.options.treat_as_filenames,
        "variable completions should not be treated as filenames"
    );

    Ok(())
}

/// Tests that file completion works after a variable (e.g., $VAR/path)
#[tokio::test(flavor = "multi_thread")]
async fn native_complete_path_after_variable() -> Result<()> {
    let mut test_shell = TestShellNative::new().await?;

    // Create files in temp dir.
    test_shell.temp_dir.child("file1.txt").touch()?;
    test_shell.temp_dir.child("file2.txt").touch()?;

    // Set a variable pointing to the temp dir.
    let temp_path = test_shell.temp_dir.path().to_str().unwrap().to_owned();
    test_shell.set_var("MYDIR", &temp_path)?;

    // Complete files after variable expansion.
    // The completion system expands variables before completing, so results use expanded paths.
    let completions = test_shell
        .complete_end_of_line_full("ls $MYDIR/file")
        .await?;
    let results: Vec<String> = completions.candidates.into_iter().collect();
    let expected = [
        std::format!("{temp_path}/file1.txt"),
        std::format!("{temp_path}/file2.txt"),
    ];
    assert_eq!(results, expected);

    // Path completions should be treated as filenames
    assert!(
        completions.options.treat_as_filenames,
        "path completions should be treated as filenames"
    );

    Ok(())
}
