use super::{RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
use crate::{db::UserId, tests::run_randomized_test};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use call::ActiveCall;
use collections::{BTreeMap, HashMap};
use editor::Bias;
use fs::{repository::GitFileStatus, FakeFs, Fs as _};
use futures::StreamExt;
use gpui::{BackgroundExecutor, Model, TestAppContext};
use language::{
    range_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, PointUtf16,
};
use lsp::FakeLanguageServer;
use pretty_assertions::assert_eq;
use project::{search::SearchQuery, Project, ProjectPath, SearchResult};
use rand::{
    distributions::{Alphanumeric, DistString},
    prelude::*,
};
use serde::{Deserialize, Serialize};
use std::{
    ops::{Deref, Range},
    path::{Path, PathBuf},
    rc::Rc,
    sync::Arc,
};
use util::ResultExt;

#[gpui::test(
    iterations = 100,
    on_failure = "crate::tests::save_randomized_test_plan"
)]
async fn test_random_project_collaboration(
    cx: &mut TestAppContext,
    executor: BackgroundExecutor,
    rng: StdRng,
) {
    run_randomized_test::<ProjectCollaborationTest>(cx, executor, rng).await;
}

#[derive(Clone, Debug, Serialize, Deserialize)]
enum ClientOperation {
    AcceptIncomingCall,
    RejectIncomingCall,
    LeaveCall,
    InviteContactToCall {
        user_id: UserId,
    },
    OpenLocalProject {
        first_root_name: String,
    },
    OpenRemoteProject {
        host_id: UserId,
        first_root_name: String,
    },
    AddWorktreeToProject {
        project_root_name: String,
        new_root_path: PathBuf,
    },
    CloseRemoteProject {
        project_root_name: String,
    },
    OpenBuffer {
        project_root_name: String,
        is_local: bool,
        full_path: PathBuf,
    },
    SearchProject {
        project_root_name: String,
        is_local: bool,
        query: String,
        detach: bool,
    },
    EditBuffer {
        project_root_name: String,
        is_local: bool,
        full_path: PathBuf,
        edits: Vec<(Range<usize>, Arc<str>)>,
    },
    CloseBuffer {
        project_root_name: String,
        is_local: bool,
        full_path: PathBuf,
    },
    SaveBuffer {
        project_root_name: String,
        is_local: bool,
        full_path: PathBuf,
        detach: bool,
    },
    RequestLspDataInBuffer {
        project_root_name: String,
        is_local: bool,
        full_path: PathBuf,
        offset: usize,
        kind: LspRequestKind,
        detach: bool,
    },
    CreateWorktreeEntry {
        project_root_name: String,
        is_local: bool,
        full_path: PathBuf,
        is_dir: bool,
    },
    WriteFsEntry {
        path: PathBuf,
        is_dir: bool,
        content: String,
    },
    GitOperation {
        operation: GitOperation,
    },
}

#[derive(Clone, Debug, Serialize, Deserialize)]
enum GitOperation {
    WriteGitIndex {
        repo_path: PathBuf,
        contents: Vec<(PathBuf, String)>,
    },
    WriteGitBranch {
        repo_path: PathBuf,
        new_branch: Option<String>,
    },
    WriteGitStatuses {
        repo_path: PathBuf,
        statuses: Vec<(PathBuf, GitFileStatus)>,
        git_operation: bool,
    },
}

#[derive(Clone, Debug, Serialize, Deserialize)]
enum LspRequestKind {
    Rename,
    Completion,
    CodeAction,
    Definition,
    Highlights,
}

struct ProjectCollaborationTest;

#[async_trait(?Send)]
impl RandomizedTest for ProjectCollaborationTest {
    type Operation = ClientOperation;

    async fn initialize(server: &mut TestServer, users: &[UserTestPlan]) {
        let db = &server.app_state.db;
        for (ix, user_a) in users.iter().enumerate() {
            for user_b in &users[ix + 1..] {
                db.send_contact_request(user_a.user_id, user_b.user_id)
                    .await
                    .unwrap();
                db.respond_to_contact_request(user_b.user_id, user_a.user_id, true)
                    .await
                    .unwrap();
            }
        }
    }

    fn generate_operation(
        client: &TestClient,
        rng: &mut StdRng,
        plan: &mut UserTestPlan,
        cx: &TestAppContext,
    ) -> ClientOperation {
        let call = cx.read(ActiveCall::global);
        loop {
            match rng.gen_range(0..100_u32) {
                // Mutate the call
                0..=29 => {
                    // Respond to an incoming call
                    if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) {
                        break if rng.gen_bool(0.7) {
                            ClientOperation::AcceptIncomingCall
                        } else {
                            ClientOperation::RejectIncomingCall
                        };
                    }

                    match rng.gen_range(0..100_u32) {
                        // Invite a contact to the current call
                        0..=70 => {
                            let available_contacts =
                                client.user_store().read_with(cx, |user_store, _| {
                                    user_store
                                        .contacts()
                                        .iter()
                                        .filter(|contact| contact.online && !contact.busy)
                                        .cloned()
                                        .collect::<Vec<_>>()
                                });
                            if !available_contacts.is_empty() {
                                let contact = available_contacts.choose(rng).unwrap();
                                break ClientOperation::InviteContactToCall {
                                    user_id: UserId(contact.user.id as i32),
                                };
                            }
                        }

                        // Leave the current call
                        71.. => {
                            if plan.allow_client_disconnection
                                && call.read_with(cx, |call, _| call.room().is_some())
                            {
                                break ClientOperation::LeaveCall;
                            }
                        }
                    }
                }

                // Mutate projects
                30..=59 => match rng.gen_range(0..100_u32) {
                    // Open a new project
                    0..=70 => {
                        // Open a remote project
                        if let Some(room) = call.read_with(cx, |call, _| call.room().cloned()) {
                            let existing_remote_project_ids = cx.read(|cx| {
                                client
                                    .remote_projects()
                                    .iter()
                                    .map(|p| p.read(cx).remote_id().unwrap())
                                    .collect::<Vec<_>>()
                            });
                            let new_remote_projects = room.read_with(cx, |room, _| {
                                room.remote_participants()
                                    .values()
                                    .flat_map(|participant| {
                                        participant.projects.iter().filter_map(|project| {
                                            if existing_remote_project_ids.contains(&project.id) {
                                                None
                                            } else {
                                                Some((
                                                    UserId::from_proto(participant.user.id),
                                                    project.worktree_root_names[0].clone(),
                                                ))
                                            }
                                        })
                                    })
                                    .collect::<Vec<_>>()
                            });
                            if !new_remote_projects.is_empty() {
                                let (host_id, first_root_name) =
                                    new_remote_projects.choose(rng).unwrap().clone();
                                break ClientOperation::OpenRemoteProject {
                                    host_id,
                                    first_root_name,
                                };
                            }
                        }
                        // Open a local project
                        else {
                            let first_root_name = plan.next_root_dir_name();
                            break ClientOperation::OpenLocalProject { first_root_name };
                        }
                    }

                    // Close a remote project
                    71..=80 => {
                        if !client.remote_projects().is_empty() {
                            let project = client.remote_projects().choose(rng).unwrap().clone();
                            let first_root_name = root_name_for_project(&project, cx);
                            break ClientOperation::CloseRemoteProject {
                                project_root_name: first_root_name,
                            };
                        }
                    }

                    // Mutate project worktrees
                    81.. => match rng.gen_range(0..100_u32) {
                        // Add a worktree to a local project
                        0..=50 => {
                            let Some(project) = client.local_projects().choose(rng).cloned() else {
                                continue;
                            };
                            let project_root_name = root_name_for_project(&project, cx);
                            let mut paths = client.fs().paths(false);
                            paths.remove(0);
                            let new_root_path = if paths.is_empty() || rng.gen() {
                                Path::new("/").join(&plan.next_root_dir_name())
                            } else {
                                paths.choose(rng).unwrap().clone()
                            };
                            break ClientOperation::AddWorktreeToProject {
                                project_root_name,
                                new_root_path,
                            };
                        }

                        // Add an entry to a worktree
                        _ => {
                            let Some(project) = choose_random_project(client, rng) else {
                                continue;
                            };
                            let project_root_name = root_name_for_project(&project, cx);
                            let is_local = project.read_with(cx, |project, _| project.is_local());
                            let worktree = project.read_with(cx, |project, cx| {
                                project
                                    .worktrees()
                                    .filter(|worktree| {
                                        let worktree = worktree.read(cx);
                                        worktree.is_visible()
                                            && worktree.entries(false).any(|e| e.is_file())
                                            && worktree.root_entry().map_or(false, |e| e.is_dir())
                                    })
                                    .choose(rng)
                            });
                            let Some(worktree) = worktree else { continue };
                            let is_dir = rng.gen::<bool>();
                            let mut full_path =
                                worktree.read_with(cx, |w, _| PathBuf::from(w.root_name()));
                            full_path.push(gen_file_name(rng));
                            if !is_dir {
                                full_path.set_extension("rs");
                            }
                            break ClientOperation::CreateWorktreeEntry {
                                project_root_name,
                                is_local,
                                full_path,
                                is_dir,
                            };
                        }
                    },
                },

                // Query and mutate buffers
                60..=90 => {
                    let Some(project) = choose_random_project(client, rng) else {
                        continue;
                    };
                    let project_root_name = root_name_for_project(&project, cx);
                    let is_local = project.read_with(cx, |project, _| project.is_local());

                    match rng.gen_range(0..100_u32) {
                        // Manipulate an existing buffer
                        0..=70 => {
                            let Some(buffer) = client
                                .buffers_for_project(&project)
                                .iter()
                                .choose(rng)
                                .cloned()
                            else {
                                continue;
                            };

                            let full_path = buffer
                                .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));

                            match rng.gen_range(0..100_u32) {
                                // Close the buffer
                                0..=15 => {
                                    break ClientOperation::CloseBuffer {
                                        project_root_name,
                                        is_local,
                                        full_path,
                                    };
                                }
                                // Save the buffer
                                16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => {
                                    let detach = rng.gen_bool(0.3);
                                    break ClientOperation::SaveBuffer {
                                        project_root_name,
                                        is_local,
                                        full_path,
                                        detach,
                                    };
                                }
                                // Edit the buffer
                                30..=69 => {
                                    let edits = buffer
                                        .read_with(cx, |buffer, _| buffer.get_random_edits(rng, 3));
                                    break ClientOperation::EditBuffer {
                                        project_root_name,
                                        is_local,
                                        full_path,
                                        edits,
                                    };
                                }
                                // Make an LSP request
                                _ => {
                                    let offset = buffer.read_with(cx, |buffer, _| {
                                        buffer.clip_offset(
                                            rng.gen_range(0..=buffer.len()),
                                            language::Bias::Left,
                                        )
                                    });
                                    let detach = rng.gen();
                                    break ClientOperation::RequestLspDataInBuffer {
                                        project_root_name,
                                        full_path,
                                        offset,
                                        is_local,
                                        kind: match rng.gen_range(0..5_u32) {
                                            0 => LspRequestKind::Rename,
                                            1 => LspRequestKind::Highlights,
                                            2 => LspRequestKind::Definition,
                                            3 => LspRequestKind::CodeAction,
                                            4.. => LspRequestKind::Completion,
                                        },
                                        detach,
                                    };
                                }
                            }
                        }

                        71..=80 => {
                            let query = rng.gen_range('a'..='z').to_string();
                            let detach = rng.gen_bool(0.3);
                            break ClientOperation::SearchProject {
                                project_root_name,
                                is_local,
                                query,
                                detach,
                            };
                        }

                        // Open a buffer
                        81.. => {
                            let worktree = project.read_with(cx, |project, cx| {
                                project
                                    .worktrees()
                                    .filter(|worktree| {
                                        let worktree = worktree.read(cx);
                                        worktree.is_visible()
                                            && worktree.entries(false).any(|e| e.is_file())
                                    })
                                    .choose(rng)
                            });
                            let Some(worktree) = worktree else { continue };
                            let full_path = worktree.read_with(cx, |worktree, _| {
                                let entry = worktree
                                    .entries(false)
                                    .filter(|e| e.is_file())
                                    .choose(rng)
                                    .unwrap();
                                if entry.path.as_ref() == Path::new("") {
                                    Path::new(worktree.root_name()).into()
                                } else {
                                    Path::new(worktree.root_name()).join(&entry.path)
                                }
                            });
                            break ClientOperation::OpenBuffer {
                                project_root_name,
                                is_local,
                                full_path,
                            };
                        }
                    }
                }

                // Update a git related action
                91..=95 => {
                    break ClientOperation::GitOperation {
                        operation: generate_git_operation(rng, client),
                    };
                }

                // Create or update a file or directory
                96.. => {
                    let is_dir = rng.gen::<bool>();
                    let content;
                    let mut path;
                    let dir_paths = client.fs().directories(false);

                    if is_dir {
                        content = String::new();
                        path = dir_paths.choose(rng).unwrap().clone();
                        path.push(gen_file_name(rng));
                    } else {
                        content = Alphanumeric.sample_string(rng, 16);

                        // Create a new file or overwrite an existing file
                        let file_paths = client.fs().files();
                        if file_paths.is_empty() || rng.gen_bool(0.5) {
                            path = dir_paths.choose(rng).unwrap().clone();
                            path.push(gen_file_name(rng));
                            path.set_extension("rs");
                        } else {
                            path = file_paths.choose(rng).unwrap().clone()
                        };
                    }
                    break ClientOperation::WriteFsEntry {
                        path,
                        is_dir,
                        content,
                    };
                }
            }
        }
    }

    async fn apply_operation(
        client: &TestClient,
        operation: ClientOperation,
        cx: &mut TestAppContext,
    ) -> Result<(), TestError> {
        match operation {
            ClientOperation::AcceptIncomingCall => {
                let active_call = cx.read(ActiveCall::global);
                if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) {
                    Err(TestError::Inapplicable)?;
                }

                log::info!("{}: accepting incoming call", client.username);
                active_call
                    .update(cx, |call, cx| call.accept_incoming(cx))
                    .await?;
            }

            ClientOperation::RejectIncomingCall => {
                let active_call = cx.read(ActiveCall::global);
                if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) {
                    Err(TestError::Inapplicable)?;
                }

                log::info!("{}: declining incoming call", client.username);
                active_call.update(cx, |call, cx| call.decline_incoming(cx))?;
            }

            ClientOperation::LeaveCall => {
                let active_call = cx.read(ActiveCall::global);
                if active_call.read_with(cx, |call, _| call.room().is_none()) {
                    Err(TestError::Inapplicable)?;
                }

                log::info!("{}: hanging up", client.username);
                active_call.update(cx, |call, cx| call.hang_up(cx)).await?;
            }

            ClientOperation::InviteContactToCall { user_id } => {
                let active_call = cx.read(ActiveCall::global);

                log::info!("{}: inviting {}", client.username, user_id,);
                active_call
                    .update(cx, |call, cx| call.invite(user_id.to_proto(), None, cx))
                    .await
                    .log_err();
            }

            ClientOperation::OpenLocalProject { first_root_name } => {
                log::info!(
                    "{}: opening local project at {:?}",
                    client.username,
                    first_root_name
                );

                let root_path = Path::new("/").join(&first_root_name);
                client.fs().create_dir(&root_path).await.unwrap();
                client
                    .fs()
                    .create_file(&root_path.join("main.rs"), Default::default())
                    .await
                    .unwrap();
                let project = client.build_local_project(root_path, cx).await.0;
                ensure_project_shared(&project, client, cx).await;
                client.local_projects_mut().push(project.clone());
            }

            ClientOperation::AddWorktreeToProject {
                project_root_name,
                new_root_path,
            } => {
                let project = project_for_root_name(client, &project_root_name, cx)
                    .ok_or(TestError::Inapplicable)?;

                log::info!(
                    "{}: finding/creating local worktree at {:?} to project with root path {}",
                    client.username,
                    new_root_path,
                    project_root_name
                );

                ensure_project_shared(&project, client, cx).await;
                if !client.fs().paths(false).contains(&new_root_path) {
                    client.fs().create_dir(&new_root_path).await.unwrap();
                }
                project
                    .update(cx, |project, cx| {
                        project.find_or_create_local_worktree(&new_root_path, true, cx)
                    })
                    .await
                    .unwrap();
            }

            ClientOperation::CloseRemoteProject { project_root_name } => {
                let project = project_for_root_name(client, &project_root_name, cx)
                    .ok_or(TestError::Inapplicable)?;

                log::info!(
                    "{}: closing remote project with root path {}",
                    client.username,
                    project_root_name,
                );

                let ix = client
                    .remote_projects()
                    .iter()
                    .position(|p| p == &project)
                    .unwrap();
                cx.update(|_| {
                    client.remote_projects_mut().remove(ix);
                    client.buffers().retain(|p, _| *p != project);
                    drop(project);
                });
            }

            ClientOperation::OpenRemoteProject {
                host_id,
                first_root_name,
            } => {
                let active_call = cx.read(ActiveCall::global);
                let project = active_call
                    .update(cx, |call, cx| {
                        let room = call.room().cloned()?;
                        let participant = room
                            .read(cx)
                            .remote_participants()
                            .get(&host_id.to_proto())?;
                        let project_id = participant
                            .projects
                            .iter()
                            .find(|project| project.worktree_root_names[0] == first_root_name)?
                            .id;
                        Some(room.update(cx, |room, cx| {
                            room.join_project(
                                project_id,
                                client.language_registry().clone(),
                                FakeFs::new(cx.background_executor().clone()),
                                cx,
                            )
                        }))
                    })
                    .ok_or(TestError::Inapplicable)?;

                log::info!(
                    "{}: joining remote project of user {}, root name {}",
                    client.username,
                    host_id,
                    first_root_name,
                );

                let project = project.await?;
                client.remote_projects_mut().push(project.clone());
            }

            ClientOperation::CreateWorktreeEntry {
                project_root_name,
                is_local,
                full_path,
                is_dir,
            } => {
                let project = project_for_root_name(client, &project_root_name, cx)
                    .ok_or(TestError::Inapplicable)?;
                let project_path = project_path_for_full_path(&project, &full_path, cx)
                    .ok_or(TestError::Inapplicable)?;

                log::info!(
                    "{}: creating {} at path {:?} in {} project {}",
                    client.username,
                    if is_dir { "dir" } else { "file" },
                    full_path,
                    if is_local { "local" } else { "remote" },
                    project_root_name,
                );

                ensure_project_shared(&project, client, cx).await;
                project
                    .update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
                    .await?;
            }

            ClientOperation::OpenBuffer {
                project_root_name,
                is_local,
                full_path,
            } => {
                let project = project_for_root_name(client, &project_root_name, cx)
                    .ok_or(TestError::Inapplicable)?;
                let project_path = project_path_for_full_path(&project, &full_path, cx)
                    .ok_or(TestError::Inapplicable)?;

                log::info!(
                    "{}: opening buffer {:?} in {} project {}",
                    client.username,
                    full_path,
                    if is_local { "local" } else { "remote" },
                    project_root_name,
                );

                ensure_project_shared(&project, client, cx).await;
                let buffer = project
                    .update(cx, |project, cx| project.open_buffer(project_path, cx))
                    .await?;
                client.buffers_for_project(&project).insert(buffer);
            }

            ClientOperation::EditBuffer {
                project_root_name,
                is_local,
                full_path,
                edits,
            } => {
                let project = project_for_root_name(client, &project_root_name, cx)
                    .ok_or(TestError::Inapplicable)?;
                let buffer = buffer_for_full_path(client, &project, &full_path, cx)
                    .ok_or(TestError::Inapplicable)?;

                log::info!(
                    "{}: editing buffer {:?} in {} project {} with {:?}",
                    client.username,
                    full_path,
                    if is_local { "local" } else { "remote" },
                    project_root_name,
                    edits
                );

                ensure_project_shared(&project, client, cx).await;
                buffer.update(cx, |buffer, cx| {
                    let snapshot = buffer.snapshot();
                    buffer.edit(
                        edits.into_iter().map(|(range, text)| {
                            let start = snapshot.clip_offset(range.start, Bias::Left);
                            let end = snapshot.clip_offset(range.end, Bias::Right);
                            (start..end, text)
                        }),
                        None,
                        cx,
                    );
                });
            }

            ClientOperation::CloseBuffer {
                project_root_name,
                is_local,
                full_path,
            } => {
                let project = project_for_root_name(client, &project_root_name, cx)
                    .ok_or(TestError::Inapplicable)?;
                let buffer = buffer_for_full_path(client, &project, &full_path, cx)
                    .ok_or(TestError::Inapplicable)?;

                log::info!(
                    "{}: closing buffer {:?} in {} project {}",
                    client.username,
                    full_path,
                    if is_local { "local" } else { "remote" },
                    project_root_name
                );

                ensure_project_shared(&project, client, cx).await;
                cx.update(|_| {
                    client.buffers_for_project(&project).remove(&buffer);
                    drop(buffer);
                });
            }

            ClientOperation::SaveBuffer {
                project_root_name,
                is_local,
                full_path,
                detach,
            } => {
                let project = project_for_root_name(client, &project_root_name, cx)
                    .ok_or(TestError::Inapplicable)?;
                let buffer = buffer_for_full_path(client, &project, &full_path, cx)
                    .ok_or(TestError::Inapplicable)?;

                log::info!(
                    "{}: saving buffer {:?} in {} project {}, {}",
                    client.username,
                    full_path,
                    if is_local { "local" } else { "remote" },
                    project_root_name,
                    if detach { "detaching" } else { "awaiting" }
                );

                ensure_project_shared(&project, client, cx).await;
                let requested_version = buffer.read_with(cx, |buffer, _| buffer.version());
                let save =
                    project.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
                let save = cx.spawn(|cx| async move {
                    save.await
                        .map_err(|err| anyhow!("save request failed: {:?}", err))?;
                    assert!(buffer
                        .read_with(&cx, |buffer, _| { buffer.saved_version().to_owned() })
                        .expect("App should not be dropped")
                        .observed_all(&requested_version));
                    anyhow::Ok(())
                });
                if detach {
                    cx.update(|cx| save.detach_and_log_err(cx));
                } else {
                    save.await?;
                }
            }

            ClientOperation::RequestLspDataInBuffer {
                project_root_name,
                is_local,
                full_path,
                offset,
                kind,
                detach,
            } => {
                let project = project_for_root_name(client, &project_root_name, cx)
                    .ok_or(TestError::Inapplicable)?;
                let buffer = buffer_for_full_path(client, &project, &full_path, cx)
                    .ok_or(TestError::Inapplicable)?;

                log::info!(
                    "{}: request LSP {:?} for buffer {:?} in {} project {}, {}",
                    client.username,
                    kind,
                    full_path,
                    if is_local { "local" } else { "remote" },
                    project_root_name,
                    if detach { "detaching" } else { "awaiting" }
                );

                use futures::{FutureExt as _, TryFutureExt as _};
                let offset = buffer.read_with(cx, |b, _| b.clip_offset(offset, Bias::Left));

                let process_lsp_request = project.update(cx, |project, cx| match kind {
                    LspRequestKind::Rename => project
                        .prepare_rename(buffer, offset, cx)
                        .map_ok(|_| ())
                        .boxed(),
                    LspRequestKind::Completion => project
                        .completions(&buffer, offset, cx)
                        .map_ok(|_| ())
                        .boxed(),
                    LspRequestKind::CodeAction => project
                        .code_actions(&buffer, offset..offset, cx)
                        .map(|_| Ok(()))
                        .boxed(),
                    LspRequestKind::Definition => project
                        .definition(&buffer, offset, cx)
                        .map_ok(|_| ())
                        .boxed(),
                    LspRequestKind::Highlights => project
                        .document_highlights(&buffer, offset, cx)
                        .map_ok(|_| ())
                        .boxed(),
                });
                let request = cx.foreground_executor().spawn(process_lsp_request);
                if detach {
                    request.detach();
                } else {
                    request.await?;
                }
            }

            ClientOperation::SearchProject {
                project_root_name,
                is_local,
                query,
                detach,
            } => {
                let project = project_for_root_name(client, &project_root_name, cx)
                    .ok_or(TestError::Inapplicable)?;

                log::info!(
                    "{}: search {} project {} for {:?}, {}",
                    client.username,
                    if is_local { "local" } else { "remote" },
                    project_root_name,
                    query,
                    if detach { "detaching" } else { "awaiting" }
                );

                let mut search = project.update(cx, |project, cx| {
                    project.search(
                        SearchQuery::text(query, false, false, false, Vec::new(), Vec::new())
                            .unwrap(),
                        cx,
                    )
                });
                drop(project);
                let search = cx.executor().spawn(async move {
                    let mut results = HashMap::default();
                    while let Some(result) = search.next().await {
                        if let SearchResult::Buffer { buffer, ranges } = result {
                            results.entry(buffer).or_insert(ranges);
                        }
                    }
                    results
                });
                search.await;
            }

            ClientOperation::WriteFsEntry {
                path,
                is_dir,
                content,
            } => {
                if !client
                    .fs()
                    .directories(false)
                    .contains(&path.parent().unwrap().to_owned())
                {
                    return Err(TestError::Inapplicable);
                }

                if is_dir {
                    log::info!("{}: creating dir at {:?}", client.username, path);
                    client.fs().create_dir(&path).await.unwrap();
                } else {
                    let exists = client.fs().metadata(&path).await?.is_some();
                    let verb = if exists { "updating" } else { "creating" };
                    log::info!("{}: {} file at {:?}", verb, client.username, path);

                    client
                        .fs()
                        .save(&path, &content.as_str().into(), text::LineEnding::Unix)
                        .await
                        .unwrap();
                }
            }

            ClientOperation::GitOperation { operation } => match operation {
                GitOperation::WriteGitIndex {
                    repo_path,
                    contents,
                } => {
                    if !client.fs().directories(false).contains(&repo_path) {
                        return Err(TestError::Inapplicable);
                    }

                    for (path, _) in contents.iter() {
                        if !client.fs().files().contains(&repo_path.join(path)) {
                            return Err(TestError::Inapplicable);
                        }
                    }

                    log::info!(
                        "{}: writing git index for repo {:?}: {:?}",
                        client.username,
                        repo_path,
                        contents
                    );

                    let dot_git_dir = repo_path.join(".git");
                    let contents = contents
                        .iter()
                        .map(|(path, contents)| (path.as_path(), contents.clone()))
                        .collect::<Vec<_>>();
                    if client.fs().metadata(&dot_git_dir).await?.is_none() {
                        client.fs().create_dir(&dot_git_dir).await?;
                    }
                    client.fs().set_index_for_repo(&dot_git_dir, &contents);
                }
                GitOperation::WriteGitBranch {
                    repo_path,
                    new_branch,
                } => {
                    if !client.fs().directories(false).contains(&repo_path) {
                        return Err(TestError::Inapplicable);
                    }

                    log::info!(
                        "{}: writing git branch for repo {:?}: {:?}",
                        client.username,
                        repo_path,
                        new_branch
                    );

                    let dot_git_dir = repo_path.join(".git");
                    if client.fs().metadata(&dot_git_dir).await?.is_none() {
                        client.fs().create_dir(&dot_git_dir).await?;
                    }
                    client
                        .fs()
                        .set_branch_name(&dot_git_dir, new_branch.clone());
                }
                GitOperation::WriteGitStatuses {
                    repo_path,
                    statuses,
                    git_operation,
                } => {
                    if !client.fs().directories(false).contains(&repo_path) {
                        return Err(TestError::Inapplicable);
                    }
                    for (path, _) in statuses.iter() {
                        if !client.fs().files().contains(&repo_path.join(path)) {
                            return Err(TestError::Inapplicable);
                        }
                    }

                    log::info!(
                        "{}: writing git statuses for repo {:?}: {:?}",
                        client.username,
                        repo_path,
                        statuses
                    );

                    let dot_git_dir = repo_path.join(".git");

                    let statuses = statuses
                        .iter()
                        .map(|(path, val)| (path.as_path(), *val))
                        .collect::<Vec<_>>();

                    if client.fs().metadata(&dot_git_dir).await?.is_none() {
                        client.fs().create_dir(&dot_git_dir).await?;
                    }

                    if git_operation {
                        client.fs().set_status_for_repo_via_git_operation(
                            &dot_git_dir,
                            statuses.as_slice(),
                        );
                    } else {
                        client.fs().set_status_for_repo_via_working_copy_change(
                            &dot_git_dir,
                            statuses.as_slice(),
                        );
                    }
                }
            },
        }
        Ok(())
    }

    async fn on_client_added(client: &Rc<TestClient>, _: &mut TestAppContext) {
        client.language_registry().add(Arc::new(Language::new(
            LanguageConfig {
                name: "Rust".into(),
                matcher: LanguageMatcher {
                    path_suffixes: vec!["rs".to_string()],
                    ..Default::default()
                },
                ..Default::default()
            },
            None,
        )));
        client.language_registry().register_fake_lsp_adapter(
            "Rust",
            FakeLspAdapter {
                name: "the-fake-language-server",
                capabilities: lsp::LanguageServer::full_capabilities(),
                initializer: Some(Box::new({
                    let fs = client.app_state.fs.clone();
                    move |fake_server: &mut FakeLanguageServer| {
                        fake_server.handle_request::<lsp::request::Completion, _, _>(
                            |_, _| async move {
                                Ok(Some(lsp::CompletionResponse::Array(vec![
                                    lsp::CompletionItem {
                                        text_edit: Some(lsp::CompletionTextEdit::Edit(
                                            lsp::TextEdit {
                                                range: lsp::Range::new(
                                                    lsp::Position::new(0, 0),
                                                    lsp::Position::new(0, 0),
                                                ),
                                                new_text: "the-new-text".to_string(),
                                            },
                                        )),
                                        ..Default::default()
                                    },
                                ])))
                            },
                        );

                        fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
                            |_, _| async move {
                                Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
                                    lsp::CodeAction {
                                        title: "the-code-action".to_string(),
                                        ..Default::default()
                                    },
                                )]))
                            },
                        );

                        fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
                            |params, _| async move {
                                Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
                                    params.position,
                                    params.position,
                                ))))
                            },
                        );

                        fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
                            let fs = fs.clone();
                            move |_, cx| {
                                let background = cx.background_executor();
                                let mut rng = background.rng();
                                let count = rng.gen_range::<usize, _>(1..3);
                                let files = fs.as_fake().files();
                                let files = (0..count)
                                    .map(|_| files.choose(&mut rng).unwrap().clone())
                                    .collect::<Vec<_>>();
                                async move {
                                    log::info!("LSP: Returning definitions in files {:?}", &files);
                                    Ok(Some(lsp::GotoDefinitionResponse::Array(
                                        files
                                            .into_iter()
                                            .map(|file| lsp::Location {
                                                uri: lsp::Url::from_file_path(file).unwrap(),
                                                range: Default::default(),
                                            })
                                            .collect(),
                                    )))
                                }
                            }
                        });

                        fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
                            move |_, cx| {
                                let mut highlights = Vec::new();
                                let background = cx.background_executor();
                                let mut rng = background.rng();

                                let highlight_count = rng.gen_range(1..=5);
                                for _ in 0..highlight_count {
                                    let start_row = rng.gen_range(0..100);
                                    let start_column = rng.gen_range(0..100);
                                    let end_row = rng.gen_range(0..100);
                                    let end_column = rng.gen_range(0..100);
                                    let start = PointUtf16::new(start_row, start_column);
                                    let end = PointUtf16::new(end_row, end_column);
                                    let range = if start > end { end..start } else { start..end };
                                    highlights.push(lsp::DocumentHighlight {
                                        range: range_to_lsp(range.clone()),
                                        kind: Some(lsp::DocumentHighlightKind::READ),
                                    });
                                }
                                highlights.sort_unstable_by_key(|highlight| {
                                    (highlight.range.start, highlight.range.end)
                                });
                                async move { Ok(Some(highlights)) }
                            },
                        );
                    }
                })),
                ..Default::default()
            },
        );
    }

    async fn on_quiesce(_: &mut TestServer, clients: &mut [(Rc<TestClient>, TestAppContext)]) {
        for (client, client_cx) in clients.iter() {
            for guest_project in client.remote_projects().iter() {
                guest_project.read_with(client_cx, |guest_project, cx| {
                        let host_project = clients.iter().find_map(|(client, cx)| {
                            let project = client
                                .local_projects()
                                .iter()
                                .find(|host_project| {
                                    host_project.read_with(cx, |host_project, _| {
                                        host_project.remote_id() == guest_project.remote_id()
                                    })
                                })?
                                .clone();
                            Some((project, cx))
                        });

                        if !guest_project.is_disconnected() {
                            if let Some((host_project, host_cx)) = host_project {
                                let host_worktree_snapshots =
                                    host_project.read_with(host_cx, |host_project, cx| {
                                        host_project
                                            .worktrees()
                                            .map(|worktree| {
                                                let worktree = worktree.read(cx);
                                                (worktree.id(), worktree.snapshot())
                                            })
                                            .collect::<BTreeMap<_, _>>()
                                    });
                                let guest_worktree_snapshots = guest_project
                                    .worktrees()
                                    .map(|worktree| {
                                        let worktree = worktree.read(cx);
                                        (worktree.id(), worktree.snapshot())
                                    })
                                    .collect::<BTreeMap<_, _>>();

                                assert_eq!(
                                    guest_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
                                    host_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
                                    "{} has different worktrees than the host for project {:?}",
                                    client.username, guest_project.remote_id(),
                                );

                                for (id, host_snapshot) in &host_worktree_snapshots {
                                    let guest_snapshot = &guest_worktree_snapshots[id];
                                    assert_eq!(
                                        guest_snapshot.root_name(),
                                        host_snapshot.root_name(),
                                        "{} has different root name than the host for worktree {}, project {:?}",
                                        client.username,
                                        id,
                                        guest_project.remote_id(),
                                    );
                                    assert_eq!(
                                        guest_snapshot.abs_path(),
                                        host_snapshot.abs_path(),
                                        "{} has different abs path than the host for worktree {}, project: {:?}",
                                        client.username,
                                        id,
                                        guest_project.remote_id(),
                                    );
                                    assert_eq!(
                                        guest_snapshot.entries(false).collect::<Vec<_>>(),
                                        host_snapshot.entries(false).collect::<Vec<_>>(),
                                        "{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}",
                                        client.username,
                                        host_snapshot.abs_path(),
                                        id,
                                        guest_project.remote_id(),
                                    );
                                    assert_eq!(guest_snapshot.repositories().collect::<Vec<_>>(), host_snapshot.repositories().collect::<Vec<_>>(),
                                        "{} has different repositories than the host for worktree {:?} and project {:?}",
                                        client.username,
                                        host_snapshot.abs_path(),
                                        guest_project.remote_id(),
                                    );
                                    assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(),
                                        "{} has different scan id than the host for worktree {:?} and project {:?}",
                                        client.username,
                                        host_snapshot.abs_path(),
                                        guest_project.remote_id(),
                                    );
                                }
                            }
                        }

                        for buffer in guest_project.opened_buffers() {
                            let buffer = buffer.read(cx);
                            assert_eq!(
                                buffer.deferred_ops_len(),
                                0,
                                "{} has deferred operations for buffer {:?} in project {:?}",
                                client.username,
                                buffer.file().unwrap().full_path(cx),
                                guest_project.remote_id(),
                            );
                        }
                    });
            }

            let buffers = client.buffers().clone();
            for (guest_project, guest_buffers) in &buffers {
                let project_id = if guest_project.read_with(client_cx, |project, _| {
                    project.is_local() || project.is_disconnected()
                }) {
                    continue;
                } else {
                    guest_project
                        .read_with(client_cx, |project, _| project.remote_id())
                        .unwrap()
                };
                let guest_user_id = client.user_id().unwrap();

                let host_project = clients.iter().find_map(|(client, cx)| {
                    let project = client
                        .local_projects()
                        .iter()
                        .find(|host_project| {
                            host_project.read_with(cx, |host_project, _| {
                                host_project.remote_id() == Some(project_id)
                            })
                        })?
                        .clone();
                    Some((client.user_id().unwrap(), project, cx))
                });

                let (host_user_id, host_project, host_cx) =
                    if let Some((host_user_id, host_project, host_cx)) = host_project {
                        (host_user_id, host_project, host_cx)
                    } else {
                        continue;
                    };

                for guest_buffer in guest_buffers {
                    let buffer_id =
                        guest_buffer.read_with(client_cx, |buffer, _| buffer.remote_id());
                    let host_buffer = host_project.read_with(host_cx, |project, _| {
                        project.buffer_for_id(buffer_id).unwrap_or_else(|| {
                            panic!(
                                "host does not have buffer for guest:{}, peer:{:?}, id:{}",
                                client.username,
                                client.peer_id(),
                                buffer_id
                            )
                        })
                    });
                    let path = host_buffer
                        .read_with(host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));

                    assert_eq!(
                        guest_buffer.read_with(client_cx, |buffer, _| buffer.deferred_ops_len()),
                        0,
                        "{}, buffer {}, path {:?} has deferred operations",
                        client.username,
                        buffer_id,
                        path,
                    );
                    assert_eq!(
                        guest_buffer.read_with(client_cx, |buffer, _| buffer.text()),
                        host_buffer.read_with(host_cx, |buffer, _| buffer.text()),
                        "{}, buffer {}, path {:?}, differs from the host's buffer",
                        client.username,
                        buffer_id,
                        path
                    );

                    let host_file = host_buffer.read_with(host_cx, |b, _| b.file().cloned());
                    let guest_file = guest_buffer.read_with(client_cx, |b, _| b.file().cloned());
                    match (host_file, guest_file) {
                        (Some(host_file), Some(guest_file)) => {
                            assert_eq!(guest_file.path(), host_file.path());
                            assert_eq!(guest_file.is_deleted(), host_file.is_deleted());
                            assert_eq!(
                                guest_file.mtime(),
                                host_file.mtime(),
                                "guest {} mtime does not match host {} for path {:?} in project {}",
                                guest_user_id,
                                host_user_id,
                                guest_file.path(),
                                project_id,
                            );
                        }
                        (None, None) => {}
                        (None, _) => panic!("host's file is None, guest's isn't"),
                        (_, None) => panic!("guest's file is None, hosts's isn't"),
                    }

                    let host_diff_base = host_buffer
                        .read_with(host_cx, |b, _| b.diff_base().map(ToString::to_string));
                    let guest_diff_base = guest_buffer
                        .read_with(client_cx, |b, _| b.diff_base().map(ToString::to_string));
                    assert_eq!(
                            guest_diff_base, host_diff_base,
                            "guest {} diff base does not match host's for path {path:?} in project {project_id}",
                            client.username
                        );

                    let host_saved_version =
                        host_buffer.read_with(host_cx, |b, _| b.saved_version().clone());
                    let guest_saved_version =
                        guest_buffer.read_with(client_cx, |b, _| b.saved_version().clone());
                    assert_eq!(
                            guest_saved_version, host_saved_version,
                            "guest {} saved version does not match host's for path {path:?} in project {project_id}",
                            client.username
                        );

                    let host_saved_version_fingerprint =
                        host_buffer.read_with(host_cx, |b, _| b.saved_version_fingerprint());
                    let guest_saved_version_fingerprint =
                        guest_buffer.read_with(client_cx, |b, _| b.saved_version_fingerprint());
                    assert_eq!(
                            guest_saved_version_fingerprint, host_saved_version_fingerprint,
                            "guest {} saved fingerprint does not match host's for path {path:?} in project {project_id}",
                            client.username
                        );

                    let host_saved_mtime = host_buffer.read_with(host_cx, |b, _| b.saved_mtime());
                    let guest_saved_mtime =
                        guest_buffer.read_with(client_cx, |b, _| b.saved_mtime());
                    assert_eq!(
                            guest_saved_mtime, host_saved_mtime,
                            "guest {} saved mtime does not match host's for path {path:?} in project {project_id}",
                            client.username
                        );

                    let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
                    let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
                    assert_eq!(guest_is_dirty, host_is_dirty,
                            "guest {} dirty status does not match host's for path {path:?} in project {project_id}",
                            client.username
                        );

                    let host_has_conflict = host_buffer.read_with(host_cx, |b, _| b.has_conflict());
                    let guest_has_conflict =
                        guest_buffer.read_with(client_cx, |b, _| b.has_conflict());
                    assert_eq!(guest_has_conflict, host_has_conflict,
                            "guest {} conflict status does not match host's for path {path:?} in project {project_id}",
                            client.username
                        );
                }
            }
        }
    }
}

fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation {
    fn generate_file_paths(
        repo_path: &Path,
        rng: &mut StdRng,
        client: &TestClient,
    ) -> Vec<PathBuf> {
        let mut paths = client
            .fs()
            .files()
            .into_iter()
            .filter(|path| path.starts_with(repo_path))
            .collect::<Vec<_>>();

        let count = rng.gen_range(0..=paths.len());
        paths.shuffle(rng);
        paths.truncate(count);

        paths
            .iter()
            .map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf())
            .collect::<Vec<_>>()
    }

    let repo_path = client.fs().directories(false).choose(rng).unwrap().clone();

    match rng.gen_range(0..100_u32) {
        0..=25 => {
            let file_paths = generate_file_paths(&repo_path, rng, client);

            let contents = file_paths
                .into_iter()
                .map(|path| (path, Alphanumeric.sample_string(rng, 16)))
                .collect();

            GitOperation::WriteGitIndex {
                repo_path,
                contents,
            }
        }
        26..=63 => {
            let new_branch = (rng.gen_range(0..10) > 3).then(|| Alphanumeric.sample_string(rng, 8));

            GitOperation::WriteGitBranch {
                repo_path,
                new_branch,
            }
        }
        64..=100 => {
            let file_paths = generate_file_paths(&repo_path, rng, client);

            let statuses = file_paths
                .into_iter()
                .map(|paths| {
                    (
                        paths,
                        match rng.gen_range(0..3_u32) {
                            0 => GitFileStatus::Added,
                            1 => GitFileStatus::Modified,
                            2 => GitFileStatus::Conflict,
                            _ => unreachable!(),
                        },
                    )
                })
                .collect::<Vec<_>>();

            let git_operation = rng.gen::<bool>();

            GitOperation::WriteGitStatuses {
                repo_path,
                statuses,
                git_operation,
            }
        }
        _ => unreachable!(),
    }
}

fn buffer_for_full_path(
    client: &TestClient,
    project: &Model<Project>,
    full_path: &PathBuf,
    cx: &TestAppContext,
) -> Option<Model<language::Buffer>> {
    client
        .buffers_for_project(project)
        .iter()
        .find(|buffer| {
            buffer.read_with(cx, |buffer, cx| {
                buffer.file().unwrap().full_path(cx) == *full_path
            })
        })
        .cloned()
}

fn project_for_root_name(
    client: &TestClient,
    root_name: &str,
    cx: &TestAppContext,
) -> Option<Model<Project>> {
    if let Some(ix) = project_ix_for_root_name(client.local_projects().deref(), root_name, cx) {
        return Some(client.local_projects()[ix].clone());
    }
    if let Some(ix) = project_ix_for_root_name(client.remote_projects().deref(), root_name, cx) {
        return Some(client.remote_projects()[ix].clone());
    }
    None
}

fn project_ix_for_root_name(
    projects: &[Model<Project>],
    root_name: &str,
    cx: &TestAppContext,
) -> Option<usize> {
    projects.iter().position(|project| {
        project.read_with(cx, |project, cx| {
            let worktree = project.visible_worktrees(cx).next().unwrap();
            worktree.read(cx).root_name() == root_name
        })
    })
}

fn root_name_for_project(project: &Model<Project>, cx: &TestAppContext) -> String {
    project.read_with(cx, |project, cx| {
        project
            .visible_worktrees(cx)
            .next()
            .unwrap()
            .read(cx)
            .root_name()
            .to_string()
    })
}

fn project_path_for_full_path(
    project: &Model<Project>,
    full_path: &Path,
    cx: &TestAppContext,
) -> Option<ProjectPath> {
    let mut components = full_path.components();
    let root_name = components.next().unwrap().as_os_str().to_str().unwrap();
    let path = components.as_path().into();
    let worktree_id = project.read_with(cx, |project, cx| {
        project.worktrees().find_map(|worktree| {
            let worktree = worktree.read(cx);
            if worktree.root_name() == root_name {
                Some(worktree.id())
            } else {
                None
            }
        })
    })?;
    Some(ProjectPath { worktree_id, path })
}

async fn ensure_project_shared(
    project: &Model<Project>,
    client: &TestClient,
    cx: &mut TestAppContext,
) {
    let first_root_name = root_name_for_project(project, cx);
    let active_call = cx.read(ActiveCall::global);
    if active_call.read_with(cx, |call, _| call.room().is_some())
        && project.read_with(cx, |project, _| project.is_local() && !project.is_shared())
    {
        match active_call
            .update(cx, |call, cx| call.share_project(project.clone(), cx))
            .await
        {
            Ok(project_id) => {
                log::info!(
                    "{}: shared project {} with id {}",
                    client.username,
                    first_root_name,
                    project_id
                );
            }
            Err(error) => {
                log::error!(
                    "{}: error sharing project {}: {:?}",
                    client.username,
                    first_root_name,
                    error
                );
            }
        }
    }
}

fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<Model<Project>> {
    client
        .local_projects()
        .deref()
        .iter()
        .chain(client.remote_projects().iter())
        .choose(rng)
        .cloned()
}

fn gen_file_name(rng: &mut StdRng) -> String {
    let mut name = String::new();
    for _ in 0..10 {
        let letter = rng.gen_range('a'..='z');
        name.push(letter);
    }
    name
}
