use crate::code_context_menus::CompletionsMenu;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::TestAppContext;
use language::CodeLabel;
use lsp::{CompletionItem, CompletionItemKind, LanguageServerId};
use project::{Completion, CompletionSource};
use settings::SnippetSortOrder;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use text::{Anchor, BufferId};

#[gpui::test]
async fn test_sort_kind(cx: &mut TestAppContext) {
    let completions = vec![
        CompletionBuilder::function("floorf128", None, "80000000"),
        CompletionBuilder::constant("foo_bar_baz", None, "80000000"),
        CompletionBuilder::variable("foo_bar_qux", None, "80000000"),
    ];
    let matches =
        filter_and_sort_matches("foo", &completions, SnippetSortOrder::default(), cx).await;

    // variable takes precedence over constant
    // constant take precedence over function
    assert_eq!(
        matches
            .iter()
            .map(|m| m.string.as_str())
            .collect::<Vec<_>>(),
        vec!["foo_bar_qux", "foo_bar_baz", "floorf128"]
    );

    // fuzzy score should match for first two items as query is common prefix
    assert_eq!(matches[0].score, matches[1].score);
}

#[gpui::test]
async fn test_fuzzy_score(cx: &mut TestAppContext) {
    // first character sensitive over sort_text and sort_kind
    {
        let completions = vec![
            CompletionBuilder::variable("element_type", None, "7ffffffe"),
            CompletionBuilder::constant("ElementType", None, "7fffffff"),
        ];
        let matches =
            filter_and_sort_matches("Elem", &completions, SnippetSortOrder::default(), cx).await;
        assert_eq!(
            matches
                .iter()
                .map(|m| m.string.as_str())
                .collect::<Vec<_>>(),
            vec!["ElementType", "element_type"]
        );
        assert!(matches[0].score > matches[1].score);
    }

    // fuzzy takes over sort_text and sort_kind
    {
        let completions = vec![
            CompletionBuilder::function("onAbort?", None, "12"),
            CompletionBuilder::function("onAuxClick?", None, "12"),
            CompletionBuilder::variable("onPlay?", None, "12"),
            CompletionBuilder::variable("onLoad?", None, "12"),
            CompletionBuilder::variable("onDrag?", None, "12"),
            CompletionBuilder::function("onPause?", None, "10"),
            CompletionBuilder::function("onPaste?", None, "10"),
            CompletionBuilder::function("onAnimationEnd?", None, "12"),
            CompletionBuilder::function("onAbortCapture?", None, "12"),
            CompletionBuilder::constant("onChange?", None, "12"),
            CompletionBuilder::constant("onWaiting?", None, "12"),
            CompletionBuilder::function("onCanPlay?", None, "12"),
        ];
        let matches =
            filter_and_sort_matches("ona", &completions, SnippetSortOrder::default(), cx).await;
        for i in 0..4 {
            assert!(matches[i].string.to_lowercase().starts_with("ona"));
        }
    }

    // plain fuzzy prefix match
    {
        let completions = vec![
            CompletionBuilder::function("set_text", None, "7fffffff"),
            CompletionBuilder::function("set_placeholder_text", None, "7fffffff"),
            CompletionBuilder::function("set_text_style_refinement", None, "7fffffff"),
            CompletionBuilder::function("set_context_menu_options", None, "7fffffff"),
            CompletionBuilder::function("select_to_next_word_end", None, "7fffffff"),
            CompletionBuilder::function("select_to_next_subword_end", None, "7fffffff"),
            CompletionBuilder::function("set_custom_context_menu", None, "7fffffff"),
            CompletionBuilder::function("select_to_end_of_excerpt", None, "7fffffff"),
            CompletionBuilder::function("select_to_start_of_excerpt", None, "7fffffff"),
            CompletionBuilder::function("select_to_start_of_next_excerpt", None, "7fffffff"),
            CompletionBuilder::function("select_to_end_of_previous_excerpt", None, "7fffffff"),
        ];
        let matches =
            filter_and_sort_matches("set_text", &completions, SnippetSortOrder::Top, cx).await;
        assert_eq!(matches[0].string, "set_text");
        assert_eq!(matches[1].string, "set_text_style_refinement");
        assert_eq!(matches[2].string, "set_placeholder_text");
    }

    // fuzzy filter text over label, sort_text and sort_kind
    {
        // Case 1: "awa"
        let completions = vec![
            CompletionBuilder::method("await", Some("await"), "7fffffff"),
            CompletionBuilder::method("await.ne", Some("ne"), "80000010"),
            CompletionBuilder::method("await.eq", Some("eq"), "80000010"),
            CompletionBuilder::method("await.or", Some("or"), "7ffffff8"),
            CompletionBuilder::method("await.zip", Some("zip"), "80000006"),
            CompletionBuilder::method("await.xor", Some("xor"), "7ffffff8"),
            CompletionBuilder::method("await.and", Some("and"), "80000006"),
            CompletionBuilder::method("await.map", Some("map"), "80000006"),
        ];

        test_for_each_prefix("await", &completions, cx, |matches| {
            // for each prefix, first item should always be one with lower sort_text
            assert_eq!(matches[0].string, "await");
        })
        .await;
    }
}

#[gpui::test]
async fn test_sort_text(cx: &mut TestAppContext) {
    // sort text takes precedance over sort_kind, when fuzzy is same
    {
        let completions = vec![
            CompletionBuilder::variable("unreachable", None, "80000000"),
            CompletionBuilder::function("unreachable!(…)", None, "7fffffff"),
            CompletionBuilder::function("unchecked_rem", None, "80000010"),
            CompletionBuilder::function("unreachable_unchecked", None, "80000020"),
        ];

        test_for_each_prefix("unreachabl", &completions, cx, |matches| {
            // for each prefix, first item should always be one with lower sort_text
            assert_eq!(matches[0].string, "unreachable!(…)");
            assert_eq!(matches[1].string, "unreachable");

            // fuzzy score should match for first two items as query is common prefix
            assert_eq!(matches[0].score, matches[1].score);
        })
        .await;

        let matches =
            filter_and_sort_matches("unreachable", &completions, SnippetSortOrder::Top, cx).await;
        // exact match comes first
        assert_eq!(matches[0].string, "unreachable");
        assert_eq!(matches[1].string, "unreachable!(…)");

        // fuzzy score should match for first two items as query is common prefix
        assert_eq!(matches[0].score, matches[1].score);
    }
}

#[gpui::test]
async fn test_sort_snippet(cx: &mut TestAppContext) {
    let completions = vec![
        CompletionBuilder::constant("println", None, "7fffffff"),
        CompletionBuilder::snippet("println!(…)", None, "80000000"),
    ];
    let matches = filter_and_sort_matches("prin", &completions, SnippetSortOrder::Top, cx).await;

    // snippet take precedence over sort_text and sort_kind
    assert_eq!(matches[0].string, "println!(…)");
}

#[gpui::test]
async fn test_sort_exact(cx: &mut TestAppContext) {
    // sort_text takes over if no exact match
    let completions = vec![
        CompletionBuilder::function("into", None, "80000004"),
        CompletionBuilder::function("try_into", None, "80000004"),
        CompletionBuilder::snippet("println", None, "80000004"),
        CompletionBuilder::function("clone_into", None, "80000004"),
        CompletionBuilder::function("into_searcher", None, "80000000"),
        CompletionBuilder::snippet("eprintln", None, "80000004"),
    ];
    let matches =
        filter_and_sort_matches("int", &completions, SnippetSortOrder::default(), cx).await;
    assert_eq!(matches[0].string, "into_searcher");

    // exact match takes over sort_text
    let completions = vec![
        CompletionBuilder::function("into", None, "80000004"),
        CompletionBuilder::function("try_into", None, "80000004"),
        CompletionBuilder::function("clone_into", None, "80000004"),
        CompletionBuilder::function("into_searcher", None, "80000000"),
        CompletionBuilder::function("split_terminator", None, "7fffffff"),
        CompletionBuilder::function("rsplit_terminator", None, "7fffffff"),
    ];
    let matches =
        filter_and_sort_matches("into", &completions, SnippetSortOrder::default(), cx).await;
    assert_eq!(matches[0].string, "into");
}

#[gpui::test]
async fn test_sort_positions(cx: &mut TestAppContext) {
    // positions take precedence over fuzzy score and sort_text
    let completions = vec![
        CompletionBuilder::function("rounded-full", None, "15788"),
        CompletionBuilder::variable("rounded-t-full", None, "15846"),
        CompletionBuilder::variable("rounded-b-full", None, "15731"),
        CompletionBuilder::function("rounded-tr-full", None, "15866"),
    ];

    let matches = filter_and_sort_matches(
        "rounded-full",
        &completions,
        SnippetSortOrder::default(),
        cx,
    )
    .await;
    assert_eq!(matches[0].string, "rounded-full");

    let matches =
        filter_and_sort_matches("roundedfull", &completions, SnippetSortOrder::default(), cx).await;
    assert_eq!(matches[0].string, "rounded-full");
}

#[gpui::test]
async fn test_case_sensitive_match_tie_breaker(cx: &mut TestAppContext) {
    let completions = vec![
        CompletionBuilder::variable("abc", None, "11"),
        CompletionBuilder::variable("ABC", None, "11"),
    ];

    let matches = filter_and_sort_matches("a", &completions, SnippetSortOrder::default(), cx).await;
    assert_eq!(
        matches
            .iter()
            .map(|m| m.string.as_str())
            .collect::<Vec<_>>(),
        vec!["abc", "ABC"]
    );

    let matches = filter_and_sort_matches("A", &completions, SnippetSortOrder::default(), cx).await;
    assert_eq!(
        matches
            .iter()
            .map(|m| m.string.as_str())
            .collect::<Vec<_>>(),
        vec!["ABC", "abc"]
    );

    let matches =
        filter_and_sort_matches("ab", &completions, SnippetSortOrder::default(), cx).await;
    assert_eq!(
        matches
            .iter()
            .map(|m| m.string.as_str())
            .collect::<Vec<_>>(),
        vec!["abc", "ABC"]
    );

    let matches =
        filter_and_sort_matches("AB", &completions, SnippetSortOrder::default(), cx).await;
    assert_eq!(
        matches
            .iter()
            .map(|m| m.string.as_str())
            .collect::<Vec<_>>(),
        vec!["ABC", "abc"]
    );

    let completions = vec![
        CompletionBuilder::variable("aBc", None, "11"),
        CompletionBuilder::variable("Abc", None, "11"),
    ];

    let matches =
        filter_and_sort_matches("Ab", &completions, SnippetSortOrder::default(), cx).await;
    assert_eq!(
        matches
            .iter()
            .map(|m| m.string.as_str())
            .collect::<Vec<_>>(),
        vec!["Abc", "aBc"]
    );

    let matches =
        filter_and_sort_matches("aB", &completions, SnippetSortOrder::default(), cx).await;
    assert_eq!(
        matches
            .iter()
            .map(|m| m.string.as_str())
            .collect::<Vec<_>>(),
        vec!["aBc", "Abc"]
    );
}

#[gpui::test]
async fn test_fuzzy_over_sort_positions(cx: &mut TestAppContext) {
    let completions = vec![
        CompletionBuilder::variable("lsp_document_colors", None, "7fffffff"), // 0.29 fuzzy score
        CompletionBuilder::function(
            "language_servers_running_disk_based_diagnostics",
            None,
            "7fffffff",
        ), // 0.168 fuzzy score
        CompletionBuilder::function("code_lens", None, "7fffffff"),           // 3.2 fuzzy score
        CompletionBuilder::variable("lsp_code_lens", None, "7fffffff"),       // 3.2 fuzzy score
        CompletionBuilder::function("fetch_code_lens", None, "7fffffff"),     // 3.2 fuzzy score
    ];

    let matches =
        filter_and_sort_matches("lens", &completions, SnippetSortOrder::default(), cx).await;

    assert_eq!(matches[0].string, "code_lens");
    assert_eq!(matches[1].string, "lsp_code_lens");
    assert_eq!(matches[2].string, "fetch_code_lens");
}

#[gpui::test]
async fn test_semver_label_sort_by_latest_version(cx: &mut TestAppContext) {
    let mut versions = [
        "10.4.112",
        "10.4.22",
        "10.4.2",
        "10.4.20",
        "10.4.21",
        "10.4.12",
        // Pre-release versions
        "10.4.22-alpha",
        "10.4.22-beta.1",
        "10.4.22-rc.1",
        // Build metadata versions
        "10.4.21+build.123",
        "10.4.20+20210327",
    ];
    versions.sort_by(|a, b| {
        match (
            semver::Version::parse(a).ok(),
            semver::Version::parse(b).ok(),
        ) {
            (Some(a_ver), Some(b_ver)) => b_ver.cmp(&a_ver),
            _ => std::cmp::Ordering::Equal,
        }
    });
    let completions: Vec<_> = versions
        .iter()
        .enumerate()
        .map(|(i, version)| {
            // This sort text would come from the LSP
            let sort_text = format!("{:08}", i);
            CompletionBuilder::new(version, None, &sort_text, None)
        })
        .collect();

    // Case 1: User types just the major and minor version
    let matches =
        filter_and_sort_matches("10.4.", &completions, SnippetSortOrder::default(), cx).await;
    // Versions are ordered by recency (latest first)
    let expected_versions = [
        "10.4.112",
        "10.4.22",
        "10.4.22-rc.1",
        "10.4.22-beta.1",
        "10.4.22-alpha",
        "10.4.21+build.123",
        "10.4.21",
        "10.4.20+20210327",
        "10.4.20",
        "10.4.12",
        "10.4.2",
    ];
    for (match_item, expected) in matches.iter().zip(expected_versions.iter()) {
        assert_eq!(match_item.string.as_ref() as &str, *expected);
    }

    // Case 2: User types the major, minor, and patch version
    let matches =
        filter_and_sort_matches("10.4.2", &completions, SnippetSortOrder::default(), cx).await;
    let expected_versions = [
        // Exact match comes first
        "10.4.2",
        // Ordered by recency with exact major, minor, and patch versions
        "10.4.22",
        "10.4.22-rc.1",
        "10.4.22-beta.1",
        "10.4.22-alpha",
        "10.4.21+build.123",
        "10.4.21",
        "10.4.20+20210327",
        "10.4.20",
        // Versions with non-exact patch versions are ordered by fuzzy score
        // Higher fuzzy score than 112 patch version since "2" appears before "1"
        // in "12", making it rank higher than "112"
        "10.4.12",
        "10.4.112",
    ];
    for (match_item, expected) in matches.iter().zip(expected_versions.iter()) {
        assert_eq!(match_item.string.as_ref() as &str, *expected);
    }
}

async fn test_for_each_prefix<F>(
    target: &str,
    completions: &Vec<Completion>,
    cx: &mut TestAppContext,
    mut test_fn: F,
) where
    F: FnMut(Vec<StringMatch>),
{
    for i in 1..=target.len() {
        let prefix = &target[..i];
        let matches =
            filter_and_sort_matches(prefix, completions, SnippetSortOrder::default(), cx).await;
        test_fn(matches);
    }
}

struct CompletionBuilder;

impl CompletionBuilder {
    fn constant(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
        Self::new(
            label,
            filter_text,
            sort_text,
            Some(CompletionItemKind::CONSTANT),
        )
    }

    fn function(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
        Self::new(
            label,
            filter_text,
            sort_text,
            Some(CompletionItemKind::FUNCTION),
        )
    }

    fn method(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
        Self::new(
            label,
            filter_text,
            sort_text,
            Some(CompletionItemKind::METHOD),
        )
    }

    fn variable(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
        Self::new(
            label,
            filter_text,
            sort_text,
            Some(CompletionItemKind::VARIABLE),
        )
    }

    fn snippet(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion {
        Self::new(
            label,
            filter_text,
            sort_text,
            Some(CompletionItemKind::SNIPPET),
        )
    }

    fn new(
        label: &str,
        filter_text: Option<&str>,
        sort_text: &str,
        kind: Option<CompletionItemKind>,
    ) -> Completion {
        Completion {
            replace_range: Anchor::min_max_range_for_buffer(BufferId::new(1).unwrap()),
            new_text: label.to_string(),
            label: CodeLabel::plain(label.to_string(), filter_text),
            documentation: None,
            source: CompletionSource::Lsp {
                insert_range: None,
                server_id: LanguageServerId(0),
                lsp_completion: Box::new(CompletionItem {
                    label: label.to_string(),
                    kind: kind,
                    sort_text: Some(sort_text.to_string()),
                    filter_text: filter_text.map(|text| text.to_string()),
                    ..Default::default()
                }),
                lsp_defaults: None,
                resolved: false,
            },
            icon_path: None,
            insert_text_mode: None,
            confirm: None,
            match_start: None,
            snippet_deduplication_key: None,
            group: None,
        }
    }
}

async fn filter_and_sort_matches(
    query: &str,
    completions: &Vec<Completion>,
    snippet_sort_order: SnippetSortOrder,
    cx: &mut TestAppContext,
) -> Vec<StringMatch> {
    let candidates: Arc<[StringMatchCandidate]> = completions
        .iter()
        .enumerate()
        .map(|(id, completion)| StringMatchCandidate::new(id, completion.label.filter_text()))
        .collect();
    let cancel_flag = Arc::new(AtomicBool::new(false));
    let background_executor = cx.executor();
    let matches = fuzzy::match_strings(
        &candidates,
        query,
        query.chars().any(|c| c.is_uppercase()),
        false,
        100,
        &cancel_flag,
        background_executor,
    )
    .await;
    CompletionsMenu::sort_string_matches(matches, Some(query), snippet_sort_order, completions)
}
