use ide_db::{
    assists::AssistId,
    defs::{Definition, NameClass, NameRefClass},
    rename::RenameDefinition,
};
use syntax::{AstNode, ast};

use crate::{AssistContext, Assists};

// Assist: remove_underscore_from_used_variables
//
// Removes underscore from used variables.
//
// ```
// fn main() {
//     let mut _$0foo = 1;
//     _foo = 2;
// }
// ```
// ->
// ```
// fn main() {
//     let mut foo = 1;
//     foo = 2;
// }
// ```
pub(crate) fn remove_underscore(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> {
    let (text, text_range, def) = if let Some(name_ref) = ctx.find_node_at_offset::<ast::Name>() {
        let text = name_ref.text();
        if !text.starts_with('_') {
            return None;
        }

        let def = match NameClass::classify(&ctx.sema, &name_ref)? {
            NameClass::Definition(def @ Definition::Local(_)) => def,
            NameClass::PatFieldShorthand { local_def, .. } => Definition::Local(local_def),
            _ => return None,
        };
        (text.to_owned(), name_ref.syntax().text_range(), def)
    } else {
        let name_ref = ctx.find_node_at_offset::<ast::NameRef>()?;
        let text = name_ref.text();
        if !text.starts_with('_') {
            return None;
        }
        let def = match NameRefClass::classify(&ctx.sema, &name_ref)? {
            NameRefClass::Definition(def @ Definition::Local(_), _) => def,
            NameRefClass::FieldShorthand { local_ref, .. } => Definition::Local(local_ref),
            _ => return None,
        };
        (text.to_owned(), name_ref.syntax().text_range(), def)
    };

    if !def.usages(&ctx.sema).at_least_one() {
        return None;
    }

    let new_name = text.trim_start_matches('_');
    if new_name.is_empty() {
        return None;
    }
    let changes =
        def.rename(&ctx.sema, new_name, RenameDefinition::Yes, &ctx.config.rename_config()).ok()?;

    acc.add(
        AssistId::refactor("remove_underscore_from_used_variables"),
        "Remove underscore from a used variable",
        text_range,
        |builder| {
            builder.source_change = changes;
        },
    )
}

#[cfg(test)]
mod tests {
    use crate::tests::{check_assist, check_assist_not_applicable};

    use super::*;

    #[test]
    fn remove_underscore_from_used_variable() {
        check_assist(
            remove_underscore,
            r#"
fn main() {
    let mut _$0foo = 1;
    _foo = 2;
}
"#,
            r#"
fn main() {
    let mut foo = 1;
    foo = 2;
}
"#,
        );
    }

    #[test]
    fn not_applicable_for_unused() {
        check_assist_not_applicable(
            remove_underscore,
            r#"
fn main() {
    let _$0unused = 1;
}
"#,
        );
    }

    #[test]
    fn not_applicable_for_no_underscore() {
        check_assist_not_applicable(
            remove_underscore,
            r#"
fn main() {
    let f$0oo = 1;
    foo = 2;
}
"#,
        );
    }

    #[test]
    fn remove_multiple_underscores() {
        check_assist(
            remove_underscore,
            r#"
fn main() {
    let mut _$0_foo = 1;
    __foo = 2;
}
"#,
            r#"
fn main() {
    let mut foo = 1;
    foo = 2;
}
"#,
        );
    }

    #[test]
    fn remove_underscore_on_usage() {
        check_assist(
            remove_underscore,
            r#"
fn main() {
    let mut _foo = 1;
    _$0foo = 2;
}
"#,
            r#"
fn main() {
    let mut foo = 1;
    foo = 2;
}
"#,
        );
    }

    #[test]
    fn remove_underscore_in_function_parameter_usage() {
        check_assist(
            remove_underscore,
            r#"
fn foo(_foo: i32) {
    let bar = _$0foo + 1;
}
"#,
            r#"
fn foo(foo: i32) {
    let bar = foo + 1;
}
"#,
        )
    }

    #[test]
    fn not_applicable_when_result_is_keyword() {
        check_assist_not_applicable(
            remove_underscore,
            r#"
fn main() {
    let _$0crate = 1;
    _crate;
}
"#,
        );
    }

    #[test]
    fn not_applicable_when_result_is_empty() {
        check_assist_not_applicable(
            remove_underscore,
            r#"
fn main() {
    let _$0_ = 1;
    __;
}
"#,
        );
    }

    #[test]
    fn remove_underscore_in_function_parameter() {
        check_assist(
            remove_underscore,
            r#"
fn foo(_$0foo: i32) {
    let bar = _foo + 1;
}
"#,
            r#"
fn foo(foo: i32) {
    let bar = foo + 1;
}
"#,
        )
    }
}
