use syntax::{
    ast::{self, edit_in_place::AttrsOwnerEdit, make, AstNode, HasAttrs},
    T,
};

use crate::{AssistContext, AssistId, AssistKind, Assists};

// Assist: generate_derive
//
// Adds a new `#[derive()]` clause to a struct or enum.
//
// ```
// struct Point {
//     x: u32,
//     y: u32,$0
// }
// ```
// ->
// ```
// #[derive($0)]
// struct Point {
//     x: u32,
//     y: u32,
// }
// ```
pub(crate) fn generate_derive(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
    let cap = ctx.config.snippet_cap?;
    let nominal = ctx.find_node_at_offset::<ast::Adt>()?;
    let target = nominal.syntax().text_range();
    let derive_attr = nominal
        .attrs()
        .filter_map(|x| x.as_simple_call())
        .filter(|(name, _arg)| name == "derive")
        .map(|(_name, arg)| arg)
        .next();

    let delimiter = match &derive_attr {
        None => None,
        Some(tt) => Some(tt.right_delimiter_token()?),
    };

    acc.add(AssistId("generate_derive", AssistKind::Generate), "Add `#[derive]`", target, |edit| {
        match derive_attr {
            None => {
                let derive = make::attr_outer(make::meta_token_tree(
                    make::ext::ident_path("derive"),
                    make::token_tree(T!['('], vec![]).clone_for_update(),
                ))
                .clone_for_update();

                let nominal = edit.make_mut(nominal);
                nominal.add_attr(derive.clone());

                let delimiter = derive
                    .meta()
                    .expect("make::attr_outer was expected to have Meta")
                    .token_tree()
                    .expect("failed to get token tree out of Meta")
                    .r_paren_token()
                    .expect("make::attr_outer was expected to have a R_PAREN");

                edit.add_tabstop_before_token(cap, delimiter);
            }
            Some(_) => {
                // Just move the cursor.
                edit.add_tabstop_before_token(
                    cap,
                    delimiter.expect("Right delim token could not be found."),
                );
            }
        };
    })
}

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

    use super::*;

    #[test]
    fn add_derive_new() {
        check_assist(
            generate_derive,
            "struct Foo { a: i32, $0}",
            "#[derive($0)]\nstruct Foo { a: i32, }",
        );
        check_assist(
            generate_derive,
            "struct Foo { $0 a: i32, }",
            "#[derive($0)]\nstruct Foo {  a: i32, }",
        );
        check_assist(
            generate_derive,
            "
mod m {
    struct Foo { a: i32,$0 }
}
            ",
            "
mod m {
    #[derive($0)]
    struct Foo { a: i32, }
}
            ",
        );
    }

    #[test]
    fn add_derive_existing() {
        check_assist(
            generate_derive,
            "#[derive(Clone)]\nstruct Foo { a: i32$0, }",
            "#[derive(Clone$0)]\nstruct Foo { a: i32, }",
        );
    }

    #[test]
    fn add_derive_existing_with_brackets() {
        check_assist(
            generate_derive,
            "
#[derive[Clone]]
struct Foo { a: i32$0, }
",
            "
#[derive[Clone$0]]
struct Foo { a: i32, }
",
        );
    }

    #[test]
    fn add_derive_existing_missing_delimiter() {
        // since `#[derive]` isn't a simple attr call (i.e. `#[derive()]`)
        // we don't consider it as a proper derive attr and generate a new
        // one instead
        check_assist(
            generate_derive,
            "
#[derive]
struct Foo { a: i32$0, }",
            "
#[derive]
#[derive($0)]
struct Foo { a: i32, }",
        );
    }

    #[test]
    fn add_derive_new_with_doc_comment() {
        check_assist(
            generate_derive,
            "
/// `Foo` is a pretty important struct.
/// It does stuff.
struct Foo { a: i32$0, }
            ",
            "
/// `Foo` is a pretty important struct.
/// It does stuff.
#[derive($0)]
struct Foo { a: i32, }
            ",
        );
        check_assist(
            generate_derive,
            "
mod m {
    /// `Foo` is a pretty important struct.
    /// It does stuff.
    struct Foo { a: i32,$0 }
}
            ",
            "
mod m {
    /// `Foo` is a pretty important struct.
    /// It does stuff.
    #[derive($0)]
    struct Foo { a: i32, }
}
            ",
        );
    }

    #[test]
    fn add_derive_target() {
        check_assist_target(
            generate_derive,
            "
struct SomeThingIrrelevant;
/// `Foo` is a pretty important struct.
/// It does stuff.
struct Foo { a: i32$0, }
struct EvenMoreIrrelevant;
            ",
            "/// `Foo` is a pretty important struct.
/// It does stuff.
struct Foo { a: i32, }",
        );
    }
}
