//! Postfix completions, like `Ok(10).ifl$0` => `if let Ok() = Ok(10) { $0 }`.

mod format_like;

use base_db::SourceDatabase;
use hir::{ItemInNs, Semantics};
use ide_db::{
    RootDatabase, SnippetCap,
    documentation::{Documentation, HasDocs},
    imports::insert_use::ImportScope,
    source_change::SnippetEdit,
    syntax_helpers::suggest_name::NameGenerator,
    text_edit::TextEdit,
    ty_filter::TryEnum,
};
use itertools::{Either, Itertools};
use stdx::never;
use syntax::{
    SmolStr,
    SyntaxKind::{CLOSURE_EXPR, EXPR_STMT, MATCH_ARM, STMT_LIST},
    T, TextRange, TextSize, ToSmolStr,
    ast::{self, AstNode, AstToken},
    format_smolstr, match_ast,
};

use crate::{
    CompletionItem, CompletionItemKind, CompletionRelevance, Completions, SnippetScope,
    completions::postfix::format_like::add_format_like_completions,
    context::{BreakableKind, CompletionContext, DotAccess, DotAccessKind},
    item::{Builder, CompletionRelevancePostfixMatch},
};

pub(crate) fn complete_postfix(
    acc: &mut Completions,
    ctx: &CompletionContext<'_, '_>,
    dot_access: &DotAccess<'_>,
) {
    if !ctx.config.enable_postfix_completions {
        return;
    }

    let (dot_receiver, receiver_ty, receiver_is_ambiguous_float_literal) = match dot_access {
        DotAccess { receiver_ty: Some(ty), receiver: Some(it), kind, .. } => (
            it,
            &ty.original,
            match *kind {
                DotAccessKind::Field { receiver_is_ambiguous_float_literal } => {
                    receiver_is_ambiguous_float_literal
                }
                DotAccessKind::Method => false,
            },
        ),
        _ => return,
    };
    let expr_ctx = &dot_access.ctx;
    let receiver_accessor = receiver_accessor(dot_receiver);

    let receiver_text =
        get_receiver_text(&ctx.sema, dot_receiver, receiver_is_ambiguous_float_literal);

    let cap = match ctx.config.snippet_cap {
        Some(it) => it,
        None => return,
    };

    let postfix_snippet = match build_postfix_snippet_builder(ctx, cap, dot_receiver) {
        Some(it) => it,
        None => return,
    };
    let semi =
        if expr_ctx.in_block_expr && ctx.token.next_token().is_none_or(|it| it.kind() != T![;]) {
            ";"
        } else {
            ""
        };

    let cfg = ctx.config.find_path_config(ctx.is_nightly);

    if let Some(drop_trait) = ctx.famous_defs().core_ops_Drop()
        && receiver_ty.impls_trait(ctx.db, drop_trait, &[])
        && let Some(drop_fn) = ctx.famous_defs().core_mem_drop()
        && let Some(path) = ctx.module.find_path(ctx.db, ItemInNs::Values(drop_fn.into()), cfg)
    {
        cov_mark::hit!(postfix_drop_completion);
        let mut item = postfix_snippet(
            "drop",
            "fn drop(&mut self)",
            format!("{path}($0{receiver_text})", path = path.display(ctx.db, ctx.edition)),
        );
        item.set_documentation(drop_fn.docs(ctx.db));
        item.add_to(acc, ctx.db);
    }

    postfix_snippet("ref", "&expr", format!("&{receiver_text}")).add_to(acc, ctx.db);
    postfix_snippet("refm", "&mut expr", format!("&mut {receiver_text}")).add_to(acc, ctx.db);
    postfix_snippet("deref", "*expr", format!("*{receiver_text}")).add_to(acc, ctx.db);

    // The rest of the postfix completions create an expression that moves an argument,
    // so it's better to consider references now to avoid breaking the compilation

    let (dot_receiver_including_refs, prefix) = include_references(&receiver_accessor);
    let mut receiver_text = receiver_text;
    receiver_text.insert_str(0, &prefix);
    let postfix_snippet =
        match build_postfix_snippet_builder(ctx, cap, &dot_receiver_including_refs) {
            Some(it) => it,
            None => return,
        };

    if !ctx.config.snippets.is_empty() {
        add_custom_postfix_completions(acc, ctx, &postfix_snippet, &receiver_text);
    }

    postfix_snippet("box", "Box::new(expr)", format!("Box::new({receiver_text})"))
        .add_to(acc, ctx.db);
    postfix_snippet("dbg", "dbg!(expr)", format!("dbg!({receiver_text})")).add_to(acc, ctx.db); // fixme
    postfix_snippet("dbgr", "dbg!(&expr)", format!("dbg!(&{receiver_text})")).add_to(acc, ctx.db);
    postfix_snippet("call", "function(expr)", format!("${{1}}({receiver_text})"))
        .add_to(acc, ctx.db);

    if let Some(expected_ty) = ctx.expected_type.as_ref()
        && let Some(adt) = expected_ty.as_adt()
    {
        let is_valid_new = expected_ty
            .iterate_assoc_items(ctx.db, |item| {
                if let hir::AssocItem::Function(func) = item
                    && func.name(ctx.db) == hir::sym::new
                    && !func.has_self_param(ctx.db)
                {
                    let params = func.params_without_self(ctx.db);
                    if params.len() == 1 {
                        return Some(());
                    }
                }
                None
            })
            .is_some();

        let adt = hir::ModuleDef::from(adt);
        if is_valid_new && let Some(path) = ctx.module.find_path(ctx.db, adt, cfg) {
            let ty_name = path.display(ctx.db, ctx.display_target.edition).to_smolstr();

            postfix_snippet(
                "new",
                &format_smolstr!("{}::new(expr)", ty_name),
                format!("{}::new({}$0)", ty_name, receiver_text),
            )
            .add_to(acc, ctx.db);
        }
    }

    let try_enum = TryEnum::from_ty(&ctx.sema, receiver_ty);
    let is_in_cond = is_in_condition(&dot_receiver_including_refs);
    let is_in_value = is_in_value(&dot_receiver_including_refs);
    if let Some(parent) = dot_receiver_including_refs.syntax().parent() {
        let placeholder = suggest_receiver_name(dot_receiver, "0", &ctx.sema);
        match &try_enum {
            Some(try_enum) if is_in_cond => match try_enum {
                TryEnum::Result => {
                    postfix_snippet(
                        "let",
                        "let Ok(_)",
                        format!("let Ok({placeholder}) = {receiver_text}"),
                    )
                    .add_to(acc, ctx.db);
                    postfix_snippet(
                        "letm",
                        "let Ok(mut _)",
                        format!("let Ok(mut {placeholder}) = {receiver_text}"),
                    )
                    .add_to(acc, ctx.db);
                }
                TryEnum::Option => {
                    postfix_snippet(
                        "let",
                        "let Some(_)",
                        format!("let Some({placeholder}) = {receiver_text}"),
                    )
                    .add_to(acc, ctx.db);
                    postfix_snippet(
                        "letm",
                        "let Some(mut _)",
                        format!("let Some(mut {placeholder}) = {receiver_text}"),
                    )
                    .add_to(acc, ctx.db);
                }
            },
            _ if is_in_cond => {
                postfix_snippet("let", "let", format!("let $1 = {receiver_text}"))
                    .add_to(acc, ctx.db);
            }
            _ if matches!(parent.kind(), STMT_LIST | EXPR_STMT) => {
                postfix_snippet("let", "let", format!("let $0 = {receiver_text}{semi}"))
                    .add_to(acc, ctx.db);
                postfix_snippet("letm", "let mut", format!("let mut $0 = {receiver_text}{semi}"))
                    .add_to(acc, ctx.db);
            }
            _ if matches!(parent.kind(), MATCH_ARM | CLOSURE_EXPR) => {
                postfix_snippet(
                    "let",
                    "let",
                    format!("{{\n    let $1 = {receiver_text};\n    $0\n}}"),
                )
                .add_to(acc, ctx.db);
                postfix_snippet(
                    "letm",
                    "let mut",
                    format!("{{\n    let mut $1 = {receiver_text};\n    $0\n}}"),
                )
                .add_to(acc, ctx.db);
            }
            _ => (),
        }
    }

    if !is_in_cond {
        match try_enum {
            Some(try_enum) => match try_enum {
                TryEnum::Result => {
                    postfix_snippet(
                    "match",
                    "match expr {}",
                    format!("match {receiver_text} {{\n    Ok(${{1:_}}) => {{$2}},\n    Err(${{3:_}}) => {{$0}},\n}}"),
                )
                .add_to(acc, ctx.db);
                }
                TryEnum::Option => {
                    postfix_snippet(
                    "match",
                    "match expr {}",
                    format!(
                        "match {receiver_text} {{\n    Some(${{1:_}}) => {{$2}},\n    None => {{$0}},\n}}"
                    ),
                )
                .add_to(acc, ctx.db);
                }
            },
            None => {
                postfix_snippet(
                    "match",
                    "match expr {}",
                    format!("match {receiver_text} {{\n    ${{1:_}} => {{$0}},\n}}"),
                )
                .add_to(acc, ctx.db);
            }
        }
        if let Some(try_enum) = &try_enum {
            let placeholder = suggest_receiver_name(dot_receiver, "1", &ctx.sema);
            let if_then_snip =
                if is_in_value { "{\n    $2\n} else {\n    $0\n}" } else { "{\n    $0\n}" };
            match try_enum {
                TryEnum::Result => {
                    postfix_snippet(
                        "ifl",
                        "if let Ok {}",
                        format!("if let Ok({placeholder}) = {receiver_text} {if_then_snip}"),
                    )
                    .add_to(acc, ctx.db);

                    postfix_snippet(
                        "lete",
                        "let Ok else {}",
                        format!("let Ok({placeholder}) = {receiver_text} else {{\n    $2\n}};$0"),
                    )
                    .add_to(acc, ctx.db);

                    postfix_snippet(
                        "while",
                        "while let Ok {}",
                        format!("while let Ok({placeholder}) = {receiver_text} {{\n    $0\n}}"),
                    )
                    .add_to(acc, ctx.db);
                }
                TryEnum::Option => {
                    postfix_snippet(
                        "ifl",
                        "if let Some {}",
                        format!("if let Some({placeholder}) = {receiver_text} {if_then_snip}"),
                    )
                    .add_to(acc, ctx.db);

                    postfix_snippet(
                        "lete",
                        "let Some else {}",
                        format!("let Some({placeholder}) = {receiver_text} else {{\n    $2\n}};$0"),
                    )
                    .add_to(acc, ctx.db);

                    postfix_snippet(
                        "while",
                        "while let Some {}",
                        format!("while let Some({placeholder}) = {receiver_text} {{\n    $0\n}}"),
                    )
                    .add_to(acc, ctx.db);
                }
            }
        } else if receiver_ty.is_bool() || receiver_ty.is_unknown() {
            let if_then_snip =
                if is_in_value { "{\n    $1\n} else {\n    $0\n}" } else { "{\n    $0\n}" };
            postfix_snippet("if", "if expr {}", format!("if {receiver_text} {if_then_snip}"))
                .add_to(acc, ctx.db);
            postfix_snippet(
                "while",
                "while expr {}",
                format!("while {receiver_text} {{\n    $0\n}}"),
            )
            .add_to(acc, ctx.db);
        } else if let Some(trait_) = ctx.famous_defs().core_iter_IntoIterator()
            && receiver_ty.impls_trait(ctx.db, trait_, &[])
        {
            postfix_snippet(
                "for",
                "for ele in expr {}",
                format!("for ele in {receiver_text} {{\n    $0\n}}"),
            )
            .add_to(acc, ctx.db);
        }
    }

    if receiver_ty.is_bool() || receiver_ty.is_unknown() {
        postfix_snippet("not", "!expr", format!("!{receiver_text}")).add_to(acc, ctx.db);
    }

    let block_should_be_wrapped = if let ast::Expr::BlockExpr(block) = dot_receiver {
        block.modifier().is_some() || !block.is_standalone()
    } else {
        true
    };
    {
        let (open_brace, close_brace) =
            if block_should_be_wrapped { ("{ ", " }") } else { ("", "") };
        // FIXME: Why add parentheses
        let (open_paren, close_paren) = if is_in_cond { ("(", ")") } else { ("", "") };
        let unsafe_completion_string =
            format!("{open_paren}unsafe {open_brace}{receiver_text}{close_brace}{close_paren}");
        postfix_snippet("unsafe", "unsafe {}", unsafe_completion_string).add_to(acc, ctx.db);

        let const_completion_string =
            format!("{open_paren}const {open_brace}{receiver_text}{close_brace}{close_paren}");
        postfix_snippet("const", "const {}", const_completion_string).add_to(acc, ctx.db);
    }

    if let ast::Expr::Literal(literal) = dot_receiver.clone()
        && let Some(literal_text) = ast::String::cast(literal.token())
    {
        add_format_like_completions(acc, ctx, dot_receiver, cap, &literal_text, semi);
    }

    postfix_snippet("return", "return expr", format!("return {receiver_text}{semi}"))
        .add_to(acc, ctx.db);

    if let Some(BreakableKind::Block | BreakableKind::Loop) = expr_ctx.in_breakable {
        postfix_snippet("break", "break expr", format!("break {receiver_text}{semi}"))
            .add_to(acc, ctx.db);
    }
}

fn suggest_receiver_name(
    receiver: &ast::Expr,
    n: &str,
    sema: &Semantics<'_, RootDatabase>,
) -> SmolStr {
    let placeholder = |name| format_smolstr!("${{{n}:{name}}}");

    match receiver {
        ast::Expr::PathExpr(path) => {
            if let Some(name) = path.path().and_then(|it| it.as_single_name_ref()) {
                return placeholder(name.text());
            }
        }
        ast::Expr::RefExpr(it) => {
            if let Some(receiver) = it.expr() {
                return suggest_receiver_name(&receiver, n, sema);
            }
        }
        _ => {}
    }

    let name = NameGenerator::new_with_names([].into_iter()).try_for_variable(receiver, sema);
    match name {
        Some(name) => placeholder(&name),
        None => format_smolstr!("${n}"),
    }
}

fn get_receiver_text(
    sema: &Semantics<'_, RootDatabase>,
    receiver: &ast::Expr,
    receiver_is_ambiguous_float_literal: bool,
) -> String {
    // Do not just call `receiver.to_string()`, as that will mess up whitespaces inside macros.
    let Some(mut range) = sema.original_range_opt(receiver.syntax()) else {
        return receiver.to_string();
    };
    if receiver_is_ambiguous_float_literal {
        range.range = TextRange::at(range.range.start(), range.range.len() - TextSize::of('.'))
    }
    let file_text = sema.db.file_text(range.file_id.file_id(sema.db));
    let text = file_text.text(sema.db);
    let indent_spaces = indent_of_tail_line(&text[TextRange::up_to(range.range.end())]);
    let mut text = stdx::dedent_by(indent_spaces, &text[range.range]);

    // The receiver texts should be interpreted as-is, as they are expected to be
    // normal Rust expressions.
    SnippetEdit::escape_snippet_bits(&mut text);
    return text;

    fn indent_of_tail_line(text: &str) -> usize {
        let tail_line = text.rsplit_once('\n').map_or(text, |(_, s)| s);
        let trimmed = tail_line.trim_start_matches(' ');
        tail_line.len() - trimmed.len()
    }
}

fn receiver_accessor(receiver: &ast::Expr) -> ast::Expr {
    receiver
        .syntax()
        .parent()
        .and_then(ast::Expr::cast)
        .filter(|it| {
            matches!(
                it,
                ast::Expr::FieldExpr(_) | ast::Expr::MethodCallExpr(_) | ast::Expr::CallExpr(_)
            )
        })
        .unwrap_or_else(|| receiver.clone())
}

/// Given an `initial_element`, tries to expand it to include deref(s), not(s), and then references.
/// Returns the expanded expressions, and the added prefix as a string
///
/// For example, if called with the `42` in `&&mut *42`, would return `(&&mut *42, "&&mut *")`.
fn include_references(initial_element: &ast::Expr) -> (ast::Expr, String) {
    let mut resulting_element = initial_element.clone();
    let mut prefix = String::new();

    while let Some(parent) = resulting_element.syntax().parent().and_then(ast::PrefixExpr::cast)
        && parent.op_kind() == Some(ast::UnaryOp::Deref)
    {
        resulting_element = ast::Expr::from(parent);
        prefix.insert(0, '*');
    }

    while let Some(parent) = resulting_element.syntax().parent().and_then(ast::PrefixExpr::cast)
        && parent.op_kind() == Some(ast::UnaryOp::Not)
    {
        resulting_element = ast::Expr::from(parent);
        prefix.insert(0, '!');
    }

    while let Some(parent_ref_element) =
        resulting_element.syntax().parent().and_then(ast::RefExpr::cast)
    {
        let last_child_or_token = parent_ref_element.syntax().last_child_or_token();
        prefix.insert_str(
            0,
            parent_ref_element
                .syntax()
                .children_with_tokens()
                .filter(|it| Some(it) != last_child_or_token.as_ref())
                .flat_map(|it| {
                    let has_ws = it.next_sibling_or_token().is_some_and(|it| it.kind().is_trivia());
                    let need_ws = !has_ws && it.kind().is_any_identifier();
                    itertools::chain([Either::Left(it)], need_ws.then_some(Either::Right(" ")))
                })
                .format("")
                .to_smolstr()
                .as_str(),
        );
        resulting_element = ast::Expr::from(parent_ref_element);
    }

    (resulting_element, prefix)
}

fn build_postfix_snippet_builder<'ctx>(
    ctx: &'ctx CompletionContext<'_, '_>,
    cap: SnippetCap,
    receiver: &'ctx ast::Expr,
) -> Option<impl Fn(&str, &str, String) -> Builder + 'ctx> {
    let receiver_range = ctx.sema.original_range_opt(receiver.syntax())?.range;
    if ctx.source_range().end() < receiver_range.start() {
        // This shouldn't happen, yet it does. I assume this might be due to an incorrect token
        // mapping.
        never!();
        return None;
    }
    let delete_range = TextRange::new(receiver_range.start(), ctx.source_range().end());

    // Wrapping impl Fn in an option ruins lifetime inference for the parameters in a way that
    // can't be annotated for the closure, hence fix it by constructing it without the Option first
    fn build<'ctx>(
        ctx: &'ctx CompletionContext<'_, '_>,
        cap: SnippetCap,
        delete_range: TextRange,
    ) -> impl Fn(&str, &str, String) -> Builder + 'ctx {
        move |label, detail, snippet| {
            let edit = TextEdit::replace(delete_range, snippet);
            let mut item = CompletionItem::new(
                CompletionItemKind::Snippet,
                ctx.source_range(),
                label,
                ctx.edition,
            );
            item.detail(detail).snippet_edit(cap, edit);
            let postfix_match = if ctx.original_token.text() == label {
                cov_mark::hit!(postfix_exact_match_is_high_priority);
                Some(CompletionRelevancePostfixMatch::Exact)
            } else {
                cov_mark::hit!(postfix_inexact_match_is_low_priority);
                Some(CompletionRelevancePostfixMatch::NonExact)
            };
            let relevance = CompletionRelevance { postfix_match, ..Default::default() };
            item.set_relevance(relevance);
            item
        }
    }
    Some(build(ctx, cap, delete_range))
}

fn add_custom_postfix_completions(
    acc: &mut Completions,
    ctx: &CompletionContext<'_, '_>,
    postfix_snippet: impl Fn(&str, &str, String) -> Builder,
    receiver_text: &str,
) -> Option<()> {
    ImportScope::find_insert_use_container(&ctx.token.parent()?, &ctx.sema)?;
    ctx.config.postfix_snippets().filter(|(_, snip)| snip.scope == SnippetScope::Expr).for_each(
        |(trigger, snippet)| {
            let imports = match snippet.imports(ctx) {
                Some(imports) => imports,
                None => return,
            };
            let body = snippet.postfix_snippet(receiver_text);
            let document = Documentation::new_owned(format!("```rust\n{body}\n```"));
            let mut builder =
                postfix_snippet(trigger, snippet.description.as_deref().unwrap_or_default(), body);
            builder.documentation(document);
            for import in imports.into_iter() {
                builder.add_import(import);
            }
            builder.add_to(acc, ctx.db);
        },
    );
    None
}

pub(crate) fn is_in_condition(it: &ast::Expr) -> bool {
    it.syntax()
        .parent()
        .and_then(|parent| {
            Some(match_ast! { match parent {
                ast::IfExpr(expr) => expr.condition()? == *it,
                ast::WhileExpr(expr) => expr.condition()? == *it,
                ast::MatchGuard(guard) => guard.condition()? == *it,
                ast::BinExpr(bin_expr) => (bin_expr.op_token()?.kind() == T![&&])
                    .then(|| is_in_condition(&bin_expr.into()))?,
                ast::Expr(expr) => (expr.syntax().text_range().start() == it.syntax().text_range().start())
                    .then(|| is_in_condition(&expr))?,
                _ => return None,
            } })
        })
        .unwrap_or(false)
}

pub(crate) fn is_in_value(it: &ast::Expr) -> bool {
    let Some(node) = it.syntax().parent() else { return false };
    let kind = node.kind();
    ast::LetStmt::can_cast(kind)
        || ast::ArgList::can_cast(kind)
        || ast::ArrayExpr::can_cast(kind)
        || ast::ParenExpr::can_cast(kind)
        || ast::BreakExpr::can_cast(kind)
        || ast::ReturnExpr::can_cast(kind)
        || ast::PrefixExpr::can_cast(kind)
        || ast::FormatArgsArg::can_cast(kind)
        || ast::RecordExprField::can_cast(kind)
        || ast::BinExpr::cast(node.clone()).is_some_and(|expr| expr.rhs().as_ref() == Some(it))
        || ast::IndexExpr::cast(node).is_some_and(|expr| expr.index().as_ref() == Some(it))
}

#[cfg(test)]
mod tests {
    use expect_test::expect;

    use crate::{
        CompletionConfig, Snippet,
        tests::{TEST_CONFIG, check, check_edit, check_edit_with_config},
    };

    #[test]
    fn postfix_completion_works_for_trivial_path_expression() {
        check(
            r#"
fn main() {
    let bar = true;
    bar.$0
}
"#,
            expect![[r#"
                sn box  Box::new(expr)
                sn call function(expr)
                sn const      const {}
                sn dbg      dbg!(expr)
                sn dbgr    dbg!(&expr)
                sn deref         *expr
                sn if       if expr {}
                sn let             let
                sn letm        let mut
                sn match match expr {}
                sn not           !expr
                sn ref           &expr
                sn refm      &mut expr
                sn return  return expr
                sn unsafe    unsafe {}
                sn while while expr {}
            "#]],
        );
    }

    #[test]
    fn postfix_completion_works_for_function_calln() {
        check(
            r#"
fn foo(elt: bool) -> bool {
    !elt
}

fn main() {
    let bar = true;
    foo(bar.$0)
}
"#,
            expect![[r#"
                sn box  Box::new(expr)
                sn call function(expr)
                sn const      const {}
                sn dbg      dbg!(expr)
                sn dbgr    dbg!(&expr)
                sn deref         *expr
                sn if       if expr {}
                sn match match expr {}
                sn not           !expr
                sn ref           &expr
                sn refm      &mut expr
                sn return  return expr
                sn unsafe    unsafe {}
                sn while while expr {}
            "#]],
        );
    }

    #[test]
    fn postfix_completion_works_in_if_condition() {
        check(
            r#"
fn foo(cond: bool) {
    if cond.$0
}
"#,
            expect![[r#"
                sn box  Box::new(expr)
                sn call function(expr)
                sn const      const {}
                sn dbg      dbg!(expr)
                sn dbgr    dbg!(&expr)
                sn deref         *expr
                sn let             let
                sn not           !expr
                sn ref           &expr
                sn refm      &mut expr
                sn return  return expr
                sn unsafe    unsafe {}
            "#]],
        );
    }

    #[test]
    fn postfix_type_filtering() {
        check(
            r#"
fn main() {
    let bar: u8 = 12;
    bar.$0
}
"#,
            expect![[r#"
                sn box  Box::new(expr)
                sn call function(expr)
                sn const      const {}
                sn dbg      dbg!(expr)
                sn dbgr    dbg!(&expr)
                sn deref         *expr
                sn let             let
                sn letm        let mut
                sn match match expr {}
                sn ref           &expr
                sn refm      &mut expr
                sn return  return expr
                sn unsafe    unsafe {}
            "#]],
        )
    }

    #[test]
    fn let_middle_block() {
        check_edit(
            "let",
            r#"
fn main() {
    baz.l$0
    res
}
"#,
            r#"
fn main() {
    let $0 = baz;
    res
}
"#,
        );

        check(
            r#"
fn main() {
    baz.l$0
    res
}
"#,
            expect![[r#"
                sn box  Box::new(expr)
                sn call function(expr)
                sn const      const {}
                sn dbg      dbg!(expr)
                sn dbgr    dbg!(&expr)
                sn deref         *expr
                sn if       if expr {}
                sn let             let
                sn letm        let mut
                sn match match expr {}
                sn not           !expr
                sn ref           &expr
                sn refm      &mut expr
                sn return  return expr
                sn unsafe    unsafe {}
                sn while while expr {}
            "#]],
        );
        check(
            r#"
fn main() {
    &baz.l$0
    res
}
"#,
            expect![[r#"
                sn box  Box::new(expr)
                sn call function(expr)
                sn const      const {}
                sn dbg      dbg!(expr)
                sn dbgr    dbg!(&expr)
                sn deref         *expr
                sn if       if expr {}
                sn let             let
                sn letm        let mut
                sn match match expr {}
                sn not           !expr
                sn ref           &expr
                sn refm      &mut expr
                sn return  return expr
                sn unsafe    unsafe {}
                sn while while expr {}
            "#]],
        );
    }

    #[test]
    fn let_tail_block() {
        check_edit(
            "let",
            r#"
fn main() {
    baz.l$0
}
"#,
            r#"
fn main() {
    let $0 = baz;
}
"#,
        );

        check(
            r#"
fn main() {
    baz.l$0
}
"#,
            expect![[r#"
                sn box  Box::new(expr)
                sn call function(expr)
                sn const      const {}
                sn dbg      dbg!(expr)
                sn dbgr    dbg!(&expr)
                sn deref         *expr
                sn if       if expr {}
                sn let             let
                sn letm        let mut
                sn match match expr {}
                sn not           !expr
                sn ref           &expr
                sn refm      &mut expr
                sn return  return expr
                sn unsafe    unsafe {}
                sn while while expr {}
            "#]],
        );

        check(
            r#"
fn main() {
    &baz.l$0
}
"#,
            expect![[r#"
                sn box  Box::new(expr)
                sn call function(expr)
                sn const      const {}
                sn dbg      dbg!(expr)
                sn dbgr    dbg!(&expr)
                sn deref         *expr
                sn if       if expr {}
                sn let             let
                sn letm        let mut
                sn match match expr {}
                sn not           !expr
                sn ref           &expr
                sn refm      &mut expr
                sn return  return expr
                sn unsafe    unsafe {}
                sn while while expr {}
            "#]],
        );
    }

    #[test]
    fn let_before_semicolon() {
        check_edit(
            "let",
            r#"
fn main() {
    baz.l$0;
}
"#,
            r#"
fn main() {
    let $0 = baz;
}
"#,
        );
    }

    #[test]
    fn option_iflet() {
        check_edit(
            "ifl",
            r#"
//- minicore: option
fn main() {
    let bar = Some(true);
    bar.$0
}
"#,
            r#"
fn main() {
    let bar = Some(true);
    if let Some(${1:bar}) = bar {
    $0
}
}
"#,
        );
    }

    #[test]
    fn option_iflet_cond() {
        check(
            r#"
//- minicore: option
fn main() {
    let bar = Some(true);
    if bar.$0
}
"#,
            expect![[r#"
                me and(…)    fn(self, Option<U>) -> Option<U>
                me as_ref()     const fn(&self) -> Option<&T>
                me ok_or(…) const fn(self, E) -> Result<T, E>
                me unwrap()               const fn(self) -> T
                me unwrap_or(…)              fn(self, T) -> T
                sn box                         Box::new(expr)
                sn call                        function(expr)
                sn const                             const {}
                sn dbg                             dbg!(expr)
                sn dbgr                           dbg!(&expr)
                sn deref                                *expr
                sn let                            let Some(_)
                sn letm                       let Some(mut _)
                sn ref                                  &expr
                sn refm                             &mut expr
                sn return                         return expr
                sn unsafe                           unsafe {}
            "#]],
        );
        check_edit(
            "let",
            r#"
//- minicore: option
fn main() {
    let bar = Some(true);
    if bar.$0
}
"#,
            r#"
fn main() {
    let bar = Some(true);
    if let Some(${0:bar}) = bar
}
"#,
        );
        check_edit(
            "let",
            r#"
//- minicore: option
fn main() {
    let bar = Some(true);
    if true && bar.$0
}
"#,
            r#"
fn main() {
    let bar = Some(true);
    if true && let Some(${0:bar}) = bar
}
"#,
        );
        check_edit(
            "let",
            r#"
//- minicore: option
fn main() {
    let bar = Some(true);
    if true && true && bar.$0
}
"#,
            r#"
fn main() {
    let bar = Some(true);
    if true && true && let Some(${0:bar}) = bar
}
"#,
        );
    }

    #[test]
    fn iflet_fallback_cond() {
        check_edit(
            "let",
            r#"
fn main() {
    let bar = 2;
    if bar.$0
}
"#,
            r#"
fn main() {
    let bar = 2;
    if let $1 = bar
}
"#,
        );
    }

    #[test]
    fn match_arm_let_block() {
        check(
            r#"
fn main() {
    match 2 {
        bar => bar.$0
    }
}
"#,
            expect![[r#"
                sn box  Box::new(expr)
                sn call function(expr)
                sn const      const {}
                sn dbg      dbg!(expr)
                sn dbgr    dbg!(&expr)
                sn deref         *expr
                sn let             let
                sn letm        let mut
                sn match match expr {}
                sn ref           &expr
                sn refm      &mut expr
                sn return  return expr
                sn unsafe    unsafe {}
            "#]],
        );
        check(
            r#"
fn main() {
    match 2 {
        bar => &bar.l$0
    }
}
"#,
            expect![[r#"
                sn box  Box::new(expr)
                sn call function(expr)
                sn const      const {}
                sn dbg      dbg!(expr)
                sn dbgr    dbg!(&expr)
                sn deref         *expr
                sn let             let
                sn letm        let mut
                sn match match expr {}
                sn ref           &expr
                sn refm      &mut expr
                sn return  return expr
                sn unsafe    unsafe {}
            "#]],
        );
        check_edit(
            "let",
            r#"
fn main() {
    match 2 {
        bar => bar.$0
    }
}
"#,
            r#"
fn main() {
    match 2 {
        bar => {
    let $1 = bar;
    $0
}
    }
}
"#,
        );
    }

    #[test]
    fn closure_let_block() {
        check_edit(
            "let",
            r#"
fn main() {
    let bar = 2;
    let f = || bar.$0;
}
"#,
            r#"
fn main() {
    let bar = 2;
    let f = || {
    let $1 = bar;
    $0
};
}
"#,
        );
    }

    #[test]
    fn option_letelse() {
        check_edit(
            "lete",
            r#"
//- minicore: option
fn main() {
    let bar = Some(true);
    bar.$0
}
"#,
            r#"
fn main() {
    let bar = Some(true);
    let Some(${1:bar}) = bar else {
    $2
};$0
}
"#,
        );

        check_edit(
            "lete",
            r#"
//- minicore: option
fn main() {
    let bar = Some(true);
    bar.$0
    other();
}
"#,
            r#"
fn main() {
    let bar = Some(true);
    let Some(${1:bar}) = bar else {
    $2
};$0
    other();
}
"#,
        );
    }

    #[test]
    fn result_match() {
        check_edit(
            "match",
            r#"
//- minicore: result
fn main() {
    let bar = Ok(true);
    bar.$0
}
"#,
            r#"
fn main() {
    let bar = Ok(true);
    match bar {
    Ok(${1:_}) => {$2},
    Err(${3:_}) => {$0},
}
}
"#,
        );
    }

    #[test]
    fn postfix_completion_works_for_ambiguous_float_literal() {
        check_edit("refm", r#"fn main() { 42.$0 }"#, r#"fn main() { &mut 42 }"#)
    }

    #[test]
    fn works_in_simple_macro() {
        check_edit(
            "dbg",
            r#"
macro_rules! m { ($e:expr) => { $e } }
fn main() {
    let bar: u8 = 12;
    m!(bar.d$0)
}
"#,
            r#"
macro_rules! m { ($e:expr) => { $e } }
fn main() {
    let bar: u8 = 12;
    m!(dbg!(bar))
}
"#,
        );
    }

    #[test]
    fn postfix_completion_for_references() {
        check_edit("dbg", r#"fn main() { &&42.$0 }"#, r#"fn main() { dbg!(&&42) }"#);
        check_edit("dbg", r#"fn main() { &&*"hello".$0 }"#, r#"fn main() { dbg!(&&*"hello") }"#);
        check_edit("refm", r#"fn main() { &&42.$0 }"#, r#"fn main() { &&&mut 42 }"#);
        check_edit(
            "ifl",
            r#"
//- minicore: option
fn main() {
    let bar = &Some(true);
    bar.$0
}
"#,
            r#"
fn main() {
    let bar = &Some(true);
    if let Some(${1:bar}) = bar {
    $0
}
}
"#,
        )
    }

    #[test]
    fn postfix_completion_for_nots() {
        check_edit(
            "if",
            r#"
fn main() {
    let is_foo = true;
    !is_foo.$0
}
"#,
            r#"
fn main() {
    let is_foo = true;
    if !is_foo {
    $0
}
}
"#,
        )
    }

    #[test]
    fn postfix_completion_if_else_in_value() {
        check_edit(
            "if",
            r#"
fn main() {
    let s = cond.is_some().$0;
}
"#,
            r#"
fn main() {
    let s = if cond.is_some() {
    $1
} else {
    $0
};
}
"#,
        );

        check_edit(
            "ifl",
            r#"
//- minicore: option
fn main() {
    let cond = Some("x");
    let s = cond.$0;
}
"#,
            r#"
fn main() {
    let cond = Some("x");
    let s = if let Some(${1:cond}) = cond {
    $2
} else {
    $0
};
}
"#,
        );

        check_edit(
            "if",
            r#"
fn main() {
    2 + true.$0;
}
"#,
            r#"
fn main() {
    2 + if true {
    $1
} else {
    $0
};
}
"#,
        );
    }

    #[test]
    fn postfix_completion_for_unsafe() {
        postfix_completion_for_block("unsafe");
    }

    #[test]
    fn postfix_completion_for_const() {
        postfix_completion_for_block("const");
    }

    fn postfix_completion_for_block(kind: &str) {
        check_edit(kind, r#"fn main() { foo.$0 }"#, &format!("fn main() {{ {kind} {{ foo }} }}"));
        check_edit(
            kind,
            r#"fn main() { { foo }.$0 }"#,
            &format!("fn main() {{ {kind} {{ foo }} }}"),
        );
        check_edit(
            kind,
            r#"fn main() { if x { foo }.$0 }"#,
            &format!("fn main() {{ {kind} {{ if x {{ foo }} }} }}"),
        );
        check_edit(
            kind,
            r#"fn main() { loop { foo }.$0 }"#,
            &format!("fn main() {{ {kind} {{ loop {{ foo }} }} }}"),
        );
        check_edit(
            kind,
            r#"fn main() { if true {}.$0 }"#,
            &format!("fn main() {{ {kind} {{ if true {{}} }} }}"),
        );
        check_edit(
            kind,
            r#"fn main() { while true {}.$0 }"#,
            &format!("fn main() {{ {kind} {{ while true {{}} }} }}"),
        );
        check_edit(
            kind,
            r#"
//- minicore: iterator
fn main() { for i in 0..10 {}.$0 }"#,
            &format!("fn main() {{ {kind} {{ for i in 0..10 {{}} }} }}"),
        );
        check_edit(
            kind,
            r#"fn main() { let x = if true {1} else {2}.$0 }"#,
            &format!("fn main() {{ let x = {kind} {{ if true {{1}} else {{2}} }} }}"),
        );

        if kind == "const" {
            check_edit(
                kind,
                r#"fn main() { unsafe {1}.$0 }"#,
                &format!("fn main() {{ {kind} {{ unsafe {{1}} }} }}"),
            );
        } else {
            check_edit(
                kind,
                r#"fn main() { const {1}.$0 }"#,
                &format!("fn main() {{ {kind} {{ const {{1}} }} }}"),
            );
        }

        // completion will not be triggered
        check_edit(
            kind,
            r#"fn main() { let x = true else {panic!()}.$0}"#,
            &format!("fn main() {{ let x = true else {{panic!()}}.{kind} $0}}"),
        );
    }

    #[test]
    fn custom_postfix_completion() {
        let config = CompletionConfig {
            snippets: vec![
                Snippet::new(
                    &[],
                    &["break".into()],
                    &["ControlFlow::Break(${receiver})".into()],
                    "",
                    &["core::ops::ControlFlow".into()],
                    crate::SnippetScope::Expr,
                )
                .unwrap(),
            ],
            ..TEST_CONFIG
        };

        check_edit_with_config(
            config.clone(),
            "break",
            r#"
//- minicore: try
fn main() { 42.$0 }
"#,
            r#"
use core::ops::ControlFlow;

fn main() { ControlFlow::Break(42) }
"#,
        );

        // The receiver texts should be escaped, see comments in `get_receiver_text()`
        // for detail.
        //
        // Note that the last argument is what *lsp clients would see* rather than
        // what users would see. Unescaping happens thereafter.
        check_edit_with_config(
            config.clone(),
            "break",
            r#"
//- minicore: try
fn main() { '\\'.$0 }
"#,
            r#"
use core::ops::ControlFlow;

fn main() { ControlFlow::Break('\\\\') }
"#,
        );

        check_edit_with_config(
            config,
            "break",
            r#"
//- minicore: try
fn main() {
    match true {
        true => "${1:placeholder}",
        false => "\$",
    }.$0
}
"#,
            r#"
use core::ops::ControlFlow;

fn main() {
    ControlFlow::Break(match true {
    true => "\${1:placeholder}",
    false => "\\\$",
})
}
"#,
        );
    }

    #[test]
    fn postfix_completion_for_format_like_strings() {
        check_edit(
            "format",
            r#"fn main() { "{some_var:?}".$0 }"#,
            r#"fn main() { format!("{some_var:?}") }"#,
        );
        check_edit(
            "panic",
            r#"fn main() { "Panic with {a}".$0 }"#,
            r#"fn main() { panic!("Panic with {a}"); }"#,
        );
        check_edit(
            "println",
            r#"fn main() { "{ 2+2 } { SomeStruct { val: 1, other: 32 } :?}".$0 }"#,
            r#"fn main() { println!("{} {:?}", 2+2, SomeStruct { val: 1, other: 32 }); }"#,
        );
        check_edit(
            "loge",
            r#"fn main() { "{2+2}".$0 }"#,
            r#"fn main() { log::error!("{}", 2+2); }"#,
        );
        check_edit(
            "logt",
            r#"fn main() { "{2+2}".$0 }"#,
            r#"fn main() { log::trace!("{}", 2+2); }"#,
        );
        check_edit(
            "logd",
            r#"fn main() { "{2+2}".$0 }"#,
            r#"fn main() { log::debug!("{}", 2+2); }"#,
        );
        check_edit(
            "logi",
            r#"fn main() { "{2+2}".$0 }"#,
            r#"fn main() { log::info!("{}", 2+2); }"#,
        );
        check_edit(
            "logw",
            r#"fn main() { "{2+2}".$0 }"#,
            r#"fn main() { log::warn!("{}", 2+2); }"#,
        );
        check_edit(
            "loge",
            r#"fn main() { "{2+2}".$0 }"#,
            r#"fn main() { log::error!("{}", 2+2); }"#,
        );
    }

    #[test]
    fn postfix_custom_snippets_completion_for_references() {
        // https://github.com/rust-lang/rust-analyzer/issues/7929

        let snippet = Snippet::new(
            &[],
            &["ok".into()],
            &["Ok(${receiver})".into()],
            "",
            &[],
            crate::SnippetScope::Expr,
        )
        .unwrap();

        check_edit_with_config(
            CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG },
            "ok",
            r#"fn main() { &&42.o$0 }"#,
            r#"fn main() { Ok(&&42) }"#,
        );

        check_edit_with_config(
            CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG },
            "ok",
            r#"fn main() { &&42.$0 }"#,
            r#"fn main() { Ok(&&42) }"#,
        );

        check_edit_with_config(
            CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG },
            "ok",
            r#"fn main() { &raw mut 42.$0 }"#,
            r#"fn main() { Ok(&raw mut 42) }"#,
        );

        check_edit_with_config(
            CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG },
            "ok",
            r#"fn main() { &raw const 42.$0 }"#,
            r#"fn main() { Ok(&raw const 42) }"#,
        );

        check_edit_with_config(
            CompletionConfig { snippets: vec![snippet], ..TEST_CONFIG },
            "ok",
            r#"
struct A {
    a: i32,
}

fn main() {
    let a = A {a :1};
    &a.a.$0
}
            "#,
            r#"
struct A {
    a: i32,
}

fn main() {
    let a = A {a :1};
    Ok(&a.a)
}
            "#,
        );
    }

    #[test]
    fn postfix_custom_snippets_completion_for_reference_expr() {
        // https://github.com/rust-lang/rust-analyzer/issues/21035
        let snippet = Snippet::new(
            &[],
            &["group".into()],
            &["(${receiver})".into()],
            "",
            &[],
            crate::SnippetScope::Expr,
        )
        .unwrap();

        check_edit_with_config(
            CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG },
            "group",
            r#"fn main() { &[1, 2, 3].g$0 }"#,
            r#"fn main() { (&[1, 2, 3]) }"#,
        );

        check_edit_with_config(
            CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG },
            "group",
            r#"fn main() { &&foo(a, b, 1+1).$0 }"#,
            r#"fn main() { (&&foo(a, b, 1+1)) }"#,
        );

        check_edit_with_config(
            CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG },
            "group",
            r#"fn main() { &mut Foo { a: 1, b: 2, c: 3 }.$0 }"#,
            r#"fn main() { (&mut Foo { a: 1, b: 2, c: 3 }) }"#,
        );

        check_edit_with_config(
            CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG },
            "group",
            r#"fn main() { &raw mut Foo::new().$0 }"#,
            r#"fn main() { (&raw mut Foo::new()) }"#,
        );

        check_edit_with_config(
            CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG },
            "group",
            r#"fn main() { &raw const Foo::bar::SOME_CONST.$0 }"#,
            r#"fn main() { (&raw const Foo::bar::SOME_CONST) }"#,
        );

        check_edit_with_config(
            CompletionConfig { snippets: vec![snippet.clone()], ..TEST_CONFIG },
            "group",
            r#"macro_rules! id { ($($t:tt)*) => ($($t)*); }
fn main() { id!(&raw const Foo::bar::SOME_CONST.$0) }"#,
            r#"macro_rules! id { ($($t:tt)*) => ($($t)*); }
fn main() { id!((&raw const Foo::bar::SOME_CONST)) }"#,
        );
    }

    #[test]
    fn no_postfix_completions_in_if_block_that_has_an_else() {
        check(
            r#"
fn test() {
    if true {}.$0 else {}
}
"#,
            expect![[r#""#]],
        );
    }

    #[test]
    fn mut_ref_consuming() {
        check_edit(
            "call",
            r#"
fn main() {
    let mut x = &mut 2;
    &mut x.$0;
}
"#,
            r#"
fn main() {
    let mut x = &mut 2;
    ${1}(&mut x);
}
"#,
        );
    }

    #[test]
    fn deref_consuming() {
        check_edit(
            "call",
            r#"
fn main() {
    let mut x = &mut 2;
    &mut *x.$0;
}
"#,
            r#"
fn main() {
    let mut x = &mut 2;
    ${1}(&mut *x);
}
"#,
        );
    }

    #[test]
    fn inside_macro() {
        check_edit(
            "box",
            r#"
macro_rules! assert {
    ( $it:expr $(,)? ) => { $it };
}

fn foo() {
    let a = true;
    assert!(if a == false { true } else { false }.$0);
}
        "#,
            r#"
macro_rules! assert {
    ( $it:expr $(,)? ) => { $it };
}

fn foo() {
    let a = true;
    assert!(Box::new(if a == false { true } else { false }));
}
        "#,
        );
    }

    #[test]
    fn snippet_dedent() {
        check_edit(
            "let",
            r#"
//- minicore: option
fn foo(x: Option<i32>, y: Option<i32>) {
    let _f = || {
        x
            .and(y)
            .map(|it| {
                it+2
            })
            .$0
    };
}
"#,
            r#"
fn foo(x: Option<i32>, y: Option<i32>) {
    let _f = || {
        let $0 = x
.and(y)
.map(|it| {
    it+2
});
    };
}
"#,
        );
    }

    #[test]
    fn postfix_new() {
        check_edit(
            "new",
            r#"
struct OtherThing;
struct RefCell<T>(T);
impl<T> RefCell<T> {
    fn new(t: T) -> Self { RefCell(t) }
}

fn main() {
    let other_thing = OtherThing;
    let thing: RefCell<OtherThing> = other_thing.$0;
}
"#,
            r#"
struct OtherThing;
struct RefCell<T>(T);
impl<T> RefCell<T> {
    fn new(t: T) -> Self { RefCell(t) }
}

fn main() {
    let other_thing = OtherThing;
    let thing: RefCell<OtherThing> = RefCell::new(other_thing$0);
}
"#,
        );

        check_edit(
            "new",
            r#"
mod foo {
    pub struct OtherThing;
    pub struct RefCell<T>(T);
    impl<T> RefCell<T> {
        pub fn new(t: T) -> Self { RefCell(t) }
    }
}

fn main() {
    let thing: foo::RefCell<foo::OtherThing> = foo::OtherThing.$0;
}
"#,
            r#"
mod foo {
    pub struct OtherThing;
    pub struct RefCell<T>(T);
    impl<T> RefCell<T> {
        pub fn new(t: T) -> Self { RefCell(t) }
    }
}

fn main() {
    let thing: foo::RefCell<foo::OtherThing> = foo::RefCell::new(foo::OtherThing$0);
}
"#,
        );
    }
}
