use syntax::{
    ast::{self, edit_in_place::Removable, make, HasVisibility},
    ted::{self, Position},
    AstNode, SyntaxKind,
};

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

// Assist: unmerge_use
//
// Extracts single use item from use list.
//
// ```
// use std::fmt::{Debug, Display$0};
// ```
// ->
// ```
// use std::fmt::{Debug};
// use std::fmt::Display;
// ```
pub(crate) fn unmerge_use(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
    let tree: ast::UseTree = ctx.find_node_at_offset::<ast::UseTree>()?.clone_for_update();

    let tree_list = tree.syntax().parent().and_then(ast::UseTreeList::cast)?;
    if tree_list.use_trees().count() < 2 {
        cov_mark::hit!(skip_single_use_item);
        return None;
    }

    let use_: ast::Use = tree_list.syntax().ancestors().find_map(ast::Use::cast)?;
    let path = resolve_full_path(&tree)?;

    let old_parent_range = use_.syntax().parent()?.text_range();
    let new_parent = use_.syntax().parent()?;

    // If possible, explain what is going to be done.
    let label = match tree.path().and_then(|path| path.first_segment()) {
        Some(name) => format!("Unmerge use of `{name}`"),
        None => "Unmerge use".into(),
    };

    let target = tree.syntax().text_range();
    acc.add(AssistId("unmerge_use", AssistKind::RefactorRewrite), label, target, |builder| {
        let new_use = make::use_(
            use_.visibility(),
            make::use_tree(path, tree.use_tree_list(), tree.rename(), tree.star_token().is_some()),
        )
        .clone_for_update();

        tree.remove();
        ted::insert(Position::after(use_.syntax()), new_use.syntax());

        builder.replace(old_parent_range, new_parent.to_string());
    })
}

fn resolve_full_path(tree: &ast::UseTree) -> Option<ast::Path> {
    let paths = tree
        .syntax()
        .ancestors()
        .take_while(|n| n.kind() != SyntaxKind::USE)
        .filter_map(ast::UseTree::cast)
        .filter_map(|t| t.path());

    let final_path = paths.reduce(|prev, next| make::path_concat(next, prev))?;
    if final_path.segment().map_or(false, |it| it.self_token().is_some()) {
        final_path.qualifier()
    } else {
        Some(final_path)
    }
}

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

    use super::*;

    #[test]
    fn skip_single_use_item() {
        cov_mark::check!(skip_single_use_item);
        check_assist_not_applicable(
            unmerge_use,
            r"
use std::fmt::Debug$0;
",
        );
        check_assist_not_applicable(
            unmerge_use,
            r"
use std::fmt::{Debug$0};
",
        );
        check_assist_not_applicable(
            unmerge_use,
            r"
use std::fmt::Debug as Dbg$0;
",
        );
    }

    #[test]
    fn skip_single_glob_import() {
        check_assist_not_applicable(
            unmerge_use,
            r"
use std::fmt::*$0;
",
        );
    }

    #[test]
    fn unmerge_use_item() {
        check_assist(
            unmerge_use,
            r"
use std::fmt::{Debug, Display$0};
",
            r"
use std::fmt::{Debug};
use std::fmt::Display;
",
        );

        check_assist(
            unmerge_use,
            r"
use std::fmt::{Debug, format$0, Display};
",
            r"
use std::fmt::{Debug, Display};
use std::fmt::format;
",
        );
    }

    #[test]
    fn unmerge_glob_import() {
        check_assist(
            unmerge_use,
            r"
use std::fmt::{*$0, Display};
",
            r"
use std::fmt::{Display};
use std::fmt::*;
",
        );
    }

    #[test]
    fn unmerge_renamed_use_item() {
        check_assist(
            unmerge_use,
            r"
use std::fmt::{Debug, Display as Disp$0};
",
            r"
use std::fmt::{Debug};
use std::fmt::Display as Disp;
",
        );
    }

    #[test]
    fn unmerge_indented_use_item() {
        check_assist(
            unmerge_use,
            r"
mod format {
    use std::fmt::{Debug, Display$0 as Disp, format};
}
",
            r"
mod format {
    use std::fmt::{Debug, format};
    use std::fmt::Display as Disp;
}
",
        );
    }

    #[test]
    fn unmerge_nested_use_item() {
        check_assist(
            unmerge_use,
            r"
use foo::bar::{baz::{qux$0, foobar}, barbaz};
",
            r"
use foo::bar::{baz::{foobar}, barbaz};
use foo::bar::baz::qux;
",
        );
        check_assist(
            unmerge_use,
            r"
use foo::bar::{baz$0::{qux, foobar}, barbaz};
",
            r"
use foo::bar::{barbaz};
use foo::bar::baz::{qux, foobar};
",
        );
    }

    #[test]
    fn unmerge_use_item_with_visibility() {
        check_assist(
            unmerge_use,
            r"
pub use std::fmt::{Debug, Display$0};
",
            r"
pub use std::fmt::{Debug};
pub use std::fmt::Display;
",
        );
    }

    #[test]
    fn unmerge_use_item_on_self() {
        check_assist(
            unmerge_use,
            r"use std::process::{Command, self$0};",
            r"use std::process::{Command};
use std::process;",
        );
    }
}
