use anyhow::Result;
use gitbutler_command_context::CommandContext;
use gitbutler_commit::commit_ext::CommitExt;
use gitbutler_patch_reference::{CommitOrChangeId, PatchReference};
use gitbutler_repo::{LogUntil, RepositoryExt as _};
use gitbutler_stack::VirtualBranchesHandle;
use gitbutler_stack_api::{PatchReferenceUpdate, Stack, TargetUpdate};
use itertools::Itertools;
use tempfile::TempDir;

#[test]
fn init_success() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let test_ctx = test_ctx(&ctx)?;
    let mut branch = test_ctx.branch;
    let result = branch.initialize(&ctx); // this is noop really
    assert!(result.is_ok());
    assert!(branch.initialized());
    assert_eq!(branch.heads.len(), 1);
    assert_eq!(branch.heads[0].name, "virtual"); // matches the stack name
    assert_eq!(
        branch.heads[0].target,
        CommitOrChangeId::ChangeId(
            ctx.repository()
                .find_commit(branch.head())?
                .change_id()
                .unwrap()
        )
    );
    // Assert persisted
    assert_eq!(branch, test_ctx.handle.get_branch(branch.id)?);
    Ok(())
}

#[test]
fn init_already_initialized_noop() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let test_ctx = test_ctx(&ctx)?;
    let mut branch = test_ctx.branch;
    let result = branch.initialize(&ctx);
    assert!(result.is_ok());
    let result = branch.initialize(&ctx);
    assert!(result.is_ok()); // noop
    Ok(())
}

#[test]
fn add_series_success() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let reference = PatchReference {
        name: "asdf".into(),
        target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()),
        description: Some("my description".into()),
    };
    let result = test_ctx.branch.add_series(&ctx, reference, None);
    assert!(result.is_ok());
    assert_eq!(test_ctx.branch.heads.len(), 2);
    assert_eq!(test_ctx.branch.heads[0].name, "asdf");
    assert_eq!(
        test_ctx.branch.heads[0].description,
        Some("my description".into())
    );
    // Assert persisted
    assert_eq!(
        test_ctx.branch,
        test_ctx.handle.get_branch(test_ctx.branch.id)?
    );
    Ok(())
}

#[test]
fn add_series_top_of_stack() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let result =
        test_ctx
            .branch
            .add_series_top_of_stack(&ctx, "asdf".into(), Some("my description".into()));
    assert!(result.is_ok());
    assert_eq!(test_ctx.branch.heads.len(), 2);
    assert_eq!(test_ctx.branch.heads[1].name, "asdf");
    assert_eq!(
        test_ctx.branch.heads[1].description,
        Some("my description".into())
    );
    // Assert persisted
    assert_eq!(
        test_ctx.branch,
        test_ctx.handle.get_branch(test_ctx.branch.id)?
    );
    Ok(())
}

#[test]
fn add_series_top_base() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let merge_base = ctx.repository().find_commit(
        ctx.repository()
            .merge_base(test_ctx.branch.head(), test_ctx.default_target.sha)?,
    )?;
    let reference = PatchReference {
        name: "asdf".into(),
        target: CommitOrChangeId::CommitId(merge_base.id().to_string()),
        description: Some("my description".into()),
    };
    let result = test_ctx.branch.add_series(&ctx, reference, None);
    println!("{:?}", result);
    // Assert persisted
    assert_eq!(
        test_ctx.branch,
        test_ctx.handle.get_branch(test_ctx.branch.id)?
    );
    Ok(())
}

#[test]
fn add_multiple_series() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;

    assert_eq!(test_ctx.branch.heads.len(), 1);
    assert_eq!(head_names(&test_ctx), vec!["virtual"]); // defalts to stack name
    let default_head = test_ctx.branch.heads[0].clone();

    let head_4 = PatchReference {
        name: "head_4".into(),
        target: CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap()),
        description: None,
    };
    let result = test_ctx
        .branch
        .add_series(&ctx, head_4, Some(default_head.name.clone()));
    assert!(result.is_ok());
    assert_eq!(head_names(&test_ctx), vec!["virtual", "head_4"]);

    let head_2 = PatchReference {
        name: "head_2".into(),
        target: CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap()),
        description: None,
    };
    let result = test_ctx.branch.add_series(&ctx, head_2, None);
    assert!(result.is_ok());
    assert_eq!(head_names(&test_ctx), vec!["head_2", "virtual", "head_4"]);

    let head_1 = PatchReference {
        name: "head_1".into(),
        target: CommitOrChangeId::ChangeId(test_ctx.commits.first().unwrap().change_id().unwrap()),
        description: None,
    };

    let result = test_ctx.branch.add_series(&ctx, head_1, None);
    assert!(result.is_ok());
    assert_eq!(
        head_names(&test_ctx),
        vec!["head_1", "head_2", "virtual", "head_4"]
    );
    Ok(())
}

#[test]
fn add_series_commitid_when_changeid_available() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let reference = PatchReference {
        name: "asdf".into(),
        target: CommitOrChangeId::CommitId(test_ctx.commits[1].id().to_string()),
        description: None,
    };
    let result = test_ctx.branch.add_series(&ctx, reference, None);
    assert_eq!(
        result.err().unwrap().to_string(),
        format!(
            "The commit {} has a change id associated with it. Use the change id instead",
            test_ctx.commits[1].id()
        )
    );
    Ok(())
}

#[test]
fn add_series_invalid_name_fails() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let reference = PatchReference {
        name: "name with spaces".into(),
        target: CommitOrChangeId::CommitId(test_ctx.commits[0].id().to_string()),
        description: None,
    };
    let result = test_ctx.branch.add_series(&ctx, reference, None);
    assert_eq!(result.err().unwrap().to_string(), "Invalid branch name");
    Ok(())
}

#[test]
fn add_series_duplicate_name_fails() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let reference = PatchReference {
        name: "asdf".into(),
        target: CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap()),
        description: None,
    };
    let result = test_ctx.branch.add_series(&ctx, reference.clone(), None);
    assert!(result.is_ok());
    let result = test_ctx.branch.add_series(&ctx, reference, None);
    assert_eq!(
        result.err().unwrap().to_string(),
        "A patch reference with the name asdf exists"
    );
    Ok(())
}

#[test]
fn add_series_matching_git_ref_fails() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let reference = PatchReference {
        name: "existing-branch".into(),
        target: CommitOrChangeId::CommitId(test_ctx.commits[0].id().to_string()),
        description: None,
    };
    let result = test_ctx.branch.add_series(&ctx, reference.clone(), None);
    assert_eq!(
        result.err().unwrap().to_string(),
        "A git reference with the name existing-branch exists"
    );
    Ok(())
}

#[test]
fn add_series_including_refs_head_fails() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let reference = PatchReference {
        name: "refs/heads/my-branch".into(),
        target: CommitOrChangeId::CommitId(test_ctx.commits[0].id().to_string()),
        description: None,
    };
    let result = test_ctx.branch.add_series(&ctx, reference.clone(), None);
    assert_eq!(
        result.err().unwrap().to_string(),
        "Stack head name cannot start with 'refs/heads'"
    );
    Ok(())
}

#[test]
fn add_series_target_commit_doesnt_exist() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let reference = PatchReference {
        name: "my-branch".into(),
        target: CommitOrChangeId::CommitId("30696678319e0fa3a20e54f22d47fc8cf1ceaade".into()), // does not exist
        description: None,
    };
    let result = test_ctx.branch.add_series(&ctx, reference.clone(), None);
    assert!(result
        .err()
        .unwrap()
        .to_string()
        .contains("object not found"),);
    Ok(())
}

#[test]
fn add_series_target_change_id_doesnt_exist() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let reference = PatchReference {
        name: "my-branch".into(),
        target: CommitOrChangeId::ChangeId("does-not-exist".into()), // does not exist
        description: None,
    };
    let result = test_ctx.branch.add_series(&ctx, reference.clone(), None);
    assert_eq!(
        result.err().unwrap().to_string(),
        "Commit with change id does-not-exist not found"
    );
    Ok(())
}

#[test]
fn add_series_target_commit_not_in_stack() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let other_commit_id = test_ctx.other_commits.last().unwrap().id().to_string();
    let reference = PatchReference {
        name: "my-branch".into(),
        target: CommitOrChangeId::CommitId(other_commit_id.clone()), // does not exist
        description: None,
    };
    let result = test_ctx.branch.add_series(&ctx, reference.clone(), None);
    assert_eq!(
        result.err().unwrap().to_string(),
        format!(
            "The commit {} is not between the stack head and the stack base",
            other_commit_id
        )
    );
    Ok(())
}

#[test]
fn remove_series_last_fails() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let result = test_ctx
        .branch
        .remove_series(&ctx, test_ctx.branch.heads[0].name.clone());
    assert_eq!(
        result.err().unwrap().to_string(),
        "Cannot remove the last branch from the stack"
    );
    Ok(())
}

#[test]
fn remove_series_nonexistent_fails() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let result = test_ctx
        .branch
        .remove_series(&ctx, "does-not-exist".to_string());
    assert_eq!(
        result.err().unwrap().to_string(),
        "Series with name does-not-exist not found"
    );
    Ok(())
}

#[test]
fn remove_series_with_multiple_last_heads() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;

    assert_eq!(test_ctx.branch.heads.len(), 1);
    assert_eq!(head_names(&test_ctx), vec!["virtual"]); // defalts to stack name
    let default_head = test_ctx.branch.heads[0].clone();

    let to_stay = PatchReference {
        name: "to_stay".into(),
        target: CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap()),
        description: None,
    };
    let result = test_ctx.branch.add_series(&ctx, to_stay.clone(), None);
    assert!(result.is_ok());
    assert_eq!(head_names(&test_ctx), vec!["to_stay", "virtual"]);

    let result = test_ctx
        .branch
        .remove_series(&ctx, default_head.name.clone());
    assert!(result.is_ok());
    assert_eq!(head_names(&test_ctx), vec!["to_stay"]);
    assert_eq!(
        test_ctx.branch.heads[0].target,
        CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap())
    ); // it references the newest commit
    Ok(())
}

#[test]
fn remove_series_no_orphan_commits() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;

    assert_eq!(test_ctx.branch.heads.len(), 1);
    assert_eq!(head_names(&test_ctx), vec!["virtual"]); // defalts to stack name
    let default_head = test_ctx.branch.heads[0].clone(); // references the newest commit

    let to_stay = PatchReference {
        name: "to_stay".into(),
        target: CommitOrChangeId::ChangeId(test_ctx.commits.first().unwrap().change_id().unwrap()),
        description: None,
    }; // references the oldest commit
    let result = test_ctx.branch.add_series(&ctx, to_stay.clone(), None);
    assert!(result.is_ok());
    assert_eq!(head_names(&test_ctx), vec!["to_stay", "virtual"]);

    let result = test_ctx
        .branch
        .remove_series(&ctx, default_head.name.clone());
    assert!(result.is_ok());
    assert_eq!(head_names(&test_ctx), vec!["to_stay"]);
    assert_eq!(
        test_ctx.branch.heads[0].target,
        CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap())
    ); // it was updated to reference the newest commit
    Ok(())
}

#[test]
fn update_series_noop_does_nothing() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let heads_before = test_ctx.branch.heads.clone();
    let noop_update = PatchReferenceUpdate::default();
    let result = test_ctx
        .branch
        .update_series(&ctx, "virtual".into(), &noop_update);
    assert!(result.is_ok());
    assert_eq!(test_ctx.branch.heads, heads_before);
    Ok(())
}

#[test]
fn update_series_name_fails_validation() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let update = PatchReferenceUpdate {
        name: Some("invalid name".into()),
        target_update: None,
        description: None,
    };
    let result = test_ctx
        .branch
        .update_series(&ctx, "virtual".into(), &update);
    assert_eq!(result.err().unwrap().to_string(), "Invalid branch name");
    Ok(())
}

#[test]
fn update_series_name_success() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let update = PatchReferenceUpdate {
        name: Some("new-name".into()),
        target_update: None,
        description: None,
    };
    let result = test_ctx
        .branch
        .update_series(&ctx, "virtual".into(), &update);
    assert!(result.is_ok());
    assert_eq!(test_ctx.branch.heads[0].name, "new-name");
    // Assert persisted
    assert_eq!(
        test_ctx.branch,
        test_ctx.handle.get_branch(test_ctx.branch.id)?
    );
    Ok(())
}

#[test]
fn update_series_set_description() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let update = PatchReferenceUpdate {
        name: None,
        target_update: None,
        description: Some(Some("my description".into())),
    };
    let result = test_ctx
        .branch
        .update_series(&ctx, "virtual".into(), &update);
    assert!(result.is_ok());
    assert_eq!(
        test_ctx.branch.heads[0].description,
        Some("my description".into())
    );
    // Assert persisted
    assert_eq!(
        test_ctx.branch,
        test_ctx.handle.get_branch(test_ctx.branch.id)?
    );
    Ok(())
}

#[test]
fn update_series_target_fails_commit_not_in_stack() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let other_commit_id = test_ctx.other_commits.last().unwrap().id().to_string();
    let update = PatchReferenceUpdate {
        name: None,
        target_update: Some(TargetUpdate {
            target: CommitOrChangeId::CommitId(other_commit_id.clone()),
            preceding_head: None,
        }),
        description: None,
    };
    let result = test_ctx
        .branch
        .update_series(&ctx, "virtual".into(), &update);
    assert_eq!(
        result.err().unwrap().to_string(),
        format!(
            "The commit {} is not between the stack head and the stack base",
            other_commit_id
        )
    );
    Ok(())
}

#[test]
fn update_series_target_orphan_commit_fails() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let initial_state = test_ctx.branch.heads.clone();
    let first_commit_change_id = test_ctx.commits.first().unwrap().change_id().unwrap();
    let update = PatchReferenceUpdate {
        name: Some("new-lol".into()),
        target_update: Some(TargetUpdate {
            target: CommitOrChangeId::ChangeId(first_commit_change_id.clone()),
            preceding_head: None,
        }),
        description: None,
    };
    let result = test_ctx
        .branch
        .update_series(&ctx, "virtual".into(), &update);

    assert_eq!(
        result.err().unwrap().to_string(),
        "This update would cause orphaned patches, which is disallowed"
    );
    assert_eq!(initial_state, test_ctx.branch.heads); // no change due to failure
    Ok(())
}

#[test]
fn update_series_target_success() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let commit_0_change_id = CommitOrChangeId::ChangeId(test_ctx.commits[0].change_id().unwrap());
    let series_1 = PatchReference {
        name: "series_1".into(),
        target: commit_0_change_id.clone(),
        description: None,
    };
    let result = test_ctx.branch.add_series(&ctx, series_1, None);
    assert!(result.is_ok());
    assert_eq!(test_ctx.branch.heads[0].target, commit_0_change_id);
    let commit_1_change_id = CommitOrChangeId::ChangeId(test_ctx.commits[1].change_id().unwrap());
    let update = PatchReferenceUpdate {
        name: None,
        target_update: Some(TargetUpdate {
            target: commit_1_change_id.clone(),
            preceding_head: None,
        }),
        description: None,
    };
    let result = test_ctx
        .branch
        .update_series(&ctx, "series_1".into(), &update);
    assert!(result.is_ok());
    assert_eq!(test_ctx.branch.heads[0].target, commit_1_change_id);
    // Assert persisted
    assert_eq!(
        test_ctx.branch,
        test_ctx.handle.get_branch(test_ctx.branch.id)?
    );
    Ok(())
}

#[test]
fn push_series_no_remote() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let result = test_ctx.branch.push_series(&ctx, "virtual".into(), false);
    assert_eq!(
        result.err().unwrap().to_string(),
        "No remote has been configured for the target branch"
    );
    Ok(())
}

#[test]
fn push_series_success() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;

    let state = VirtualBranchesHandle::new(ctx.project().gb_dir());
    let mut target = state.get_default_target()?;
    target.push_remote_name = Some("origin".into());
    state.set_default_target(target)?;

    let result = test_ctx.branch.push_series(&ctx, "virtual".into(), false);
    assert!(result.is_ok());
    Ok(())
}

#[test]
fn update_name_after_push() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;

    let state = VirtualBranchesHandle::new(ctx.project().gb_dir());
    let mut target = state.get_default_target()?;
    target.push_remote_name = Some("origin".into());
    state.set_default_target(target)?;

    let result = test_ctx.branch.push_series(&ctx, "virtual".into(), false);
    assert!(result.is_ok());
    let result = test_ctx.branch.update_series(
        &ctx,
        "virtual".into(),
        &PatchReferenceUpdate {
            name: Some("new-name".into()),
            ..Default::default()
        },
    );
    assert!(result.is_err());
    assert_eq!(
        result.err().unwrap().to_string(),
        "Cannot update the name of a head that has been pushed to a remote"
    );
    Ok(())
}

#[test]
fn list_series_default_head() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let result = test_ctx.branch.list_series(&ctx);
    assert!(result.is_ok());
    let result = result.unwrap();
    // the number of series matches the number of heads
    assert_eq!(result.len(), test_ctx.branch.heads.len());
    assert_eq!(result[0].head.name, "virtual");
    let expected_patches = test_ctx
        .commits
        .iter()
        .map(|c| CommitOrChangeId::ChangeId(c.change_id().unwrap()))
        .collect_vec();
    assert_eq!(result[0].local_commits, expected_patches);
    Ok(())
}

#[test]
fn list_series_two_heads_same_commit() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let head_before = PatchReference {
        name: "head_before".into(),
        target: CommitOrChangeId::ChangeId(test_ctx.commits.last().unwrap().change_id().unwrap()),
        description: None,
    };
    // add `head_before` before the initial head
    let result = test_ctx.branch.add_series(&ctx, head_before, None);
    assert!(result.is_ok());

    let result = test_ctx.branch.list_series(&ctx);
    assert!(result.is_ok());
    let result = result.unwrap();

    // the number of series matches the number of heads
    assert_eq!(result.len(), test_ctx.branch.heads.len());

    let expected_patches = test_ctx
        .commits
        .iter()
        .map(|c| CommitOrChangeId::ChangeId(c.change_id().unwrap()))
        .collect_vec();
    // Expect the commits to be part of the `head_before`
    assert_eq!(result[0].local_commits, expected_patches);
    assert_eq!(result[0].head.name, "head_before");
    assert_eq!(result[1].local_commits, vec![]);
    assert_eq!(result[1].head.name, "virtual");
    Ok(())
}

#[test]
fn list_series_two_heads_different_commit() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    test_ctx.branch.initialize(&ctx)?;
    let head_before = PatchReference {
        name: "head_before".into(),
        // point to the first commit
        target: CommitOrChangeId::ChangeId(test_ctx.commits.first().unwrap().change_id().unwrap()),
        description: None,
    };
    // add `head_before` before the initial head
    let result = test_ctx.branch.add_series(&ctx, head_before, None);
    assert!(result.is_ok());
    let result = test_ctx.branch.list_series(&ctx);
    assert!(result.is_ok());
    let result = result.unwrap();
    // the number of series matches the number of heads
    assert_eq!(result.len(), test_ctx.branch.heads.len());
    let mut expected_patches = test_ctx
        .commits
        .iter()
        .map(|c| CommitOrChangeId::ChangeId(c.change_id().unwrap()))
        .collect_vec();
    assert_eq!(result[0].local_commits, vec![expected_patches.remove(0)]); // the first patch is in the first series
    assert_eq!(result[0].head.name, "head_before");
    assert_eq!(expected_patches.len(), 2);
    assert_eq!(result[1].local_commits, expected_patches); // the other two patches are in the second series
    assert_eq!(result[1].head.name, "virtual");

    Ok(())
}

#[test]
fn set_stack_head_commit_from_other_stack() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    let result = test_ctx
        .branch
        .set_stack_head(&ctx, test_ctx.other_commits.first().unwrap().id());
    assert!(result.is_err());
    Ok(())
}

#[test]
fn set_stack_head_commit_not_head() -> Result<()> {
    let (ctx, _temp_dir) = command_ctx("multiple-commits")?;
    let mut test_ctx = test_ctx(&ctx)?;
    let result = test_ctx
        .branch
        .set_stack_head(&ctx, test_ctx.commits.get(1).unwrap().id());
    assert!(result.is_err());
    assert_eq!(
        result.err().unwrap().to_string(),
        format!(
            "The commit {} is not the head of the stack",
            test_ctx.commits[1].id()
        )
    );
    Ok(())
}

fn command_ctx(name: &str) -> Result<(CommandContext, TempDir)> {
    gitbutler_testsupport::writable::fixture("stacking.sh", name)
}

fn head_names(test_ctx: &TestContext) -> Vec<String> {
    test_ctx
        .branch
        .heads
        .iter()
        .map(|h| h.name.clone())
        .collect_vec()
}

fn test_ctx(ctx: &CommandContext) -> Result<TestContext> {
    let handle = VirtualBranchesHandle::new(ctx.project().gb_dir());
    let branches = handle.list_all_branches()?;
    let branch = branches.iter().find(|b| b.name == "virtual").unwrap();
    let other_branch = branches.iter().find(|b| b.name != "virtual").unwrap();
    let target = handle.get_default_target()?;
    let mut branch_commits = ctx
        .repository()
        .log(branch.head(), LogUntil::Commit(target.sha))?;
    branch_commits.reverse();
    let mut other_commits = ctx
        .repository()
        .log(other_branch.head(), LogUntil::Commit(target.sha))?;
    other_commits.reverse();
    Ok(TestContext {
        branch: branch.clone(),
        commits: branch_commits,
        // other_branch: other_branch.clone(),
        other_commits,
        handle,
        default_target: target,
    })
}
struct TestContext<'a> {
    branch: gitbutler_stack::Branch,
    /// Oldest commit first
    commits: Vec<git2::Commit<'a>>,
    /// Oldest commit first
    #[allow(dead_code)]
    other_commits: Vec<git2::Commit<'a>>,
    handle: VirtualBranchesHandle,
    default_target: gitbutler_stack::Target,
}