use std::path::{Path, PathBuf};
use std::sync::Arc;

use anyhow::Context as _;
use collections::HashSet;
use fuzzy::StringMatchCandidate;
use git::repository::Worktree as GitWorktree;
use gpui::{
    Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
    InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, PromptLevel,
    Render, SharedString, Styled, Subscription, Task, TaskExt, WeakEntity, Window, actions,
};
use picker::{Picker, PickerDelegate, PickerEditorPosition};
use project::Project;
use project::git_store::RepositoryEvent;
use ui::{
    Button, CommonAnimationExt as _, Divider, HighlightedLabel, IconButton, KeyBinding, ListItem,
    ListItemSpacing, ListSubHeader, Tooltip, prelude::*,
};
use util::ResultExt as _;
use util::paths::PathExt;
use workspace::{
    ModalView, MultiWorkspace, RemovalIntent, Workspace, dock::DockPosition,
    notifications::DetachAndPromptErr,
};

use crate::notifications::show_error_toast;
use crate::worktree_service::{RemoteBranchName, WorktreeCreateTarget, worktree_create_targets};
use zed_actions::{
    CreateWorktree, NewWorktreeBranchTarget, OpenWorktreeInNewWindow, OpenWorktreeSetupTasks,
    SwitchWorktree,
};

actions!(
    worktree_picker,
    [
        /// Deletes the selected git worktree.
        DeleteWorktree,
        /// Force deletes the selected git worktree.
        ForceDeleteWorktree
    ]
);

pub struct WorktreePicker {
    picker: Entity<Picker<WorktreePickerDelegate>>,
    focus_handle: FocusHandle,
    _subscriptions: Vec<Subscription>,
}

impl WorktreePicker {
    pub fn new(
        project: Entity<Project>,
        workspace: WeakEntity<Workspace>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Self {
        let focused_dock = workspace
            .upgrade()
            .and_then(|workspace| workspace.read(cx).focused_dock_position(window, cx));
        Self::new_inner(project, workspace, focused_dock, false, window, cx)
    }

    pub fn new_modal(
        project: Entity<Project>,
        workspace: WeakEntity<Workspace>,
        focused_dock: Option<DockPosition>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Self {
        Self::new_inner(project, workspace, focused_dock, true, window, cx)
    }

    fn new_inner(
        project: Entity<Project>,
        workspace: WeakEntity<Workspace>,
        focused_dock: Option<DockPosition>,
        show_footer: bool,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Self {
        let project_ref = project.read(cx);

        let active_worktree_paths: HashSet<PathBuf> = project_ref
            .visible_worktrees(cx)
            .map(|wt| wt.read(cx).abs_path().to_path_buf())
            .collect();

        let project_worktree_paths = active_worktree_paths.clone();

        let has_multiple_repositories = project_ref.repositories(cx).len() > 1;
        let repository = project_ref.active_repository(cx);

        let current_branch_name = repository.as_ref().and_then(|repo| {
            repo.read(cx)
                .branch
                .as_ref()
                .map(|branch| branch.name().to_string())
        });

        let all_worktrees_request = repository
            .clone()
            .map(|repository| repository.update(cx, |repository, _| repository.worktrees()));

        let default_branch_request = repository.clone().map(|repository| {
            repository.update(cx, |repository, _| repository.default_branch(true))
        });

        let initial_matches = vec![WorktreeEntry::CreateFromCurrentBranch];

        let delegate = WorktreePickerDelegate {
            matches: initial_matches,
            all_worktrees: Vec::new(),
            project_worktree_paths,
            selected_index: 0,
            project,
            workspace,
            focused_dock,
            current_branch_name,
            default_branch: None,
            has_multiple_repositories,
            focus_handle: cx.focus_handle(),
            show_footer,
            modifiers: Modifiers::default(),
            active_worktree_paths,
            hovered_delete_index: None,
            deleting_worktree_paths: HashSet::default(),
        };

        let picker = cx.new(|cx| {
            Picker::list(delegate, window, cx)
                .list_measure_all()
                .show_scrollbar(true)
                .embedded()
        });

        let picker_focus_handle = picker.focus_handle(cx);
        picker.update(cx, |picker, _| {
            picker.delegate.focus_handle = picker_focus_handle;
        });

        let mut subscriptions = Vec::new();

        {
            let picker_handle = picker.downgrade();
            cx.spawn_in(window, async move |_this, cx| {
                let all_worktrees: Vec<_> = match all_worktrees_request {
                    Some(req) => match req.await {
                        Ok(Ok(worktrees)) => {
                            worktrees.into_iter().filter(|wt| !wt.is_bare).collect()
                        }
                        Ok(Err(err)) => {
                            log::warn!("WorktreePicker: git worktree list failed: {err}");
                            return anyhow::Ok(());
                        }
                        Err(_) => {
                            log::warn!("WorktreePicker: worktree request was cancelled");
                            return anyhow::Ok(());
                        }
                    },
                    None => Vec::new(),
                };

                let default_branch = match default_branch_request {
                    Some(req) => req.await.ok().and_then(Result::ok).flatten(),
                    None => None,
                };

                picker_handle.update_in(cx, |picker, window, cx| {
                    picker.delegate.all_worktrees = all_worktrees;
                    picker.delegate.default_branch =
                        default_branch.and_then(|branch| RemoteBranchName::parse(&branch));
                    picker.delegate.refresh_project_worktree_paths(window, cx);
                    picker.refresh(window, cx);
                })?;

                anyhow::Ok(())
            })
            .detach_and_log_err(cx);
        }

        if let Some(repo) = &repository {
            let picker_entity = picker.downgrade();
            subscriptions.push(cx.subscribe_in(
                repo,
                window,
                move |_this, repo, event: &RepositoryEvent, window, cx| {
                    if matches!(event, RepositoryEvent::GitWorktreeListChanged) {
                        let worktrees_request = repo.update(cx, |repo, _| repo.worktrees());
                        let picker = picker_entity.clone();
                        cx.spawn_in(window, async move |_, cx| {
                            let all_worktrees: Vec<_> = worktrees_request
                                .await??
                                .into_iter()
                                .filter(|wt| !wt.is_bare)
                                .collect();
                            picker.update_in(cx, |picker, window, cx| {
                                picker.delegate.all_worktrees = all_worktrees;
                                picker.refresh(window, cx);
                            })?;
                            anyhow::Ok(())
                        })
                        .detach_and_log_err(cx);
                    }
                },
            ));
        }

        subscriptions.push(cx.subscribe(&picker, |_, _, _, cx| {
            cx.emit(DismissEvent);
        }));

        Self {
            focus_handle: picker.focus_handle(cx),
            picker,
            _subscriptions: subscriptions,
        }
    }

    fn handle_modifiers_changed(
        &mut self,
        ev: &ModifiersChangedEvent,
        _: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.picker.update(cx, |picker, cx| {
            picker.delegate.modifiers = ev.modifiers;
            cx.notify();
        });
    }
}

impl Focusable for WorktreePicker {
    fn focus_handle(&self, _cx: &App) -> FocusHandle {
        self.focus_handle.clone()
    }
}

impl ModalView for WorktreePicker {}
impl EventEmitter<DismissEvent> for WorktreePicker {}

impl Render for WorktreePicker {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        v_flex()
            .key_context("WorktreePicker")
            .elevation_3(cx)
            .child(self.picker.clone())
            .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
            .on_mouse_down_out(cx.listener(|_, _, _, cx| {
                cx.emit(DismissEvent);
            }))
            .on_action(cx.listener(|_, _: &OpenWorktreeSetupTasks, _, cx| {
                cx.emit(DismissEvent);
                cx.propagate();
            }))
            .on_action(cx.listener(|this, _: &DeleteWorktree, window, cx| {
                this.picker.update(cx, |picker, cx| {
                    let ix = picker.delegate.selected_index;
                    picker.delegate.delete_worktree(ix, false, window, cx);
                });
            }))
            .on_action(cx.listener(|this, _: &ForceDeleteWorktree, window, cx| {
                this.picker.update(cx, |picker, cx| {
                    let ix = picker.delegate.selected_index;
                    picker.delegate.delete_worktree(ix, true, window, cx);
                });
            }))
    }
}

#[derive(Clone)]
enum WorktreeEntry {
    CreateFromCurrentBranch,
    CreateFromDefaultBranch {
        default_branch: RemoteBranchName,
    },
    Separator,
    SectionHeader(SharedString),
    Worktree {
        worktree: GitWorktree,
        positions: Vec<usize>,
    },
    CreateNamed {
        name: String,
        from_branch: Option<RemoteBranchName>,
        disabled_reason: Option<String>,
    },
}

struct WorktreePickerDelegate {
    matches: Vec<WorktreeEntry>,
    all_worktrees: Vec<GitWorktree>,
    project_worktree_paths: HashSet<PathBuf>,
    active_worktree_paths: HashSet<PathBuf>,
    selected_index: usize,
    project: Entity<Project>,
    workspace: WeakEntity<Workspace>,
    focused_dock: Option<DockPosition>,
    current_branch_name: Option<String>,
    default_branch: Option<RemoteBranchName>,
    has_multiple_repositories: bool,
    focus_handle: FocusHandle,
    show_footer: bool,
    modifiers: Modifiers,
    hovered_delete_index: Option<usize>,
    deleting_worktree_paths: HashSet<PathBuf>,
}

fn remove_worktree_command(path: &Path, force: bool) -> String {
    if force {
        format!("worktree remove --force {}", path.display())
    } else {
        format!("worktree remove {}", path.display())
    }
}

struct WorktreeRemoveForceDeletePrompt {
    required_error_substrings: &'static [&'static str],
    message: fn(&str) -> String,
}

impl WorktreeRemoveForceDeletePrompt {
    fn matches(&self, normalized_error_message: &str) -> bool {
        self.required_error_substrings
            .iter()
            .all(|substring| normalized_error_message.contains(substring))
    }
}

const WORKTREE_REMOVE_FORCE_DELETE_PROMPTS: &[WorktreeRemoveForceDeletePrompt] =
    &[WorktreeRemoveForceDeletePrompt {
        required_error_substrings: &[
            "contains modified or untracked files",
            "use --force to delete it",
        ],
        message: dirty_worktree_force_delete_prompt,
    }];

fn dirty_worktree_force_delete_prompt(display_name: &str) -> String {
    format!("Worktree \"{display_name}\" contains modified or untracked files. Force delete it?")
}

fn force_delete_prompt_for_worktree_remove_error(
    error: &anyhow::Error,
    display_name: &str,
) -> Option<String> {
    let normalized_error_message = error.to_string().to_lowercase();
    WORKTREE_REMOVE_FORCE_DELETE_PROMPTS
        .iter()
        .find(|prompt| prompt.matches(&normalized_error_message))
        .map(|prompt| (prompt.message)(display_name))
}

struct DeleteWorktreeTooltip {
    picker: WeakEntity<Picker<WorktreePickerDelegate>>,
    focus_handle: FocusHandle,
    delete_index: usize,
    _subscription: Subscription,
}

impl DeleteWorktreeTooltip {
    fn new(
        picker: Entity<Picker<WorktreePickerDelegate>>,
        focus_handle: FocusHandle,
        delete_index: usize,
        cx: &mut Context<Self>,
    ) -> Self {
        let subscription = cx.observe(&picker, |_, _, cx| cx.notify());
        Self {
            picker: picker.downgrade(),
            focus_handle,
            delete_index,
            _subscription: subscription,
        }
    }
}

impl Render for DeleteWorktreeTooltip {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let force_delete = self
            .picker
            .read_with(cx, |picker, _| {
                picker
                    .delegate
                    .is_force_delete_hovering_index(self.delete_index)
            })
            .unwrap_or(false);

        if force_delete {
            Tooltip::for_action_in(
                "Force Delete Worktree",
                &ForceDeleteWorktree,
                &self.focus_handle,
                cx,
            )
            .into_any_element()
        } else {
            Tooltip::with_meta_in(
                "Delete Worktree",
                Some(&DeleteWorktree),
                "Hold alt to force delete",
                &self.focus_handle,
                cx,
            )
            .into_any_element()
        }
    }
}

impl WorktreePickerDelegate {
    fn build_fixed_entries(&self) -> Vec<WorktreeEntry> {
        worktree_create_targets(
            self.has_multiple_repositories,
            self.default_branch.clone(),
            self.current_branch_name.as_deref(),
        )
        .into_iter()
        .map(|target| match target {
            WorktreeCreateTarget::CurrentBranch => WorktreeEntry::CreateFromCurrentBranch,
            WorktreeCreateTarget::DefaultBranch(default_branch) => {
                WorktreeEntry::CreateFromDefaultBranch { default_branch }
            }
        })
        .collect()
    }

    fn all_repo_worktrees(&self) -> &[GitWorktree] {
        &self.all_worktrees
    }

    fn creation_blocked_reason(&self, cx: &App) -> Option<SharedString> {
        let project = self.project.read(cx);
        if project.is_via_collab() {
            Some("Worktree creation is not supported in collaborative projects".into())
        } else if project.repositories(cx).is_empty() {
            Some("Requires a Git repository in the project".into())
        } else {
            None
        }
    }

    fn can_delete_worktree(&self, worktree: &GitWorktree) -> bool {
        !worktree.is_main && !self.project_worktree_paths.contains(&worktree.path)
    }

    fn refresh_project_worktree_paths(&mut self, window: &mut Window, cx: &mut App) {
        let mut paths = self.active_worktree_paths.clone();

        if let Some(multi_workspace) = window.root::<MultiWorkspace>().flatten()
            && let Some(workspace) = self.workspace.upgrade()
        {
            let group_key = workspace.read(cx).project_group_key(cx);
            for group_workspace in multi_workspace
                .read(cx)
                .workspaces_for_project_group(&group_key, cx)
            {
                for worktree in group_workspace
                    .read(cx)
                    .project()
                    .read(cx)
                    .visible_worktrees(cx)
                {
                    paths.insert(worktree.read(cx).abs_path().to_path_buf());
                }
            }
        }

        self.project_worktree_paths = paths;
    }

    fn is_force_delete_hovering_index(&self, index: usize) -> bool {
        self.modifiers.alt && self.hovered_delete_index == Some(index)
    }

    fn delete_worktree(
        &mut self,
        ix: usize,
        force: bool,
        window: &mut Window,
        cx: &mut Context<Picker<Self>>,
    ) {
        let Some(entry) = self.matches.get(ix) else {
            return;
        };
        let WorktreeEntry::Worktree { worktree, .. } = entry else {
            return;
        };
        if !self.can_delete_worktree(worktree)
            || self.deleting_worktree_paths.contains(&worktree.path)
        {
            return;
        }

        let repo = self.project.read(cx).active_repository(cx);
        let Some(repo) = repo else {
            return;
        };
        let path = worktree.path.clone();
        let display_name = worktree.directory_name(
            self.all_worktrees
                .iter()
                .find(|worktree| worktree.is_main)
                .map(|worktree| worktree.path.as_path()),
        );
        let workspace = self.workspace.clone();

        self.deleting_worktree_paths.insert(path.clone());
        if self.hovered_delete_index == Some(ix) {
            self.hovered_delete_index = None;
        }
        cx.notify();

        cx.spawn_in(window, async move |picker, cx| {
            let initial_result = match repo
                .update(cx, |repo, _| repo.remove_worktree(path.clone(), force))
                .await
            {
                Ok(result) => result,
                Err(error) => {
                    picker.update_in(cx, |picker, _window, cx| {
                        if picker.delegate.deleting_worktree_paths.remove(&path) {
                            cx.notify();
                        }
                    })?;
                    return Err(error.into());
                }
            };

            let (result, attempted_force) = match initial_result {
                Ok(()) => (Ok(()), force),
                Err(error) => {
                    log::error!("Failed to remove worktree: {}", error);

                    let force_delete_prompt = (!force)
                        .then(|| {
                            force_delete_prompt_for_worktree_remove_error(&error, &display_name)
                        })
                        .flatten();

                    if let Some(prompt_message) = force_delete_prompt {
                        picker.update_in(cx, |picker, _window, cx| {
                            if picker.delegate.deleting_worktree_paths.remove(&path) {
                                cx.notify();
                            }
                        })?;

                        let answer = cx.update(|window, cx| {
                            window.prompt(
                                PromptLevel::Warning,
                                &prompt_message,
                                None,
                                &["Force Delete", "Cancel"],
                                cx,
                            )
                        })?;

                        if answer.await != Ok(0) {
                            return Ok(());
                        }

                        let should_retry = picker.update_in(cx, |picker, _window, cx| {
                            let worktree_still_exists = picker
                                .delegate
                                .all_worktrees
                                .iter()
                                .any(|worktree| worktree.path == path);
                            if !worktree_still_exists
                                || !picker.delegate.deleting_worktree_paths.insert(path.clone())
                            {
                                return false;
                            }
                            cx.notify();
                            true
                        })?;

                        if !should_retry {
                            return Ok(());
                        }

                        let retry = match repo
                            .update(cx, |repo, _| repo.remove_worktree(path.clone(), true))
                            .await
                        {
                            Ok(result) => result,
                            Err(error) => {
                                picker.update_in(cx, |picker, _window, cx| {
                                    if picker.delegate.deleting_worktree_paths.remove(&path) {
                                        cx.notify();
                                    }
                                })?;
                                return Err(error.into());
                            }
                        };

                        if let Err(error) = &retry {
                            log::error!("Failed to force remove worktree: {error}");
                        }

                        (retry, true)
                    } else {
                        (Err(error), force)
                    }
                }
            };

            if let Err(error) = result {
                picker.update_in(cx, |picker, _window, cx| {
                    if picker.delegate.deleting_worktree_paths.remove(&path) {
                        cx.notify();
                    }
                })?;

                if let Some(workspace) = workspace.upgrade() {
                    cx.update(|_window, cx| {
                        show_error_toast(
                            workspace,
                            remove_worktree_command(&path, attempted_force),
                            error,
                            cx,
                        )
                    })?;
                }

                return Ok(());
            }

            picker.update_in(cx, |picker, _window, cx| {
                picker.delegate.deleting_worktree_paths.remove(&path);
                picker.delegate.matches.retain(|e| {
                    !matches!(e, WorktreeEntry::Worktree { worktree, .. } if worktree.path == path)
                });
                picker.delegate.all_worktrees.retain(|w| w.path != path);
                if picker.delegate.matches.is_empty() {
                    picker.delegate.selected_index = 0;
                } else if picker.delegate.selected_index >= picker.delegate.matches.len() {
                    picker.delegate.selected_index = picker.delegate.matches.len() - 1;
                }
                picker.delegate.hovered_delete_index = None;
                cx.notify();
            })?;

            anyhow::Ok(())
        })
        .detach_and_log_err(cx);
    }

    /// Finds the workspace in this window (other than the picker's own
    /// workspace) that has `worktree_path` open as a visible worktree.
    fn workspace_for_open_worktree(
        &self,
        worktree_path: &Path,
        window: &Window,
        cx: &App,
    ) -> Option<Entity<Workspace>> {
        if self.active_worktree_paths.contains(worktree_path) {
            return None;
        }
        let multi_workspace = window.root::<MultiWorkspace>().flatten()?;
        let workspace = self.workspace.upgrade()?;
        let group_key = workspace.read(cx).project_group_key(cx);
        multi_workspace
            .read(cx)
            .workspaces_for_project_group(&group_key, cx)
            .into_iter()
            .find(|group_workspace| {
                *group_workspace != workspace
                    && group_workspace
                        .read(cx)
                        .project()
                        .read(cx)
                        .visible_worktrees(cx)
                        .any(|worktree| worktree.read(cx).abs_path().as_ref() == worktree_path)
            })
    }

    fn remove_worktree_from_window(
        &mut self,
        worktree_path: &Path,
        window: &mut Window,
        cx: &mut Context<Picker<Self>>,
    ) {
        if self.deleting_worktree_paths.contains(worktree_path) {
            return;
        }
        let Some(workspace_to_remove) = self.workspace_for_open_worktree(worktree_path, window, cx)
        else {
            return;
        };
        let Some(window_handle) = window.window_handle().downcast::<MultiWorkspace>() else {
            return;
        };

        cx.spawn_in(window, async move |picker, cx| {
            let removed = window_handle
                .update(cx, |multi_workspace, window, cx| {
                    multi_workspace.remove(
                        [workspace_to_remove.clone()],
                        RemovalIntent::CloseProject,
                        window,
                        cx,
                    )
                })?
                .await?;

            if removed {
                picker.update_in(cx, |picker, window, cx| {
                    picker.delegate.refresh_project_worktree_paths(window, cx);
                    picker.refresh(window, cx);
                })?;
            }

            anyhow::Ok(())
        })
        .detach_and_log_err(cx);
    }

    fn sync_selected_index(&mut self, has_query: bool) {
        if !has_query {
            return;
        }

        if let Some(index) = self
            .matches
            .iter()
            .position(|entry| matches!(entry, WorktreeEntry::Worktree { .. }))
        {
            self.selected_index = index;
        } else if let Some(index) = self
            .matches
            .iter()
            .position(|entry| matches!(entry, WorktreeEntry::CreateNamed { .. }))
        {
            self.selected_index = index;
        } else {
            self.selected_index = 0;
        }
    }
}

impl PickerDelegate for WorktreePickerDelegate {
    type ListItem = AnyElement;

    fn name() -> &'static str {
        "worktree picker"
    }

    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
        "Select or type to create a worktree…".into()
    }

    fn editor_position(&self) -> PickerEditorPosition {
        PickerEditorPosition::Start
    }

    fn match_count(&self) -> usize {
        self.matches.len()
    }

    fn selected_index(&self) -> usize {
        self.selected_index
    }

    fn set_selected_index(
        &mut self,
        ix: usize,
        _window: &mut Window,
        _cx: &mut Context<Picker<Self>>,
    ) {
        self.selected_index = ix;
    }

    fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {
        !matches!(
            self.matches.get(ix),
            Some(WorktreeEntry::Separator | WorktreeEntry::SectionHeader(_))
        )
    }

    fn update_matches(
        &mut self,
        query: String,
        window: &mut Window,
        cx: &mut Context<Picker<Self>>,
    ) -> Task<()> {
        let repo_worktrees = self.all_repo_worktrees().to_vec();

        let normalized_query = query.replace(' ', "-");
        let main_worktree_path = self
            .all_worktrees
            .iter()
            .find(|wt| wt.is_main)
            .map(|wt| wt.path.clone());
        let has_named_worktree = self.all_worktrees.iter().any(|worktree| {
            worktree.directory_name(main_worktree_path.as_deref()) == normalized_query
        });
        let create_named_disabled_reason: Option<String> = if self.has_multiple_repositories {
            Some("Cannot create a named worktree in a project with multiple repositories".into())
        } else if has_named_worktree {
            Some("A worktree with this name already exists".into())
        } else {
            None
        };

        let show_default_branch_create =
            !self.has_multiple_repositories && self.default_branch.is_some();
        let default_branch = self.default_branch.clone();

        if query.is_empty() {
            let mut matches = self.build_fixed_entries();

            if !repo_worktrees.is_empty() {
                let main_worktree_path = repo_worktrees
                    .iter()
                    .find(|wt| wt.is_main)
                    .map(|wt| wt.path.clone());

                let project_paths = &self.project_worktree_paths;

                let sort_by_name = |a: &GitWorktree, b: &GitWorktree| {
                    a.directory_name(main_worktree_path.as_deref())
                        .cmp(&b.directory_name(main_worktree_path.as_deref()))
                };

                let (mut open_here, mut others): (Vec<_>, Vec<_>) = repo_worktrees
                    .into_iter()
                    .partition(|worktree| project_paths.contains(&worktree.path));
                open_here.sort_by(sort_by_name);
                others.sort_by(sort_by_name);

                matches.push(WorktreeEntry::Separator);

                if open_here.len() > 1 {
                    matches.push(WorktreeEntry::SectionHeader("This Window".into()));
                    for worktree in open_here {
                        matches.push(WorktreeEntry::Worktree {
                            worktree,
                            positions: Vec::new(),
                        });
                    }

                    if !others.is_empty() {
                        matches.push(WorktreeEntry::Separator);
                    }

                    for worktree in others {
                        matches.push(WorktreeEntry::Worktree {
                            worktree,
                            positions: Vec::new(),
                        });
                    }
                } else {
                    for worktree in open_here.into_iter().chain(others) {
                        matches.push(WorktreeEntry::Worktree {
                            worktree,
                            positions: Vec::new(),
                        });
                    }
                }
            }

            self.matches = matches;
            self.sync_selected_index(false);
            return Task::ready(());
        }

        let main_worktree_path = repo_worktrees
            .iter()
            .find(|wt| wt.is_main)
            .map(|wt| wt.path.clone());
        let candidates: Vec<_> = repo_worktrees
            .iter()
            .enumerate()
            .map(|(ix, worktree)| {
                StringMatchCandidate::new(
                    ix,
                    &worktree.directory_name(main_worktree_path.as_deref()),
                )
            })
            .collect();

        let executor = cx.background_executor().clone();

        let task = cx.background_executor().spawn(async move {
            fuzzy::match_strings(
                &candidates,
                &query,
                true,
                true,
                10000,
                &Default::default(),
                executor,
            )
            .await
        });

        let repo_worktrees_clone = repo_worktrees;
        cx.spawn_in(window, async move |picker, cx| {
            let fuzzy_matches = task.await;

            picker
                .update_in(cx, |picker, _window, cx| {
                    let mut new_matches: Vec<WorktreeEntry> = Vec::new();

                    for candidate in &fuzzy_matches {
                        new_matches.push(WorktreeEntry::Worktree {
                            worktree: repo_worktrees_clone[candidate.candidate_id].clone(),
                            positions: candidate.positions.clone(),
                        });
                    }

                    if !new_matches.is_empty() {
                        new_matches.push(WorktreeEntry::Separator);
                    }
                    if show_default_branch_create {
                        if let Some(ref default_branch) = default_branch {
                            new_matches.push(WorktreeEntry::CreateNamed {
                                name: normalized_query.clone(),
                                from_branch: Some(default_branch.clone()),
                                disabled_reason: create_named_disabled_reason.clone(),
                            });
                        }
                    } else {
                        new_matches.push(WorktreeEntry::CreateNamed {
                            name: normalized_query.clone(),
                            from_branch: None,
                            disabled_reason: create_named_disabled_reason.clone(),
                        });
                    }

                    picker.delegate.matches = new_matches;
                    picker.delegate.sync_selected_index(true);

                    cx.notify();
                })
                .log_err();
        })
    }

    fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
        let Some(entry) = self.matches.get(self.selected_index) else {
            return;
        };

        match entry {
            WorktreeEntry::Separator | WorktreeEntry::SectionHeader(_) => return,
            WorktreeEntry::CreateFromCurrentBranch => {
                if self.creation_blocked_reason(cx).is_some() {
                    return;
                }
                if let Some(workspace) = self.workspace.upgrade() {
                    workspace.update(cx, |workspace, cx| {
                        crate::worktree_service::handle_create_worktree(
                            workspace,
                            &CreateWorktree {
                                worktree_name: None,
                                branch_target: NewWorktreeBranchTarget::CurrentBranch,
                            },
                            window,
                            self.focused_dock,
                            cx,
                        );
                    });
                }
            }
            WorktreeEntry::CreateFromDefaultBranch { default_branch } => {
                if self.creation_blocked_reason(cx).is_some() {
                    return;
                }
                if let Some(workspace) = self.workspace.upgrade() {
                    workspace.update(cx, |workspace, cx| {
                        crate::worktree_service::handle_create_worktree(
                            workspace,
                            &CreateWorktree {
                                worktree_name: None,
                                branch_target: NewWorktreeBranchTarget::RemoteBranch {
                                    remote_name: default_branch.remote_name.clone(),
                                    branch_name: default_branch.branch_name.clone(),
                                },
                            },
                            window,
                            self.focused_dock,
                            cx,
                        );
                    });
                }
            }
            WorktreeEntry::Worktree { worktree, .. } => {
                if self.deleting_worktree_paths.contains(&worktree.path) {
                    return;
                }

                let is_current = self.active_worktree_paths.contains(&worktree.path);

                if !is_current {
                    if secondary {
                        window.dispatch_action(
                            Box::new(OpenWorktreeInNewWindow {
                                path: worktree.path.clone(),
                            }),
                            cx,
                        );
                    } else {
                        let main_worktree_path = self
                            .all_worktrees
                            .iter()
                            .find(|wt| wt.is_main)
                            .map(|wt| wt.path.as_path());
                        if let Some(workspace) = self.workspace.upgrade() {
                            workspace.update(cx, |workspace, cx| {
                                crate::worktree_service::handle_switch_worktree(
                                    workspace,
                                    &SwitchWorktree {
                                        path: worktree.path.clone(),
                                        display_name: worktree.directory_name(main_worktree_path),
                                    },
                                    window,
                                    self.focused_dock,
                                    cx,
                                );
                            });
                        }
                    }
                }
            }
            WorktreeEntry::CreateNamed {
                name,
                from_branch,
                disabled_reason: None,
            } => {
                let branch_target = match from_branch {
                    Some(branch) => NewWorktreeBranchTarget::RemoteBranch {
                        remote_name: branch.remote_name.clone(),
                        branch_name: branch.branch_name.clone(),
                    },
                    None => NewWorktreeBranchTarget::CurrentBranch,
                };
                if let Some(workspace) = self.workspace.upgrade() {
                    workspace.update(cx, |workspace, cx| {
                        crate::worktree_service::handle_create_worktree(
                            workspace,
                            &CreateWorktree {
                                worktree_name: Some(name.clone()),
                                branch_target,
                            },
                            window,
                            self.focused_dock,
                            cx,
                        );
                    });
                }
            }
            WorktreeEntry::CreateNamed {
                disabled_reason: Some(_),
                ..
            } => {
                return;
            }
        }

        cx.emit(DismissEvent);
    }

    fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}

    fn render_match(
        &self,
        ix: usize,
        selected: bool,
        _window: &mut Window,
        cx: &mut Context<Picker<Self>>,
    ) -> Option<Self::ListItem> {
        let entry = self.matches.get(ix)?;

        match entry {
            WorktreeEntry::Separator => Some(
                div()
                    .py(DynamicSpacing::Base04.rems(cx))
                    .child(Divider::horizontal())
                    .into_any_element(),
            ),
            WorktreeEntry::SectionHeader(label) => Some(
                ListSubHeader::new(label.clone())
                    .inset(true)
                    .into_any_element(),
            ),
            WorktreeEntry::CreateFromCurrentBranch => {
                let branch_label = WorktreeCreateTarget::CurrentBranch.branch_label(
                    self.has_multiple_repositories,
                    self.current_branch_name.as_deref(),
                );

                let label = format!("Create new worktree based on {branch_label}");

                let item = create_new_list_item(
                    "create-from-current".to_string().into(),
                    label.into(),
                    self.creation_blocked_reason(cx),
                    selected,
                );

                Some(item.into_any_element())
            }
            WorktreeEntry::CreateFromDefaultBranch { default_branch } => {
                let branch_label = WorktreeCreateTarget::DefaultBranch(default_branch.clone())
                    .branch_label(
                        self.has_multiple_repositories,
                        self.current_branch_name.as_deref(),
                    );
                let label = format!("Create new worktree based on {branch_label}");

                let item = create_new_list_item(
                    "create-from-main".to_string().into(),
                    label.into(),
                    self.creation_blocked_reason(cx),
                    selected,
                );

                Some(item.into_any_element())
            }
            WorktreeEntry::Worktree {
                worktree,
                positions,
            } => {
                let main_worktree_path = self
                    .all_worktrees
                    .iter()
                    .find(|wt| wt.is_main)
                    .map(|wt| wt.path.as_path());
                let display_name = worktree.directory_name(main_worktree_path);
                let first_line = display_name.lines().next().unwrap_or(&display_name);
                let positions: Vec<_> = positions
                    .iter()
                    .copied()
                    .filter(|&pos| pos < first_line.len())
                    .collect();
                let path = worktree.path.compact().to_string_lossy().to_string();
                let sha = worktree.sha.chars().take(7).collect::<String>();

                let is_current = self.active_worktree_paths.contains(&worktree.path);
                let is_deleting = self.deleting_worktree_paths.contains(&worktree.path);
                let can_delete = self.can_delete_worktree(worktree);
                let can_remove_from_window =
                    !is_current && self.project_worktree_paths.contains(&worktree.path);

                let entry_icon = if is_current {
                    IconName::Check
                } else {
                    IconName::GitWorktree
                };
                let picker = cx.entity();

                Some(
                    ListItem::new(SharedString::from(format!("worktree-{ix}")))
                        .inset(true)
                        .spacing(ListItemSpacing::Sparse)
                        .toggle_state(selected)
                        .child(
                            h_flex()
                                .w_full()
                                .gap_2p5()
                                .child(
                                    Icon::new(entry_icon)
                                        .color(if is_current {
                                            Color::Accent
                                        } else {
                                            Color::Muted
                                        })
                                        .size(IconSize::Small),
                                )
                                .child(
                                    v_flex()
                                        .w_full()
                                        .min_w_0()
                                        .child(
                                            HighlightedLabel::new(first_line.to_owned(), positions)
                                                .truncate(),
                                        )
                                        .child(
                                            h_flex()
                                                .w_full()
                                                .min_w_0()
                                                .gap_1p5()
                                                .when_some(
                                                    worktree.branch_name().map(|b| b.to_string()),
                                                    |this, branch| {
                                                        this.child(
                                                            Label::new(branch)
                                                                .size(LabelSize::Small)
                                                                .color(Color::Muted),
                                                        )
                                                        .child(
                                                            Label::new("\u{2022}")
                                                                .alpha(0.5)
                                                                .color(Color::Muted)
                                                                .size(LabelSize::Small),
                                                        )
                                                    },
                                                )
                                                .when(!sha.is_empty(), |this| {
                                                    this.child(
                                                        Label::new(sha)
                                                            .size(LabelSize::Small)
                                                            .color(Color::Muted),
                                                    )
                                                    .child(
                                                        Label::new("\u{2022}")
                                                            .alpha(0.5)
                                                            .color(Color::Muted)
                                                            .size(LabelSize::Small),
                                                    )
                                                })
                                                .child(
                                                    Label::new(path)
                                                        .truncate_start()
                                                        .color(Color::Muted)
                                                        .size(LabelSize::Small)
                                                        .flex_1(),
                                                ),
                                        ),
                                ),
                        )
                        .when(is_deleting, |this| {
                            this.end_slot(
                                h_flex()
                                    .gap_1()
                                    .child(
                                        Icon::new(IconName::LoadCircle)
                                            .size(IconSize::Small)
                                            .color(Color::Muted)
                                            .with_rotate_animation(2),
                                    )
                                    .child(
                                        Label::new("Deleting…")
                                            .size(LabelSize::Small)
                                            .color(Color::Muted),
                                    ),
                            )
                        })
                        .when(!is_deleting && !is_current, |this| {
                            let open_in_new_window_button =
                                IconButton::new(("open-new-window", ix), IconName::ArrowUpRight)
                                    .icon_size(IconSize::Small)
                                    .tooltip(Tooltip::text("Open in New Window"))
                                    .on_click(cx.listener(move |picker, _, window, cx| {
                                        let Some(entry) = picker.delegate.matches.get(ix) else {
                                            return;
                                        };
                                        if let WorktreeEntry::Worktree { worktree, .. } = entry {
                                            if picker
                                                .delegate
                                                .deleting_worktree_paths
                                                .contains(&worktree.path)
                                            {
                                                return;
                                            }
                                            window.dispatch_action(
                                                Box::new(OpenWorktreeInNewWindow {
                                                    path: worktree.path.clone(),
                                                }),
                                                cx,
                                            );
                                            cx.emit(DismissEvent);
                                        }
                                    }));

                            let focus_handle_delete = self.focus_handle.clone();
                            let force_delete = self.is_force_delete_hovering_index(ix);
                            let delete_button = div()
                                .id(("delete-worktree-hover", ix))
                                .on_hover(cx.listener(move |picker, hovered: &bool, _, cx| {
                                    if *hovered {
                                        picker.delegate.hovered_delete_index = Some(ix);
                                    } else if picker.delegate.hovered_delete_index == Some(ix) {
                                        picker.delegate.hovered_delete_index = None;
                                    }
                                    cx.notify();
                                }))
                                .child(
                                    IconButton::new(("delete-worktree", ix), IconName::Trash)
                                        .icon_size(IconSize::Small)
                                        .when(force_delete, |this| this.icon_color(Color::Error))
                                        .tooltip(move |_, cx| {
                                            cx.new(|cx| {
                                                DeleteWorktreeTooltip::new(
                                                    picker.clone(),
                                                    focus_handle_delete.clone(),
                                                    ix,
                                                    cx,
                                                )
                                            })
                                            .into()
                                        })
                                        .on_click(cx.listener(move |picker, _, window, cx| {
                                            let force = picker.delegate.modifiers.alt;
                                            picker.delegate.delete_worktree(ix, force, window, cx);
                                        })),
                                );

                            this.end_slot(
                                h_flex()
                                    .gap_0p5()
                                    .child(open_in_new_window_button)
                                    .when(can_remove_from_window, |this| {
                                        let worktree_path = worktree.path.clone();
                                        this.child(
                                            IconButton::new(
                                                ("remove-worktree-from-window", ix),
                                                IconName::Close,
                                            )
                                            .icon_size(IconSize::Small)
                                            .tooltip(Tooltip::text("Remove Worktree from Window"))
                                            .on_click(
                                                cx.listener(move |picker, _, window, cx| {
                                                    picker.delegate.remove_worktree_from_window(
                                                        &worktree_path,
                                                        window,
                                                        cx,
                                                    );
                                                }),
                                            ),
                                        )
                                    })
                                    .when(can_delete, |this| this.child(delete_button)),
                            )
                            .show_end_slot_on_hover()
                        })
                        .into_any_element(),
                )
            }
            WorktreeEntry::CreateNamed {
                name,
                from_branch,
                disabled_reason,
            } => {
                let branch_label = from_branch
                    .as_ref()
                    .map(RemoteBranchName::display_name)
                    .unwrap_or_else(|| {
                        self.current_branch_name
                            .clone()
                            .unwrap_or_else(|| "HEAD".to_string())
                    });
                let label = format!("Create \"{name}\" based on {branch_label}");
                let element_id = match from_branch {
                    Some(branch) => format!("create-named-from-{}", branch.display_name()),
                    None => "create-named-from-current".to_string(),
                };

                let item = create_new_list_item(
                    element_id.into(),
                    label.into(),
                    disabled_reason.clone().map(SharedString::from),
                    selected,
                );

                Some(item.into_any_element())
            }
        }
    }

    fn searchbar_trailer(
        &self,
        _window: &mut Window,
        _cx: &mut Context<Picker<Self>>,
    ) -> Option<AnyElement> {
        if self.show_footer {
            return None;
        }

        let focus_handle = self.focus_handle.clone();

        Some(
            IconButton::new("configure-worktree-tasks", IconName::Settings)
                .icon_size(IconSize::Small)
                .tooltip(move |_window, cx| {
                    Tooltip::for_action_in(
                        "Automate Worktree Setup",
                        &OpenWorktreeSetupTasks,
                        &focus_handle,
                        cx,
                    )
                })
                .on_click(|_, window, cx| {
                    window.dispatch_action(OpenWorktreeSetupTasks.boxed_clone(), cx)
                })
                .into_any_element(),
        )
    }

    fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
        if !self.show_footer {
            return None;
        }

        let focus_handle = self.focus_handle.clone();
        let selected_entry = self.matches.get(self.selected_index);

        let is_creating = selected_entry.is_some_and(|e| {
            matches!(
                e,
                WorktreeEntry::CreateFromCurrentBranch
                    | WorktreeEntry::CreateFromDefaultBranch { .. }
                    | WorktreeEntry::CreateNamed { .. }
            )
        });

        let is_existing_worktree =
            selected_entry.is_some_and(|e| matches!(e, WorktreeEntry::Worktree { .. }));

        let can_delete = selected_entry.is_some_and(|e| {
            matches!(e, WorktreeEntry::Worktree { worktree, .. } if self.can_delete_worktree(worktree))
        });

        let is_current = selected_entry.is_some_and(|e| {
            matches!(e, WorktreeEntry::Worktree { worktree, .. } if self.project_worktree_paths.contains(&worktree.path))
        });

        let is_deleting = selected_entry.is_some_and(|e| {
            matches!(e, WorktreeEntry::Worktree { worktree, .. } if self.deleting_worktree_paths.contains(&worktree.path))
        });

        let footer = h_flex()
            .w_full()
            .p_1p5()
            .gap_0p5()
            .justify_between()
            .border_t_1()
            .border_color(cx.theme().colors().border_variant)
            .child(
                Button::new("configure-worktree-tasks", "Automate Setup")
                    .key_binding(
                        KeyBinding::for_action_in(&OpenWorktreeSetupTasks, &focus_handle, cx)
                            .map(|kb| kb.size(rems_from_px(12_f32))),
                    )
                    .on_click(|_, window, cx| {
                        window.dispatch_action(OpenWorktreeSetupTasks.boxed_clone(), cx)
                    }),
            );

        if is_creating {
            Some(
                footer
                    .child(
                        Button::new("create-worktree", "Create")
                            .key_binding(
                                KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
                                    .map(|kb| kb.size(rems_from_px(12_f32))),
                            )
                            .on_click(|_, window, cx| {
                                window.dispatch_action(menu::Confirm.boxed_clone(), cx)
                            }),
                    )
                    .into_any(),
            )
        } else if is_existing_worktree {
            Some(
                footer
                    .child(
                        h_flex()
                            .gap_0p5()
                            .when(is_deleting, |this| {
                                this.child(
                                    Button::new("delete-worktree", "Deleting…")
                                        .loading(true)
                                        .disabled(true),
                                )
                            })
                            .when(!is_deleting && can_delete, |this| {
                                let focus_handle = focus_handle.clone();
                                this.child(
                                    Button::new("delete-worktree", "Delete")
                                        .key_binding(
                                            KeyBinding::for_action_in(
                                                &DeleteWorktree,
                                                &focus_handle,
                                                cx,
                                            )
                                            .map(|kb| kb.size(rems_from_px(12_f32))),
                                        )
                                        .on_click(|_, window, cx| {
                                            window.dispatch_action(DeleteWorktree.boxed_clone(), cx)
                                        }),
                                )
                            })
                            .when(!is_deleting && !is_current, |this| {
                                let focus_handle = focus_handle.clone();
                                this.child(
                                    Button::new("open-in-new-window", "Open in New Window")
                                        .key_binding(
                                            KeyBinding::for_action_in(
                                                &menu::SecondaryConfirm,
                                                &focus_handle,
                                                cx,
                                            )
                                            .map(|kb| kb.size(rems_from_px(12_f32))),
                                        )
                                        .on_click(|_, window, cx| {
                                            window.dispatch_action(
                                                menu::SecondaryConfirm.boxed_clone(),
                                                cx,
                                            )
                                        }),
                                )
                            })
                            .when(!is_deleting, |this| {
                                this.child(
                                    Button::new("open-worktree", "Open")
                                        .key_binding(
                                            KeyBinding::for_action_in(
                                                &menu::Confirm,
                                                &focus_handle,
                                                cx,
                                            )
                                            .map(|kb| kb.size(rems_from_px(12_f32))),
                                        )
                                        .on_click(|_, window, cx| {
                                            window.dispatch_action(menu::Confirm.boxed_clone(), cx)
                                        }),
                                )
                            }),
                    )
                    .into_any(),
            )
        } else {
            None
        }
    }
}

fn create_new_list_item(
    id: SharedString,
    label: SharedString,
    disabled_tooltip: Option<SharedString>,
    selected: bool,
) -> AnyElement {
    let is_disabled = disabled_tooltip.is_some();

    ListItem::new(id)
        .inset(true)
        .spacing(ListItemSpacing::Sparse)
        .toggle_state(selected)
        .child(
            h_flex()
                .w_full()
                .gap_2p5()
                .child(
                    Icon::new(IconName::Plus)
                        .map(|this| {
                            if is_disabled {
                                this.color(Color::Disabled)
                            } else {
                                this.color(Color::Muted)
                            }
                        })
                        .size(IconSize::Small),
                )
                .child(Label::new(label).when(is_disabled, |this| this.color(Color::Disabled))),
        )
        .when_some(disabled_tooltip, |this, reason| {
            this.tooltip(Tooltip::text(reason))
        })
        .into_any_element()
}

pub async fn open_remote_worktree(
    connection_options: remote::RemoteConnectionOptions,
    paths: Vec<PathBuf>,
    app_state: Arc<workspace::AppState>,
    workspace: gpui::WeakEntity<Workspace>,
    cx: &mut gpui::AsyncWindowContext,
) -> anyhow::Result<()> {
    let connect_task = workspace.update_in(cx, |workspace, window, cx| {
        workspace.toggle_modal(window, cx, |window, cx| {
            remote_connection::RemoteConnectionModal::new(
                &connection_options,
                Vec::new(),
                window,
                cx,
            )
        });

        let prompt = workspace
            .active_modal::<remote_connection::RemoteConnectionModal>(cx)
            .expect("Modal just created")
            .read(cx)
            .prompt
            .clone();

        remote_connection::connect(
            remote::remote_client::ConnectionIdentifier::setup(),
            connection_options.clone(),
            prompt,
            window,
            cx,
        )
        .prompt_err("Failed to connect", window, cx, |_, _, _| None)
    })?;

    let session = connect_task.await;

    workspace
        .update_in(cx, |workspace, _window, cx| {
            if let Some(prompt) =
                workspace.active_modal::<remote_connection::RemoteConnectionModal>(cx)
            {
                prompt.update(cx, |prompt, cx| prompt.finished(cx))
            }
        })
        .ok();

    let Some(Some(session)) = session else {
        return Ok(());
    };

    let new_project = cx.update(|_, cx| {
        project::Project::remote(
            session,
            app_state.client.clone(),
            app_state.node_runtime.clone(),
            app_state.user_store.clone(),
            app_state.languages.clone(),
            app_state.fs.clone(),
            true,
            cx,
        )
    })?;

    let workspace_position = cx
        .update(|_, cx| {
            workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx)
        })?
        .await
        .context("fetching workspace position from db")?;

    let mut options =
        cx.update(|_, cx| (app_state.build_window_options)(workspace_position.display, cx))?;
    options.window_bounds = workspace_position.window_bounds;

    let new_window = cx.open_window(options, |window, cx| {
        let workspace = cx.new(|cx| {
            let mut workspace =
                Workspace::new(None, new_project.clone(), app_state.clone(), window, cx);
            workspace.centered_layout = workspace_position.centered_layout;
            workspace
        });
        cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
    })?;

    workspace::open_remote_project_with_existing_connection(
        connection_options,
        new_project,
        paths,
        app_state,
        new_window,
        None,
        None,
        cx,
    )
    .await?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use fs::FakeFs;
    use gpui::{AppContext, TestAppContext, VisualTestContext};
    use project::project_settings::ProjectSettings;
    use project::{Project, WorktreeSettings};
    use serde_json::json;
    use settings::Settings as _;
    use settings::SettingsStore;
    use util::path;
    use workspace::MultiWorkspace;

    fn init_test(cx: &mut TestAppContext) {
        cx.update(|cx| {
            let settings_store = SettingsStore::test(cx);
            cx.set_global(settings_store);
            theme_settings::init(theme::LoadThemes::JustBase, cx);
            editor::init(cx);
            ProjectSettings::register(cx);
            WorktreeSettings::register(cx);
        });
    }

    async fn init_worktree_picker_test(
        cx: &mut TestAppContext,
    ) -> (
        Arc<FakeFs>,
        Entity<WorktreePicker>,
        Entity<project::git_store::Repository>,
        PathBuf,
        VisualTestContext,
    ) {
        init_test(cx);

        let fs = FakeFs::new(cx.executor());
        fs.insert_tree(
            path!("/root"),
            json!({
                "project": {
                    ".git": {},
                    "file.txt": "buffer_text",
                },
                "worktrees": {},
            }),
        )
        .await;
        fs.set_head_for_repo(
            path!("/root/project/.git").as_ref(),
            &[("file.txt", "buffer_text".to_string())],
            "deadbeef",
        );

        let project = Project::test(fs.clone(), [path!("/root/project").as_ref()], cx).await;
        cx.executor().run_until_parked();

        let repository = project.read_with(cx, |project, cx| {
            project.repositories(cx).values().next().unwrap().clone()
        });
        let worktree_path = PathBuf::from(path!("/root/worktrees/dirty-wt"));

        cx.update(|cx| {
            repository.update(cx, |repository, _| {
                repository.create_worktree(
                    git::repository::CreateWorktreeTarget::NewBranch {
                        branch_name: "dirty-wt".to_string(),
                        base_sha: Some("deadbeef".to_string()),
                    },
                    worktree_path.clone(),
                )
            })
        })
        .await
        .unwrap()
        .unwrap();

        let window_handle =
            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
        let workspace = window_handle
            .read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone())
            .unwrap();

        let mut cx = VisualTestContext::from_window(window_handle.into(), cx);

        let worktree_picker = cx.update(|window, cx| {
            cx.new(|cx| WorktreePicker::new(project, workspace.downgrade(), window, cx))
        });

        cx.run_until_parked();

        (fs, worktree_picker, repository, worktree_path, cx)
    }

    fn worktree_index(
        worktree_picker: &Entity<WorktreePicker>,
        worktree_path: &Path,
        cx: &mut VisualTestContext,
    ) -> usize {
        worktree_picker.update(cx, |worktree_picker, cx| {
            worktree_picker.picker.update(cx, |picker, _| {
                picker
                    .delegate
                    .matches
                    .iter()
                    .position(|entry| {
                        matches!(entry, WorktreeEntry::Worktree { worktree, .. } if worktree.path == *worktree_path)
                    })
                    .expect("worktree should appear in picker")
            })
        })
    }

    fn picker_contains_worktree(
        worktree_picker: &Entity<WorktreePicker>,
        worktree_path: &Path,
        cx: &mut VisualTestContext,
    ) -> bool {
        worktree_picker.update(cx, |worktree_picker, cx| {
            worktree_picker.picker.update(cx, |picker, _| {
                picker.delegate.all_worktrees.iter().any(|worktree| {
                    worktree.path == *worktree_path
                }) && picker.delegate.matches.iter().any(|entry| {
                    matches!(entry, WorktreeEntry::Worktree { worktree, .. } if worktree.path == *worktree_path)
                })
            })
        })
    }

    fn deleting_worktree_paths(
        worktree_picker: &Entity<WorktreePicker>,
        cx: &mut VisualTestContext,
    ) -> HashSet<PathBuf> {
        worktree_picker.update(cx, |worktree_picker, cx| {
            worktree_picker.picker.update(cx, |picker, _| {
                picker.delegate.deleting_worktree_paths.clone()
            })
        })
    }

    async fn repo_contains_worktree(
        repository: &Entity<project::git_store::Repository>,
        worktree_path: &Path,
        cx: &mut VisualTestContext,
    ) -> bool {
        let worktrees = repository
            .update(cx, |repository, _| repository.worktrees())
            .await
            .unwrap()
            .unwrap();
        worktrees
            .iter()
            .any(|worktree| worktree.path == *worktree_path)
    }

    #[gpui::test]
    async fn test_delete_worktree_marks_row_pending_immediately(cx: &mut TestAppContext) {
        let (_, worktree_picker, _repository, worktree_path, mut cx) =
            init_worktree_picker_test(cx).await;

        let index = worktree_index(&worktree_picker, &worktree_path, &mut cx);
        worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
            worktree_picker.picker.update(cx, |picker, cx| {
                picker.delegate.delete_worktree(index, false, window, cx);
            })
        });

        let pending_paths = deleting_worktree_paths(&worktree_picker, &mut cx);
        assert_eq!(pending_paths.len(), 1);
        assert!(pending_paths.contains(&worktree_path));

        cx.run_until_parked();
    }

    #[gpui::test]
    async fn test_delete_worktree_clears_pending_and_removes_row_on_success(
        cx: &mut TestAppContext,
    ) {
        let (_, worktree_picker, repository, worktree_path, mut cx) =
            init_worktree_picker_test(cx).await;

        let index = worktree_index(&worktree_picker, &worktree_path, &mut cx);
        worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
            worktree_picker.picker.update(cx, |picker, cx| {
                picker.delegate.delete_worktree(index, false, window, cx);
            })
        });
        assert!(deleting_worktree_paths(&worktree_picker, &mut cx).contains(&worktree_path));

        cx.run_until_parked();

        assert!(deleting_worktree_paths(&worktree_picker, &mut cx).is_empty());
        assert!(!picker_contains_worktree(
            &worktree_picker,
            &worktree_path,
            &mut cx
        ));
        assert!(
            !repo_contains_worktree(&repository, &worktree_path, &mut cx).await,
            "worktree should be removed after successful delete"
        );
    }

    #[gpui::test]
    async fn test_remote_default_branch_is_preferred_create_target(cx: &mut TestAppContext) {
        let (_fs, worktree_picker, _repository, _worktree_path, mut cx) =
            init_worktree_picker_test(cx).await;

        worktree_picker.update(&mut cx, |worktree_picker, cx| {
            worktree_picker.picker.update(cx, |picker, _| {
                assert_eq!(picker.delegate.selected_index, 0);
                match picker.delegate.matches.first() {
                    Some(WorktreeEntry::CreateFromDefaultBranch { default_branch }) => {
                        assert_eq!(default_branch.display_name(), "origin/main");
                    }
                    _ => panic!("remote default branch should be the first create target"),
                }
            })
        });

        let update_matches = worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
            worktree_picker.picker.update(cx, |picker, cx| {
                picker
                    .delegate
                    .update_matches("feature".to_string(), window, cx)
            })
        });
        update_matches.await;
        cx.run_until_parked();

        worktree_picker.update(&mut cx, |worktree_picker, cx| {
            worktree_picker
                .picker
                .update(cx, |picker, _| match picker.delegate.matches.first() {
                    Some(WorktreeEntry::CreateNamed {
                        from_branch: Some(default_branch),
                        ..
                    }) => {
                        assert_eq!(default_branch.display_name(), "origin/main");
                    }
                    _ => panic!("named worktree creation should prefer the remote default branch"),
                })
        });
    }

    #[gpui::test]
    async fn test_current_branch_create_target_is_shown_without_default_branch(
        cx: &mut TestAppContext,
    ) {
        let (_fs, worktree_picker, _repository, _worktree_path, mut cx) =
            init_worktree_picker_test(cx).await;

        worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
            worktree_picker.picker.update(cx, |picker, cx| {
                picker.delegate.default_branch = None;
                picker.refresh(window, cx);
            });
        });
        cx.run_until_parked();

        worktree_picker.update(&mut cx, |worktree_picker, cx| {
            worktree_picker.picker.update(cx, |picker, _| {
                assert!(matches!(
                    picker.delegate.matches.first(),
                    Some(WorktreeEntry::CreateFromCurrentBranch)
                ));
                assert!(
                    !picker.delegate.matches.iter().any(|entry| matches!(
                        entry,
                        WorktreeEntry::CreateFromDefaultBranch { .. }
                    ))
                );
            });
        });
    }

    #[gpui::test]
    async fn test_delete_dirty_worktree_prompts_for_force_delete(cx: &mut TestAppContext) {
        let (fs, worktree_picker, repository, worktree_path, mut cx) =
            init_worktree_picker_test(cx).await;

        fs.with_git_state(path!("/root/project/.git").as_ref(), true, |state| {
            state
                .worktrees_requiring_force_delete
                .insert(worktree_path.clone());
        })
        .expect("failed to mark test worktree as requiring force delete");

        let index = worktree_index(&worktree_picker, &worktree_path, &mut cx);
        worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
            worktree_picker.picker.update(cx, |picker, cx| {
                picker.delegate.delete_worktree(index, false, window, cx);
            })
        });
        assert!(deleting_worktree_paths(&worktree_picker, &mut cx).contains(&worktree_path));

        cx.run_until_parked();
        assert!(cx.has_pending_prompt());
        assert!(
            !deleting_worktree_paths(&worktree_picker, &mut cx).contains(&worktree_path),
            "pending delete state should clear while waiting for force-delete confirmation"
        );

        cx.simulate_prompt_answer("Force Delete");
        cx.run_until_parked();

        assert!(!cx.has_pending_prompt());
        assert!(deleting_worktree_paths(&worktree_picker, &mut cx).is_empty());
        assert!(!picker_contains_worktree(
            &worktree_picker,
            &worktree_path,
            &mut cx
        ));
        assert!(
            !repo_contains_worktree(&repository, &worktree_path, &mut cx).await,
            "worktree should be removed after confirming force delete"
        );
    }

    #[gpui::test]
    async fn test_duplicate_delete_worktree_is_ignored_while_pending(cx: &mut TestAppContext) {
        let (fs, worktree_picker, _repository, worktree_path, mut cx) =
            init_worktree_picker_test(cx).await;

        fs.with_git_state(path!("/root/project/.git").as_ref(), true, |state| {
            state
                .worktrees_requiring_force_delete
                .insert(worktree_path.clone());
        })
        .expect("failed to mark test worktree as requiring force delete");

        let index = worktree_index(&worktree_picker, &worktree_path, &mut cx);
        worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
            worktree_picker.picker.update(cx, |picker, cx| {
                picker.delegate.delete_worktree(index, false, window, cx);
                picker.delegate.delete_worktree(index, false, window, cx);
            })
        });

        let pending_paths = deleting_worktree_paths(&worktree_picker, &mut cx);
        assert_eq!(pending_paths.len(), 1);
        assert!(pending_paths.contains(&worktree_path));

        cx.run_until_parked();
        assert!(cx.has_pending_prompt());
        assert!(deleting_worktree_paths(&worktree_picker, &mut cx).is_empty());

        cx.simulate_prompt_answer("Cancel");
        cx.run_until_parked();

        assert!(!cx.has_pending_prompt());
        assert!(picker_contains_worktree(
            &worktree_picker,
            &worktree_path,
            &mut cx
        ));
    }

    #[gpui::test]
    async fn test_selected_deleting_worktree_cannot_be_opened(cx: &mut TestAppContext) {
        let (_, worktree_picker, _repository, worktree_path, mut cx) =
            init_worktree_picker_test(cx).await;

        let subscription = cx.update(|_, cx| {
            cx.subscribe(&worktree_picker, |_, _: &DismissEvent, _| {
                panic!("DismissEvent should not be emitted for a deleting worktree");
            })
        });

        let index = worktree_index(&worktree_picker, &worktree_path, &mut cx);
        worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
            worktree_picker.picker.update(cx, |picker, cx| {
                picker.delegate.selected_index = index;
                picker.delegate.delete_worktree(index, false, window, cx);
                picker.delegate.confirm(false, window, cx);
            })
        });

        assert!(deleting_worktree_paths(&worktree_picker, &mut cx).contains(&worktree_path));

        drop(subscription);
        cx.run_until_parked();
    }

    #[gpui::test]
    async fn test_force_delete_worktree_deletes_without_prompt(cx: &mut TestAppContext) {
        let (fs, worktree_picker, repository, worktree_path, mut cx) =
            init_worktree_picker_test(cx).await;

        fs.with_git_state(path!("/root/project/.git").as_ref(), true, |state| {
            state
                .worktrees_requiring_force_delete
                .insert(worktree_path.clone());
        })
        .expect("failed to mark test worktree as requiring force delete");

        let index = worktree_index(&worktree_picker, &worktree_path, &mut cx);
        worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
            worktree_picker.picker.update(cx, |picker, cx| {
                picker.delegate.modifiers = Modifiers::alt();
                picker.delegate.delete_worktree(index, true, window, cx);
            })
        });
        assert!(deleting_worktree_paths(&worktree_picker, &mut cx).contains(&worktree_path));

        cx.run_until_parked();

        assert!(!cx.has_pending_prompt());
        assert!(deleting_worktree_paths(&worktree_picker, &mut cx).is_empty());
        assert!(!picker_contains_worktree(
            &worktree_picker,
            &worktree_path,
            &mut cx
        ));
        assert!(
            !repo_contains_worktree(&repository, &worktree_path, &mut cx).await,
            "worktree should be removed by explicit force delete"
        );
    }

    #[gpui::test]
    async fn test_open_worktrees_are_grouped_under_section_header(cx: &mut TestAppContext) {
        init_test(cx);

        let fs = FakeFs::new(cx.executor());
        fs.insert_tree(
            path!("/root"),
            json!({
                "project": {
                    ".git": {},
                    "file.txt": "buffer_text",
                },
                "worktrees": {},
            }),
        )
        .await;
        fs.set_head_for_repo(
            path!("/root/project/.git").as_ref(),
            &[("file.txt", "buffer_text".to_string())],
            "deadbeef",
        );

        let project = Project::test(fs.clone(), [path!("/root/project").as_ref()], cx).await;
        cx.executor().run_until_parked();

        let repository = project.read_with(cx, |project, cx| {
            project.repositories(cx).values().next().unwrap().clone()
        });
        let second_worktree_path = PathBuf::from(path!("/root/worktrees/second-wt"));

        cx.update(|cx| {
            repository.update(cx, |repository, _| {
                repository.create_worktree(
                    git::repository::CreateWorktreeTarget::NewBranch {
                        branch_name: "second-wt".to_string(),
                        base_sha: Some("deadbeef".to_string()),
                    },
                    second_worktree_path.clone(),
                )
            })
        })
        .await
        .unwrap()
        .unwrap();

        // Open the second worktree as a visible worktree of the active project so
        // that two worktrees of the same repo are open in this window.
        project
            .update(cx, |project, cx| {
                project.create_worktree(&second_worktree_path, true, cx)
            })
            .await
            .unwrap();
        cx.executor().run_until_parked();

        let window_handle =
            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
        let workspace = window_handle
            .read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone())
            .unwrap();

        let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
        let worktree_picker = cx.update(|window, cx| {
            cx.new(|cx| WorktreePicker::new(project, workspace.downgrade(), window, cx))
        });
        cx.run_until_parked();

        let project_path = PathBuf::from(path!("/root/project"));
        worktree_picker.update(&mut cx, |worktree_picker, cx| {
            worktree_picker.picker.update(cx, |picker, _| {
                let matches = &picker.delegate.matches;

                let header_index = matches
                    .iter()
                    .position(|entry| {
                        matches!(entry, WorktreeEntry::SectionHeader(label) if label.as_ref() == "This Window")
                    })
                    .expect("section header should be present when multiple worktrees are open");

                let grouped_paths: Vec<&Path> = matches[header_index + 1..]
                    .iter()
                    .map_while(|entry| match entry {
                        WorktreeEntry::Worktree { worktree, .. } => Some(worktree.path.as_path()),
                        _ => None,
                    })
                    .collect();

                assert!(
                    grouped_paths.contains(&project_path.as_path()),
                    "main worktree should be grouped under the header"
                );
                assert!(
                    grouped_paths.contains(&second_worktree_path.as_path()),
                    "second open worktree should be grouped under the header"
                );
            })
        });
    }

    #[gpui::test]
    async fn test_remove_open_worktree_workspace_from_window(cx: &mut TestAppContext) {
        init_test(cx);

        let fs = FakeFs::new(cx.executor());
        fs.insert_tree(
            path!("/root"),
            json!({
                "project": {
                    ".git": {},
                    "file.txt": "buffer_text",
                },
                "worktrees": {},
            }),
        )
        .await;
        fs.set_head_for_repo(
            path!("/root/project/.git").as_ref(),
            &[("file.txt", "buffer_text".to_string())],
            "deadbeef",
        );

        let project = Project::test(fs.clone(), [path!("/root/project").as_ref()], cx).await;
        cx.executor().run_until_parked();

        let repository = project.read_with(cx, |project, cx| {
            project.repositories(cx).values().next().unwrap().clone()
        });
        let worktree_path = PathBuf::from(path!("/root/worktrees/open-wt"));
        cx.update(|cx| {
            repository.update(cx, |repository, _| {
                repository.create_worktree(
                    git::repository::CreateWorktreeTarget::NewBranch {
                        branch_name: "open-wt".to_string(),
                        base_sha: Some("deadbeef".to_string()),
                    },
                    worktree_path.clone(),
                )
            })
        })
        .await
        .unwrap()
        .unwrap();

        let worktree_project = Project::test(fs.clone(), [worktree_path.as_path()], cx).await;
        cx.executor().run_until_parked();

        let main_group_key = project.read_with(cx, |project, cx| project.project_group_key(cx));
        let worktree_group_key =
            worktree_project.read_with(cx, |project, cx| project.project_group_key(cx));
        assert_eq!(
            main_group_key, worktree_group_key,
            "the worktree workspace should belong to the same project group as the main repo"
        );

        let window_handle =
            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
        let workspace = window_handle
            .read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone())
            .unwrap();
        let worktree_workspace = window_handle
            .update(cx, |multi_workspace, window, cx| {
                let worktree_workspace =
                    cx.new(|cx| Workspace::test_new(worktree_project.clone(), window, cx));
                multi_workspace.add(worktree_workspace.clone(), window, cx);
                worktree_workspace
            })
            .unwrap();

        let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
        let worktree_picker = cx.update(|window, cx| {
            cx.new(|cx| WorktreePicker::new(project, workspace.downgrade(), window, cx))
        });
        cx.run_until_parked();

        worktree_picker.update(&mut cx, |worktree_picker, cx| {
            worktree_picker.picker.update(cx, |picker, _| {
                assert!(
                    picker
                        .delegate
                        .project_worktree_paths
                        .contains(&worktree_path),
                    "the worktree should be considered open in this window"
                );
            })
        });

        worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
            worktree_picker.picker.update(cx, |picker, cx| {
                picker
                    .delegate
                    .remove_worktree_from_window(&worktree_path, window, cx);
            })
        });
        cx.run_until_parked();

        window_handle
            .read_with(&cx, |multi_workspace, _| {
                assert!(
                    multi_workspace
                        .workspaces()
                        .all(|workspace| *workspace != worktree_workspace),
                    "the worktree workspace should be removed from the window"
                );
            })
            .unwrap();

        worktree_picker.update(&mut cx, |worktree_picker, cx| {
            worktree_picker.picker.update(cx, |picker, _| {
                assert!(
                    !picker
                        .delegate
                        .project_worktree_paths
                        .contains(&worktree_path),
                    "the worktree should no longer be considered open in this window"
                );
            })
        });

        assert!(
            repo_contains_worktree(&repository, &worktree_path, &mut cx).await,
            "removing the worktree from the window should not delete the git worktree"
        );
    }
}
