use super::{HoverTarget, HoveredWord, TerminalView};
use anyhow::{Context as _, Result};
use editor::Editor;
use gpui::{App, AppContext, Context, Task, TaskExt, WeakEntity, Window};
use itertools::Itertools;
use project::{Entry, Metadata};
use std::path::PathBuf;
use terminal::PathLikeTarget;
use util::{
    ResultExt, debug_panic,
    paths::{PathStyle, PathWithPosition, normalize_lexically},
    rel_path::RelPath,
};
use workspace::{OpenOptions, OpenVisible, Workspace};

/// The way we found the open target. This is important to have for test assertions.
/// For example, remote projects never look in the file system.
#[cfg(test)]
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum OpenTargetFoundBy {
    WorktreeExact,
    WorktreeScan,
    FileSystemBackground,
}

#[cfg(test)]
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum BackgroundFsChecks {
    Enabled,
    Disabled,
}

#[derive(Debug, Clone)]
enum OpenTarget {
    Worktree(PathWithPosition, Entry, #[cfg(test)] OpenTargetFoundBy),
    File(PathWithPosition, Metadata),
}

impl OpenTarget {
    fn is_file(&self) -> bool {
        match self {
            OpenTarget::Worktree(_, entry, ..) => entry.is_file(),
            OpenTarget::File(_, metadata) => !metadata.is_dir,
        }
    }

    fn is_dir(&self) -> bool {
        match self {
            OpenTarget::Worktree(_, entry, ..) => entry.is_dir(),
            OpenTarget::File(_, metadata) => metadata.is_dir,
        }
    }

    fn path(&self) -> &PathWithPosition {
        match self {
            OpenTarget::Worktree(path, ..) => path,
            OpenTarget::File(path, _) => path,
        }
    }

    #[cfg(test)]
    fn found_by(&self) -> OpenTargetFoundBy {
        match self {
            OpenTarget::Worktree(.., found_by) => *found_by,
            OpenTarget::File(..) => OpenTargetFoundBy::FileSystemBackground,
        }
    }
}

pub(super) fn hover_path_like_target(
    workspace: &WeakEntity<Workspace>,
    hovered_word: HoveredWord,
    path_like_target: &PathLikeTarget,
    cx: &mut Context<TerminalView>,
) -> Task<()> {
    #[cfg(not(test))]
    {
        possible_hover_target(workspace, hovered_word, path_like_target, cx)
    }
    #[cfg(test)]
    {
        possible_hover_target(
            workspace,
            hovered_word,
            path_like_target,
            cx,
            BackgroundFsChecks::Enabled,
        )
    }
}

fn possible_hover_target(
    workspace: &WeakEntity<Workspace>,
    hovered_word: HoveredWord,
    path_like_target: &PathLikeTarget,
    cx: &mut Context<TerminalView>,
    #[cfg(test)] background_fs_checks: BackgroundFsChecks,
) -> Task<()> {
    let file_to_open_task = possible_open_target(
        workspace,
        path_like_target,
        cx,
        #[cfg(test)]
        background_fs_checks,
    );
    cx.spawn(async move |terminal_view, cx| {
        let file_to_open = file_to_open_task.await;
        terminal_view
            .update(cx, |terminal_view, _| match file_to_open {
                Some(OpenTarget::File(path, _) | OpenTarget::Worktree(path, ..)) => {
                    terminal_view.hover = Some(HoverTarget {
                        tooltip: path
                            .to_string(&|path: &PathBuf| path.to_string_lossy().into_owned()),
                        hovered_word,
                    });
                }
                None => {
                    terminal_view.hover = None;
                }
            })
            .ok();
    })
}

fn possible_open_target(
    workspace: &WeakEntity<Workspace>,
    path_like_target: &PathLikeTarget,
    cx: &App,
    #[cfg(test)] background_fs_checks: BackgroundFsChecks,
) -> Task<Option<OpenTarget>> {
    let Some(workspace) = workspace.upgrade() else {
        return Task::ready(None);
    };
    // We have to check for both paths, as on Unix, certain paths with positions are valid file paths too.
    // We can be on FS remote part, without real FS, so cannot canonicalize or check for existence the path right away.
    let mut potential_paths = Vec::new();
    let cwd = path_like_target.terminal_dir.as_ref();
    let maybe_path = &path_like_target.maybe_path;
    let original_path = PathWithPosition::from_path(PathBuf::from(maybe_path));
    let path_with_position = PathWithPosition::parse_str(maybe_path);
    let worktree_candidates = workspace
        .read(cx)
        .worktrees(cx)
        .sorted_by_key(|worktree| {
            let worktree_root = worktree.read(cx).abs_path();
            match cwd.and_then(|cwd| worktree_root.strip_prefix(cwd).ok()) {
                Some(cwd_child) => cwd_child.components().count(),
                None => usize::MAX,
            }
        })
        .collect::<Vec<_>>();
    // Since we do not check paths via FS and joining, we need to strip off potential `./`, `a/`, `b/` prefixes out of it.
    const GIT_DIFF_PATH_PREFIXES: &[&str] = &["a", "b"];
    for prefix_str in GIT_DIFF_PATH_PREFIXES.iter().chain(std::iter::once(&".")) {
        if let Some(stripped) = original_path.path.strip_prefix(prefix_str).ok() {
            potential_paths.push(PathWithPosition {
                path: stripped.to_owned(),
                row: original_path.row,
                column: original_path.column,
            });
        }
        if let Some(stripped) = path_with_position.path.strip_prefix(prefix_str).ok() {
            potential_paths.push(PathWithPosition {
                path: stripped.to_owned(),
                row: path_with_position.row,
                column: path_with_position.column,
            });
        }
    }

    let insert_both_paths = original_path != path_with_position;
    potential_paths.insert(0, original_path);
    if insert_both_paths {
        potential_paths.insert(1, path_with_position);
    }

    // If we won't find paths "easily", we can traverse the entire worktree to look what ends with the potential path suffix.
    // That will be slow, though, so do the fast checks first.
    let mut worktree_paths_to_check = Vec::new();
    let mut is_cwd_in_worktree = false;
    let mut open_target = None;
    'worktree_loop: for worktree in &worktree_candidates {
        let worktree_root = worktree.read(cx).abs_path();
        let mut paths_to_check = Vec::with_capacity(potential_paths.len());
        let relative_cwd = cwd
            .and_then(|cwd| cwd.strip_prefix(&worktree_root).ok())
            .and_then(|cwd| RelPath::new(cwd, PathStyle::local()).ok())
            .and_then(|cwd_stripped| {
                (cwd_stripped.as_ref() != RelPath::empty()).then(|| {
                    is_cwd_in_worktree = true;
                    cwd_stripped
                })
            });

        for path_with_position in &potential_paths {
            let path_to_check = if worktree_root.ends_with(&path_with_position.path) {
                let root_path_with_position = PathWithPosition {
                    path: worktree_root.to_path_buf(),
                    row: path_with_position.row,
                    column: path_with_position.column,
                };
                match worktree.read(cx).root_entry() {
                    Some(root_entry) => {
                        open_target = Some(OpenTarget::Worktree(
                            root_path_with_position,
                            root_entry.clone(),
                            #[cfg(test)]
                            OpenTargetFoundBy::WorktreeExact,
                        ));
                        break 'worktree_loop;
                    }
                    None => root_path_with_position,
                }
            } else {
                PathWithPosition {
                    path: path_with_position
                        .path
                        .strip_prefix(&worktree_root)
                        .unwrap_or(&path_with_position.path)
                        .to_owned(),
                    row: path_with_position.row,
                    column: path_with_position.column,
                }
            };

            // Normalize the path by joining with cwd if available (handles `.` and `..` segments)
            let normalized_path = if path_to_check.path.is_relative() {
                relative_cwd.as_ref().and_then(|relative_cwd| {
                    let joined = relative_cwd
                        .as_ref()
                        .as_std_path()
                        .join(&path_to_check.path);
                    normalize_lexically(&joined).ok().and_then(|p| {
                        RelPath::new(&p, PathStyle::local())
                            .ok()
                            .map(std::borrow::Cow::into_owned)
                    })
                })
            } else {
                None
            };
            let original_path = RelPath::new(&path_to_check.path, PathStyle::local()).ok();

            if !worktree.read(cx).is_single_file()
                && let Some(entry) = normalized_path
                    .as_ref()
                    .and_then(|p| worktree.read(cx).entry_for_path(p))
                    .or_else(|| {
                        original_path
                            .as_ref()
                            .and_then(|p| worktree.read(cx).entry_for_path(p.as_ref()))
                    })
            {
                open_target = Some(OpenTarget::Worktree(
                    PathWithPosition {
                        path: worktree.read(cx).absolutize(&entry.path),
                        row: path_to_check.row,
                        column: path_to_check.column,
                    },
                    entry.clone(),
                    #[cfg(test)]
                    OpenTargetFoundBy::WorktreeExact,
                ));
                break 'worktree_loop;
            }

            paths_to_check.push(path_to_check);
        }

        if !paths_to_check.is_empty() {
            worktree_paths_to_check.push((worktree.clone(), paths_to_check));
        }
    }

    #[cfg(not(test))]
    let enable_background_fs_checks = workspace.read(cx).project().read(cx).is_local();
    #[cfg(test)]
    let enable_background_fs_checks = background_fs_checks == BackgroundFsChecks::Enabled;

    if open_target.is_some() {
        // We we want to prefer open targets found via background fs checks over worktree matches,
        // however we can return early if either:
        //   - This is a remote project, or
        //   - If the terminal working directory is inside of at least one worktree
        if !enable_background_fs_checks || is_cwd_in_worktree {
            return Task::ready(open_target);
        }
    }

    // Before entire worktree traversal(s), make an attempt to do FS checks if available.
    let fs_paths_to_check =
        if enable_background_fs_checks {
            let fs_cwd_paths_to_check = cwd
                .iter()
                .flat_map(|cwd| {
                    let mut paths_to_check = Vec::new();
                    for path_to_check in &potential_paths {
                        let maybe_path = &path_to_check.path;
                        if path_to_check.path.is_relative() {
                            paths_to_check.push(PathWithPosition {
                                path: cwd.join(&maybe_path),
                                row: path_to_check.row,
                                column: path_to_check.column,
                            });
                        }
                    }
                    paths_to_check
                })
                .collect::<Vec<_>>();
            fs_cwd_paths_to_check
                .into_iter()
                .chain(
                    potential_paths
                        .into_iter()
                        .flat_map(|path_to_check| {
                            let mut paths_to_check = Vec::new();
                            let maybe_path = &path_to_check.path;
                            if maybe_path.starts_with("~") {
                                if let Some(home_path) = maybe_path.strip_prefix("~").ok().and_then(
                                    |stripped_maybe_path| {
                                        Some(dirs::home_dir()?.join(stripped_maybe_path))
                                    },
                                ) {
                                    paths_to_check.push(PathWithPosition {
                                        path: home_path,
                                        row: path_to_check.row,
                                        column: path_to_check.column,
                                    });
                                }
                            } else {
                                paths_to_check.push(PathWithPosition {
                                    path: maybe_path.clone(),
                                    row: path_to_check.row,
                                    column: path_to_check.column,
                                });
                                if maybe_path.is_relative() {
                                    for worktree in &worktree_candidates {
                                        if !worktree.read(cx).is_single_file() {
                                            paths_to_check.push(PathWithPosition {
                                                path: worktree.read(cx).abs_path().join(maybe_path),
                                                row: path_to_check.row,
                                                column: path_to_check.column,
                                            });
                                        }
                                    }
                                }
                            }
                            paths_to_check
                        })
                        .collect::<Vec<_>>(),
                )
                .collect()
        } else {
            Vec::new()
        };

    let fs = workspace.read(cx).project().read(cx).fs().clone();
    let background_fs_checks_task = cx.background_spawn(async move {
        for mut path_to_check in fs_paths_to_check {
            if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok()
                && let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten()
            {
                if open_target
                    .as_ref()
                    .map(|open_target| open_target.path().path != fs_path_to_check)
                    .unwrap_or(true)
                {
                    path_to_check.path = fs_path_to_check;
                    return Some(OpenTarget::File(path_to_check, metadata));
                }

                break;
            }
        }

        open_target
    });

    cx.spawn(async move |cx| {
        background_fs_checks_task.await.or_else(|| {
            for (worktree, worktree_paths_to_check) in worktree_paths_to_check {
                if let Some(found_entry) =
                    worktree.update(cx, |worktree, _| -> Option<OpenTarget> {
                        let traversal =
                            worktree.traverse_from_path(true, true, false, RelPath::empty());
                        for entry in traversal {
                            if let Some(path_in_worktree) =
                                worktree_paths_to_check.iter().find(|path_to_check| {
                                    RelPath::new(&path_to_check.path, PathStyle::local())
                                        .is_ok_and(|path| entry.path.ends_with(&path))
                                })
                            {
                                return Some(OpenTarget::Worktree(
                                    PathWithPosition {
                                        path: worktree.absolutize(&entry.path),
                                        row: path_in_worktree.row,
                                        column: path_in_worktree.column,
                                    },
                                    entry.clone(),
                                    #[cfg(test)]
                                    OpenTargetFoundBy::WorktreeScan,
                                ));
                            }
                        }
                        None
                    })
                {
                    return Some(found_entry);
                }
            }
            None
        })
    })
}

pub(super) fn open_path_like_target(
    workspace: &WeakEntity<Workspace>,
    terminal_view: &mut TerminalView,
    path_like_target: &PathLikeTarget,
    window: &mut Window,
    cx: &mut Context<TerminalView>,
) {
    #[cfg(not(test))]
    {
        possibly_open_target(workspace, terminal_view, path_like_target, window, cx)
            .detach_and_log_err(cx)
    }
    #[cfg(test)]
    {
        possibly_open_target(
            workspace,
            terminal_view,
            path_like_target,
            window,
            cx,
            BackgroundFsChecks::Enabled,
        )
        .detach_and_log_err(cx)
    }
}

fn possibly_open_target(
    workspace: &WeakEntity<Workspace>,
    terminal_view: &mut TerminalView,
    path_like_target: &PathLikeTarget,
    window: &mut Window,
    cx: &mut Context<TerminalView>,
    #[cfg(test)] background_fs_checks: BackgroundFsChecks,
) -> Task<Result<Option<OpenTarget>>> {
    if terminal_view.hover.is_none() {
        return Task::ready(Ok(None));
    }
    let workspace = workspace.clone();
    let path_like_target = path_like_target.clone();
    cx.spawn_in(window, async move |terminal_view, cx| {
        let Some(open_target) = terminal_view
            .update(cx, |_, cx| {
                possible_open_target(
                    &workspace,
                    &path_like_target,
                    cx,
                    #[cfg(test)]
                    background_fs_checks,
                )
            })?
            .await
        else {
            return Ok(None);
        };

        let path_to_open = open_target.path();
        let opened_items = workspace
            .update_in(cx, |workspace, window, cx| {
                workspace.open_paths(
                    vec![path_to_open.path.clone()],
                    OpenOptions {
                        visible: Some(OpenVisible::OnlyDirectories),
                        ..Default::default()
                    },
                    None,
                    window,
                    cx,
                )
            })
            .context("workspace update")?
            .await;
        if opened_items.len() != 1 {
            debug_panic!(
                "Received {} items for one path {path_to_open:?}",
                opened_items.len(),
            );
        }

        if let Some(opened_item) = opened_items.first() {
            if open_target.is_file() {
                if let Some(Ok(opened_item)) = opened_item {
                    if let Some(row) = path_to_open.row {
                        let col = path_to_open.column.unwrap_or(0);
                        if let Some(active_editor) = opened_item.downcast::<Editor>() {
                            active_editor
                                .downgrade()
                                .update_in(cx, |editor, window, cx| {
                                    editor.go_to_singleton_buffer_point(
                                        language::Point::new(
                                            row.saturating_sub(1),
                                            col.saturating_sub(1),
                                        ),
                                        window,
                                        cx,
                                    )
                                })
                                .log_err();
                        }
                    }
                    return Ok(Some(open_target));
                }
            } else if open_target.is_dir() {
                workspace.update(cx, |workspace, cx| {
                    workspace.project().update(cx, |_, cx| {
                        cx.emit(project::Event::ActivateProjectPanel);
                    })
                })?;
                return Ok(Some(open_target));
            }
        }
        Ok(None)
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use gpui::TestAppContext;
    use project::Project;
    use serde_json::json;
    use std::path::{Path, PathBuf};
    use terminal::{
        HoveredWord, TerminalBuilder,
        alacritty_terminal::index::Point as AlacPoint,
        terminal_settings::{AlternateScroll, CursorShape},
    };
    use util::path;
    use workspace::{AppState, MultiWorkspace};

    async fn init_test(
        app_cx: &mut TestAppContext,
        trees: impl IntoIterator<Item = (&str, serde_json::Value)>,
        worktree_roots: impl IntoIterator<Item = &str>,
    ) -> impl AsyncFnMut(
        HoveredWord,
        PathLikeTarget,
        BackgroundFsChecks,
    ) -> (Option<HoverTarget>, Option<OpenTarget>) {
        let fs = app_cx.update(AppState::test).fs.as_fake().clone();

        app_cx.update(|cx| {
            theme_settings::init(theme::LoadThemes::JustBase, cx);
            editor::init(cx);
        });

        for (path, tree) in trees {
            fs.insert_tree(path, tree).await;
        }

        let project: gpui::Entity<Project> = Project::test(
            fs.clone(),
            worktree_roots.into_iter().map(Path::new),
            app_cx,
        )
        .await;

        let (multi_workspace, cx) = app_cx
            .add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());

        let terminal = app_cx.new(|cx| {
            TerminalBuilder::new_display_only(
                CursorShape::default(),
                AlternateScroll::On,
                None,
                0,
                cx.background_executor(),
                PathStyle::local(),
            )
            .expect("Failed to create display-only terminal")
            .subscribe(cx)
        });

        let workspace_a = workspace.clone();
        let (terminal_view, cx) = app_cx.add_window_view(|window, cx| {
            TerminalView::new(
                terminal,
                workspace_a.downgrade(),
                None,
                project.downgrade(),
                window,
                cx,
            )
        });

        async move |hovered_word: HoveredWord,
                    path_like_target: PathLikeTarget,
                    background_fs_checks: BackgroundFsChecks|
                    -> (Option<HoverTarget>, Option<OpenTarget>) {
            let workspace_a = workspace.clone();
            terminal_view
                .update(cx, |_, cx| {
                    possible_hover_target(
                        &workspace_a.downgrade(),
                        hovered_word,
                        &path_like_target,
                        cx,
                        background_fs_checks,
                    )
                })
                .await;

            let hover_target =
                terminal_view.read_with(cx, |terminal_view, _| terminal_view.hover.clone());

            let open_target = terminal_view
                .update_in(cx, |terminal_view, window, cx| {
                    possibly_open_target(
                        &workspace.downgrade(),
                        terminal_view,
                        &path_like_target,
                        window,
                        cx,
                        background_fs_checks,
                    )
                })
                .await
                .expect("Failed to possibly open target");

            (hover_target, open_target)
        }
    }

    async fn test_path_like_simple(
        test_path_like: &mut impl AsyncFnMut(
            HoveredWord,
            PathLikeTarget,
            BackgroundFsChecks,
        ) -> (Option<HoverTarget>, Option<OpenTarget>),
        maybe_path: &str,
        tooltip: &str,
        terminal_dir: Option<PathBuf>,
        background_fs_checks: BackgroundFsChecks,
        mut open_target_found_by: OpenTargetFoundBy,
        file: &str,
        line: u32,
    ) {
        let (hover_target, open_target) = test_path_like(
            HoveredWord {
                word: maybe_path.to_string(),
                word_match: AlacPoint::default()..=AlacPoint::default(),
                id: 0,
            },
            PathLikeTarget {
                maybe_path: maybe_path.to_string(),
                terminal_dir,
            },
            background_fs_checks,
        )
        .await;

        let Some(hover_target) = hover_target else {
            assert!(
                hover_target.is_some(),
                "Hover target should not be `None` at {file}:{line}:"
            );
            return;
        };

        assert_eq!(
            hover_target.tooltip, tooltip,
            "Tooltip mismatch at {file}:{line}:"
        );
        assert_eq!(
            hover_target.hovered_word.word, maybe_path,
            "Hovered word mismatch at {file}:{line}:"
        );

        let Some(open_target) = open_target else {
            assert!(
                open_target.is_some(),
                "Open target should not be `None` at {file}:{line}:"
            );
            return;
        };

        assert_eq!(
            open_target.path().path,
            Path::new(tooltip),
            "Open target path mismatch at {file}:{line}:"
        );

        if background_fs_checks == BackgroundFsChecks::Disabled
            && open_target_found_by == OpenTargetFoundBy::FileSystemBackground
        {
            open_target_found_by = OpenTargetFoundBy::WorktreeScan;
        }

        assert_eq!(
            open_target.found_by(),
            open_target_found_by,
            "Open target found by mismatch at {file}:{line}:"
        );
    }

    macro_rules! none_or_some_pathbuf {
        (None) => {
            None
        };
        ($cwd:literal) => {
            Some($crate::PathBuf::from(path!($cwd)))
        };
    }

    macro_rules! test_path_like {
        (
            $test_path_like:expr,
            $maybe_path:literal,
            $tooltip:literal,
            $cwd:tt,
            $found_by:expr
        ) => {{
            test_path_like!(
                $test_path_like,
                $maybe_path,
                $tooltip,
                $cwd,
                BackgroundFsChecks::Enabled,
                $found_by
            );
            test_path_like!(
                $test_path_like,
                $maybe_path,
                $tooltip,
                $cwd,
                BackgroundFsChecks::Disabled,
                $found_by
            );
        }};

        (
            $test_path_like:expr,
            $maybe_path:literal,
            $tooltip:literal,
            $cwd:tt,
            $background_fs_checks:path,
            $found_by:expr
        ) => {
            test_path_like_simple(
                &mut $test_path_like,
                path!($maybe_path),
                path!($tooltip),
                none_or_some_pathbuf!($cwd),
                $background_fs_checks,
                $found_by,
                std::file!(),
                std::line!(),
            )
            .await
        };
    }

    // Note the arms of `test`, `test_local`, and `test_remote` should be collapsed once macro
    // metavariable expressions (#![feature(macro_metavar_expr)]) are stabilized.
    // See https://github.com/rust-lang/rust/issues/83527
    #[doc = "test_path_likes!(<cx>, <trees>, <worktrees>, { $(<tests>;)+ })"]
    macro_rules! test_path_likes {
        ($cx:expr, $trees:expr, $worktrees:expr, { $($tests:expr;)+ }) => { {
            let mut test_path_like = init_test($cx, $trees, $worktrees).await;
            #[doc ="test!(<hovered maybe_path>, <expected tooltip>, <terminal cwd> "]
            #[doc ="\\[, found by \\])"]
            #[allow(unused_macros)]
            macro_rules! test {
                ($maybe_path:literal, $tooltip:literal, $cwd:tt) => {
                    test_path_like!(
                        test_path_like,
                        $maybe_path,
                        $tooltip,
                        $cwd,
                        OpenTargetFoundBy::WorktreeExact
                    )
                };
                ($maybe_path:literal, $tooltip:literal, $cwd:tt, $found_by:ident) => {
                    test_path_like!(
                        test_path_like,
                        $maybe_path,
                        $tooltip,
                        $cwd,
                        OpenTargetFoundBy::$found_by
                    )
                }
            }
            #[doc ="test_local!(<hovered maybe_path>, <expected tooltip>, <terminal cwd> "]
            #[doc ="\\[, found by \\])"]
            #[allow(unused_macros)]
            macro_rules! test_local {
                ($maybe_path:literal, $tooltip:literal, $cwd:tt) => {
                    test_path_like!(
                        test_path_like,
                        $maybe_path,
                        $tooltip,
                        $cwd,
                        BackgroundFsChecks::Enabled,
                        OpenTargetFoundBy::WorktreeExact
                    )
                };
                ($maybe_path:literal, $tooltip:literal, $cwd:tt, $found_by:ident) => {
                    test_path_like!(
                        test_path_like,
                        $maybe_path,
                        $tooltip,
                        $cwd,
                        BackgroundFsChecks::Enabled,
                        OpenTargetFoundBy::$found_by
                    )
                }
            }
            #[doc ="test_remote!(<hovered maybe_path>, <expected tooltip>, <terminal cwd> "]
            #[doc ="\\[, found by \\])"]
            #[allow(unused_macros)]
            macro_rules! test_remote {
                ($maybe_path:literal, $tooltip:literal, $cwd:tt) => {
                    test_path_like!(
                        test_path_like,
                        $maybe_path,
                        $tooltip,
                        $cwd,
                        BackgroundFsChecks::Disabled,
                        OpenTargetFoundBy::WorktreeExact
                    )
                };
                ($maybe_path:literal, $tooltip:literal, $cwd:tt, $found_by:ident) => {
                    test_path_like!(
                        test_path_like,
                        $maybe_path,
                        $tooltip,
                        $cwd,
                        BackgroundFsChecks::Disabled,
                        OpenTargetFoundBy::$found_by
                    )
                }
            }
            $($tests);+
        } }
    }

    #[gpui::test]
    async fn one_folder_worktree(cx: &mut TestAppContext) {
        test_path_likes!(
            cx,
            vec![(
                path!("/test"),
                json!({
                    "lib.rs": "",
                    "test.rs": "",
                }),
            )],
            vec![path!("/test")],
            {
                test!("lib.rs", "/test/lib.rs", None);
                test!("/test/lib.rs", "/test/lib.rs", None);
                test!("test.rs", "/test/test.rs", None);
                test!("/test/test.rs", "/test/test.rs", None);
            }
        )
    }

    #[gpui::test]
    async fn mixed_worktrees(cx: &mut TestAppContext) {
        test_path_likes!(
            cx,
            vec![
                (
                    path!("/"),
                    json!({
                        "file.txt": "",
                    }),
                ),
                (
                    path!("/test"),
                    json!({
                        "lib.rs": "",
                        "test.rs": "",
                        "file.txt": "",
                    }),
                ),
            ],
            vec![path!("/file.txt"), path!("/test")],
            {
                test!("file.txt", "/file.txt", "/");
                test!("/file.txt", "/file.txt", "/");

                test!("lib.rs", "/test/lib.rs", "/test");
                test!("test.rs", "/test/test.rs", "/test");
                test!("file.txt", "/test/file.txt", "/test");

                test!("/test/lib.rs", "/test/lib.rs", "/test");
                test!("/test/test.rs", "/test/test.rs", "/test");
                test!("/test/file.txt", "/test/file.txt", "/test");
            }
        )
    }

    #[gpui::test]
    async fn worktree_file_preferred(cx: &mut TestAppContext) {
        test_path_likes!(
            cx,
            vec![
                (
                    path!("/"),
                    json!({
                        "file.txt": "",
                    }),
                ),
                (
                    path!("/test"),
                    json!({
                        "file.txt": "",
                    }),
                ),
            ],
            vec![path!("/test")],
            {
                test!("file.txt", "/test/file.txt", "/test");
            }
        )
    }

    mod issues {
        use super::*;

        // https://github.com/zed-industries/zed/issues/28407
        #[gpui::test]
        async fn issue_28407_siblings(cx: &mut TestAppContext) {
            test_path_likes!(
                cx,
                vec![(
                    path!("/dir1"),
                    json!({
                        "dir 2": {
                            "C.py": ""
                        },
                        "dir 3": {
                            "C.py": ""
                        },
                    }),
                )],
                vec![path!("/dir1")],
                {
                    test!("C.py", "/dir1/dir 2/C.py", "/dir1", WorktreeScan);
                    test!("C.py", "/dir1/dir 2/C.py", "/dir1/dir 2");
                    test!("C.py", "/dir1/dir 3/C.py", "/dir1/dir 3");
                }
            )
        }

        // https://github.com/zed-industries/zed/issues/28407
        // See https://github.com/zed-industries/zed/issues/34027
        // See https://github.com/zed-industries/zed/issues/33498
        #[gpui::test]
        async fn issue_28407_nesting(cx: &mut TestAppContext) {
            test_path_likes!(
                cx,
                vec![(
                    path!("/project"),
                    json!({
                        "lib": {
                            "src": {
                                "main.rs": "",
                                "only_in_lib.rs": ""
                            },
                        },
                        "src": {
                            "main.rs": ""
                        },
                    }),
                )],
                vec![path!("/project")],
                {
                    test!("main.rs", "/project/src/main.rs", "/project/src");
                    test!("main.rs", "/project/lib/src/main.rs", "/project/lib/src");

                    test!("src/main.rs", "/project/src/main.rs", "/project");
                    test!("src/main.rs", "/project/src/main.rs", "/project/src");
                    test!("src/main.rs", "/project/lib/src/main.rs", "/project/lib");

                    test!("lib/src/main.rs", "/project/lib/src/main.rs", "/project");
                    test!(
                        "lib/src/main.rs",
                        "/project/lib/src/main.rs",
                        "/project/src"
                    );
                    test!(
                        "lib/src/main.rs",
                        "/project/lib/src/main.rs",
                        "/project/lib"
                    );
                    test!(
                        "lib/src/main.rs",
                        "/project/lib/src/main.rs",
                        "/project/lib/src"
                    );
                    test!(
                        "src/only_in_lib.rs",
                        "/project/lib/src/only_in_lib.rs",
                        "/project/lib/src",
                        WorktreeScan
                    );
                }
            )
        }

        // https://github.com/zed-industries/zed/issues/28339
        #[gpui::test]
        async fn issue_28339(cx: &mut TestAppContext) {
            test_path_likes!(
                cx,
                vec![(
                    path!("/tmp"),
                    json!({
                        "issue28339": {
                            "foo": {
                                "bar.txt": ""
                            },
                        },
                    }),
                )],
                vec![path!("/tmp")],
                {
                    test_local!(
                        "foo/./bar.txt",
                        "/tmp/issue28339/foo/bar.txt",
                        "/tmp/issue28339",
                        WorktreeExact
                    );
                    test_local!(
                        "foo/../foo/bar.txt",
                        "/tmp/issue28339/foo/bar.txt",
                        "/tmp/issue28339",
                        WorktreeExact
                    );
                    test_local!(
                        "foo/..///foo/bar.txt",
                        "/tmp/issue28339/foo/bar.txt",
                        "/tmp/issue28339",
                        WorktreeExact
                    );
                    test_local!(
                        "issue28339/../issue28339/foo/../foo/bar.txt",
                        "/tmp/issue28339/foo/bar.txt",
                        "/tmp/issue28339",
                        WorktreeExact
                    );
                    test_local!(
                        "./bar.txt",
                        "/tmp/issue28339/foo/bar.txt",
                        "/tmp/issue28339/foo",
                        WorktreeExact
                    );
                    test_local!(
                        "../foo/bar.txt",
                        "/tmp/issue28339/foo/bar.txt",
                        "/tmp/issue28339/foo",
                        WorktreeExact
                    );
                }
            )
        }

        // https://github.com/zed-industries/zed/issues/28339
        #[gpui::test]
        async fn issue_28339_remote(cx: &mut TestAppContext) {
            test_path_likes!(
                cx,
                vec![(
                    path!("/tmp"),
                    json!({
                        "issue28339": {
                            "foo": {
                                "bar.txt": ""
                            },
                        },
                    }),
                )],
                vec![path!("/tmp")],
                {
                    test_remote!(
                        "foo/./bar.txt",
                        "/tmp/issue28339/foo/bar.txt",
                        "/tmp/issue28339"
                    );
                    test_remote!(
                        "foo/../foo/bar.txt",
                        "/tmp/issue28339/foo/bar.txt",
                        "/tmp/issue28339"
                    );
                    test_remote!(
                        "foo/..///foo/bar.txt",
                        "/tmp/issue28339/foo/bar.txt",
                        "/tmp/issue28339"
                    );
                    test_remote!(
                        "issue28339/../issue28339/foo/../foo/bar.txt",
                        "/tmp/issue28339/foo/bar.txt",
                        "/tmp/issue28339"
                    );
                    test_remote!(
                        "./bar.txt",
                        "/tmp/issue28339/foo/bar.txt",
                        "/tmp/issue28339/foo"
                    );
                    test_remote!(
                        "../foo/bar.txt",
                        "/tmp/issue28339/foo/bar.txt",
                        "/tmp/issue28339/foo"
                    );
                }
            )
        }

        // https://github.com/zed-industries/zed/issues/34027
        #[gpui::test]
        async fn issue_34027(cx: &mut TestAppContext) {
            test_path_likes!(
                cx,
                vec![(
                    path!("/tmp/issue34027"),
                    json!({
                        "test.txt": "",
                        "foo": {
                            "test.txt": "",
                        }
                    }),
                ),],
                vec![path!("/tmp/issue34027")],
                {
                    test!("test.txt", "/tmp/issue34027/test.txt", "/tmp/issue34027");
                    test!(
                        "test.txt",
                        "/tmp/issue34027/foo/test.txt",
                        "/tmp/issue34027/foo"
                    );
                }
            )
        }

        // https://github.com/zed-industries/zed/issues/34027
        #[gpui::test]
        async fn issue_34027_siblings(cx: &mut TestAppContext) {
            test_path_likes!(
                cx,
                vec![(
                    path!("/test"),
                    json!({
                        "sub1": {
                            "file.txt": "",
                        },
                        "sub2": {
                            "file.txt": "",
                        }
                    }),
                ),],
                vec![path!("/test")],
                {
                    test!("file.txt", "/test/sub1/file.txt", "/test/sub1");
                    test!("file.txt", "/test/sub2/file.txt", "/test/sub2");
                    test!("sub1/file.txt", "/test/sub1/file.txt", "/test/sub1");
                    test!("sub2/file.txt", "/test/sub2/file.txt", "/test/sub2");
                    test!("sub1/file.txt", "/test/sub1/file.txt", "/test/sub2");
                    test!("sub2/file.txt", "/test/sub2/file.txt", "/test/sub1");
                }
            )
        }

        // https://github.com/zed-industries/zed/issues/34027
        #[gpui::test]
        async fn issue_34027_nesting(cx: &mut TestAppContext) {
            test_path_likes!(
                cx,
                vec![(
                    path!("/test"),
                    json!({
                        "sub1": {
                            "file.txt": "",
                            "subsub1": {
                                "file.txt": "",
                            }
                        },
                        "sub2": {
                            "file.txt": "",
                            "subsub1": {
                                "file.txt": "",
                            }
                        }
                    }),
                ),],
                vec![path!("/test")],
                {
                    test!(
                        "file.txt",
                        "/test/sub1/subsub1/file.txt",
                        "/test/sub1/subsub1"
                    );
                    test!(
                        "file.txt",
                        "/test/sub2/subsub1/file.txt",
                        "/test/sub2/subsub1"
                    );
                    test!(
                        "subsub1/file.txt",
                        "/test/sub1/subsub1/file.txt",
                        "/test",
                        WorktreeScan
                    );
                    test!(
                        "subsub1/file.txt",
                        "/test/sub1/subsub1/file.txt",
                        "/test",
                        WorktreeScan
                    );
                    test!(
                        "subsub1/file.txt",
                        "/test/sub1/subsub1/file.txt",
                        "/test/sub1"
                    );
                    test!(
                        "subsub1/file.txt",
                        "/test/sub2/subsub1/file.txt",
                        "/test/sub2"
                    );
                    test!(
                        "subsub1/file.txt",
                        "/test/sub1/subsub1/file.txt",
                        "/test/sub1/subsub1",
                        WorktreeScan
                    );
                }
            )
        }

        // https://github.com/zed-industries/zed/issues/34027
        #[gpui::test]
        async fn issue_34027_non_worktree_local_file(cx: &mut TestAppContext) {
            test_path_likes!(
                cx,
                vec![
                    (
                        path!("/"),
                        json!({
                            "file.txt": "",
                        }),
                    ),
                    (
                        path!("/test"),
                        json!({
                            "file.txt": "",
                        }),
                    ),
                ],
                vec![path!("/test")],
                {
                    // Note: Opening a non-worktree file adds that file as a single file worktree.
                    test_local!("file.txt", "/file.txt", "/", FileSystemBackground);
                }
            )
        }

        // https://github.com/zed-industries/zed/issues/34027
        #[gpui::test]
        async fn issue_34027_non_worktree_remote_file(cx: &mut TestAppContext) {
            test_path_likes!(
                cx,
                vec![
                    (
                        path!("/"),
                        json!({
                            "file.txt": "",
                        }),
                    ),
                    (
                        path!("/test"),
                        json!({
                            "file.txt": "",
                        }),
                    ),
                ],
                vec![path!("/test")],
                {
                    // Note: Opening a non-worktree file adds that file as a single file worktree.
                    test_remote!("file.txt", "/test/file.txt", "/");
                    test_remote!("/test/file.txt", "/test/file.txt", "/");
                }
            )
        }

        // See https://github.com/zed-industries/zed/issues/34027
        #[gpui::test]
        #[should_panic(expected = "Tooltip mismatch")]
        async fn issue_34027_gaps(cx: &mut TestAppContext) {
            test_path_likes!(
                cx,
                vec![(
                    path!("/project"),
                    json!({
                        "lib": {
                            "src": {
                                "main.rs": ""
                            },
                        },
                        "src": {
                            "main.rs": ""
                        },
                    }),
                )],
                vec![path!("/project")],
                {
                    test!("main.rs", "/project/src/main.rs", "/project");
                    test!("main.rs", "/project/lib/src/main.rs", "/project/lib");
                }
            )
        }

        // See https://github.com/zed-industries/zed/issues/34027
        #[gpui::test]
        #[should_panic(expected = "Tooltip mismatch")]
        async fn issue_34027_overlap(cx: &mut TestAppContext) {
            test_path_likes!(
                cx,
                vec![(
                    path!("/project"),
                    json!({
                        "lib": {
                            "src": {
                                "main.rs": ""
                            },
                        },
                        "src": {
                            "main.rs": ""
                        },
                    }),
                )],
                vec![path!("/project")],
                {
                    // Finds "/project/src/main.rs"
                    test!(
                        "src/main.rs",
                        "/project/lib/src/main.rs",
                        "/project/lib/src"
                    );
                }
            )
        }
    }
}
