// Copyright 2024 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::path::Path;
use std::path::PathBuf;

use indoc::formatdoc;
use test_case::test_case;
use testutils::TestResult;
use testutils::git;

use crate::common::CommandOutput;
use crate::common::TestEnvironment;
use crate::common::TestWorkDir;
use crate::common::to_toml_value;

fn init_git_repo(git_repo_path: &Path, bare: bool) -> gix::Repository {
    let git_repo = if bare {
        git::init_bare(git_repo_path)
    } else {
        git::init(git_repo_path)
    };

    let git::CommitResult { commit_id, .. } = git::add_commit(
        &git_repo,
        "refs/heads/my-bookmark",
        "some-file",
        b"some content",
        "My commit message",
        &[],
    );
    git::set_head_to_id(&git_repo, commit_id);
    git_repo
}

#[must_use]
fn get_bookmark_output(work_dir: &TestWorkDir) -> CommandOutput {
    work_dir.run_jj(["bookmark", "list", "--all-remotes"])
}

#[must_use]
fn get_log_output(work_dir: &TestWorkDir) -> CommandOutput {
    let template = r#"
    separate(" ",
      commit_id.short(),
      bookmarks,
      description,
    )"#;
    work_dir.run_jj(["log", "-T", template, "-r=all()"])
}

#[must_use]
fn get_colocation_status(work_dir: &TestWorkDir) -> CommandOutput {
    work_dir.run_jj([
        "git",
        "colocation",
        "status",
        "--ignore-working-copy",
        "--quiet", // suppress hint
    ])
}

fn read_git_target(work_dir: &TestWorkDir) -> String {
    String::from_utf8(work_dir.read_file(".jj/repo/store/git_target").into()).unwrap()
}

#[test]
fn test_git_init_internal() {
    let test_env = TestEnvironment::default();
    test_env.add_config("git.colocate = false");
    let output = test_env.run_jj_in(".", ["git", "init", "repo"]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Initialized repo in "repo"
    [EOF]
    "#);

    let work_dir = test_env.work_dir("repo");
    let jj_path = work_dir.root().join(".jj");
    let repo_path = jj_path.join("repo");
    let store_path = repo_path.join("store");
    assert!(work_dir.root().is_dir());
    assert!(jj_path.is_dir());
    assert!(jj_path.join("working_copy").is_dir());
    assert!(repo_path.is_dir());
    assert!(store_path.is_dir());
    assert!(store_path.join("git").is_dir());
    assert_eq!(read_git_target(&work_dir), "git");
}

#[test]
fn test_git_init_internal_preexisting_git_repo() {
    let test_env = TestEnvironment::default();
    test_env.add_config("git.colocate = false");
    test_env.work_dir("").create_dir_all("repo/.git");
    let output = test_env.run_jj_in(".", ["git", "init", "repo"]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Error: Did not create a jj repo because there is an existing Git repo in this directory.
    Hint: To create a repo backed by the existing Git repo, run `jj git init --colocate` instead.
    [EOF]
    [exit status: 1]
    ");
}

#[test]
fn test_git_init_internal_no_integrate_operation() {
    let test_env = TestEnvironment::default();
    let workspace_root = test_env.env_root().join("repo");
    std::fs::create_dir(&workspace_root).unwrap();

    let output = test_env.run_jj_in(
        &workspace_root,
        &["git", "init", "--no-integrate-operation"],
    );
    insta::assert_snapshot!(output, @r"
    ------- stderr -------
    Error: --no-integrate-operation is not respected
    [EOF]
    [exit status: 2]
    ");
}

#[test]
fn test_git_init_ignore_working_copy() {
    let test_env = TestEnvironment::default();
    test_env.add_config("git.colocate = true");
    let work_dir = test_env.work_dir("").create_dir("repo");
    work_dir.write_file("file1", "");

    let output = work_dir.run_jj(["git", "init", "--ignore-working-copy"]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Error: --ignore-working-copy is not respected
    [EOF]
    [exit status: 2]
    ");
}

#[test]
fn test_git_init_at_operation() {
    let test_env = TestEnvironment::default();
    test_env.add_config("git.colocate = true");
    let work_dir = test_env.work_dir("").create_dir("repo");

    let output = work_dir.run_jj(["git", "init", "--at-op=@-"]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Error: --at-op is not respected
    [EOF]
    [exit status: 2]
    ");
}

#[test_case(false; "full")]
#[test_case(true; "bare")]
fn test_git_init_external(bare: bool) {
    let test_env = TestEnvironment::default();
    let git_repo_path = test_env.env_root().join("git-repo");
    init_git_repo(&git_repo_path, bare);

    // Explicitly enable git.colocate (which is also the default)
    test_env.add_config("git.colocate = true");

    // --git-repo takes precedence over git.colocate=true
    let output = test_env.run_jj_in(
        ".",
        [
            "git",
            "init",
            "repo",
            "--git-repo",
            git_repo_path.to_str().unwrap(),
        ],
    );
    insta::allow_duplicates! {
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Done importing changes from the underlying Git repo.
    Working copy  (@) now at: sqpuoqvx ed6b5138 (empty) (no description set)
    Parent commit (@-)      : nntyzxmz e80a42cc my-bookmark | My commit message
    Added 1 files, modified 0 files, removed 0 files
    Initialized repo in "repo"
    [EOF]
    "#);
    }

    let work_dir = test_env.work_dir("repo");
    let jj_path = work_dir.root().join(".jj");
    let repo_path = jj_path.join("repo");
    let store_path = repo_path.join("store");
    assert!(work_dir.root().is_dir());
    assert!(jj_path.is_dir());
    assert!(jj_path.join("working_copy").is_dir());
    assert!(repo_path.is_dir());
    assert!(store_path.is_dir());
    let unix_git_target_file_contents = read_git_target(&work_dir).replace('\\', "/");
    if bare {
        assert!(unix_git_target_file_contents.ends_with("/git-repo"));
    } else {
        assert!(unix_git_target_file_contents.ends_with("/git-repo/.git"));
    }

    // Check that the Git repo's HEAD got checked out
    insta::allow_duplicates! {
        insta::assert_snapshot!(get_log_output(&work_dir), @"
        @  ed6b513890ae
        ○  e80a42cccd06 my-bookmark My commit message
        ◆  000000000000
        [EOF]
        ");
        insta::assert_snapshot!(get_colocation_status(&work_dir), @"
        Workspace is currently not colocated with Git.
        Last imported/exported Git HEAD: e80a42cccd069007c7a2bb427ac7f1d10b408633
        [EOF]
        ");
    }
}

#[test]
fn test_git_init_external_with_colocate_config() {
    let test_env = TestEnvironment::default();
    let git_repo_path = test_env.env_root().join("git-repo");
    init_git_repo(&git_repo_path, true);

    // Explicitly enable git.colocate (which is also the default)
    test_env.add_config("git.colocate = true");

    // --git-repo takes precedence over git.colocate.true
    let output = test_env.run_jj_in(
        ".",
        [
            "git",
            "init",
            "repo",
            "--git-repo",
            git_repo_path.to_str().unwrap(),
        ],
    );
    insta::allow_duplicates! {
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Done importing changes from the underlying Git repo.
    Working copy  (@) now at: sqpuoqvx ed6b5138 (empty) (no description set)
    Parent commit (@-)      : nntyzxmz e80a42cc my-bookmark | My commit message
    Added 1 files, modified 0 files, removed 0 files
    Initialized repo in "repo"
    [EOF]
    "#);
    }
}

#[test_case(false; "full")]
#[test_case(true; "bare")]
fn test_git_init_external_import_trunk(bare: bool) -> TestResult {
    let test_env = TestEnvironment::default();
    let git_repo_path = test_env.env_root().join("git-repo");
    let git_repo = init_git_repo(&git_repo_path, bare);

    // Explicitly enable git.colocate (which is also the default)
    test_env.add_config("git.colocate = true");

    // Add remote bookmark "trunk" for remote "origin", and set it as "origin/HEAD"
    let oid = git_repo.find_reference("refs/heads/my-bookmark")?.id();

    git_repo.reference(
        "refs/remotes/origin/trunk",
        oid.detach(),
        gix::refs::transaction::PreviousValue::MustNotExist,
        "create remote ref",
    )?;

    git::set_symbolic_reference(
        &git_repo,
        "refs/remotes/origin/HEAD",
        "refs/remotes/origin/trunk",
    );

    let output = test_env.run_jj_in(
        ".",
        [
            "git",
            "init",
            "repo",
            "--git-repo",
            git_repo_path.to_str().unwrap(),
        ],
    );
    insta::allow_duplicates! {
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Done importing changes from the underlying Git repo.
    Setting the revset alias `trunk()` to `trunk@origin`
    Working copy  (@) now at: sqpuoqvx ed6b5138 (empty) (no description set)
    Parent commit (@-)      : nntyzxmz e80a42cc my-bookmark trunk@origin | My commit message
    Added 1 files, modified 0 files, removed 0 files
    Initialized repo in "repo"
    [EOF]
    "#);
    }

    // "trunk()" alias should be set to remote "origin"'s default bookmark "trunk"
    let work_dir = test_env.work_dir("repo");
    let output = work_dir.run_jj(["config", "list", "--repo", "revset-aliases.\"trunk()\""]);
    insta::allow_duplicates! {
        insta::assert_snapshot!(output, @r#"
        revset-aliases."trunk()" = "trunk@origin"
        [EOF]
        "#);
    }
    Ok(())
}

#[test]
fn test_git_init_external_import_trunk_upstream_takes_precedence() -> TestResult {
    let test_env = TestEnvironment::default();
    let git_repo_path = test_env.env_root().join("git-repo");
    let git_repo = init_git_repo(&git_repo_path, false);

    // Explicitly enable git.colocate (which is also the default)
    test_env.add_config("git.colocate = true");

    let oid = git_repo.find_reference("refs/heads/my-bookmark")?.id();

    // Add both upstream and origin remotes with different default branches
    // upstream has "develop" as default
    git_repo.reference(
        "refs/remotes/upstream/develop",
        oid.detach(),
        gix::refs::transaction::PreviousValue::MustNotExist,
        "create upstream remote ref",
    )?;

    git::set_symbolic_reference(
        &git_repo,
        "refs/remotes/upstream/HEAD",
        "refs/remotes/upstream/develop",
    );

    // origin has "trunk" as default
    git_repo.reference(
        "refs/remotes/origin/trunk",
        oid.detach(),
        gix::refs::transaction::PreviousValue::MustNotExist,
        "create origin remote ref",
    )?;

    git::set_symbolic_reference(
        &git_repo,
        "refs/remotes/origin/HEAD",
        "refs/remotes/origin/trunk",
    );

    // also accepts full .git path
    let output = test_env.run_jj_in(
        ".",
        [
            "git",
            "init",
            "repo",
            "--git-repo",
            git_repo_path.join(".git").to_str().unwrap(),
        ],
    );
    insta::allow_duplicates! {
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Done importing changes from the underlying Git repo.
    Setting the revset alias `trunk()` to `develop@upstream`
    Working copy  (@) now at: sqpuoqvx ed6b5138 (empty) (no description set)
    Parent commit (@-)      : nntyzxmz e80a42cc develop@upstream my-bookmark trunk@origin | My commit message
    Added 1 files, modified 0 files, removed 0 files
    Initialized repo in "repo"
    [EOF]
    "#);
    }

    // "trunk()" alias should be set to "upstream"'s default, not "origin"'s
    let work_dir = test_env.work_dir("repo");
    let output = work_dir.run_jj(["config", "list", "--repo", "revset-aliases.\"trunk()\""]);
    insta::allow_duplicates! {
        insta::assert_snapshot!(output, @r#"
        revset-aliases."trunk()" = "develop@upstream"
        [EOF]
        "#);
    }
    Ok(())
}

#[test]
fn test_git_init_external_non_existent_directory() {
    let test_env = TestEnvironment::default();
    test_env.add_config("git.colocate = true");
    let output = test_env.run_jj_in(".", ["git", "init", "repo", "--git-repo", "non-existent"]);
    insta::assert_snapshot!(output.strip_stderr_last_line(), @"
    ------- stderr -------
    Error: Failed to access the repository
    Caused by:
    1: Cannot access $TEST_ENV/non-existent
    [EOF]
    [exit status: 1]
    ");
}

#[test]
fn test_git_init_external_non_existent_git_directory() {
    let test_env = TestEnvironment::default();
    test_env.add_config("git.colocate = true");
    let work_dir = test_env.work_dir("repo");
    let output = test_env.run_jj_in(".", ["git", "init", "repo", "--git-repo", "repo"]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Error: Failed to access the repository
    Caused by:
    1: Failed to open git repository
    2: "$TEST_ENV/repo" does not appear to be a git repository
    3: Missing HEAD at '.git/HEAD'
    [EOF]
    [exit status: 1]
    "#);
    let jj_path = work_dir.root().join(".jj");
    assert!(!jj_path.exists());
}

#[test]
fn test_git_init_colocated_via_git_repo_path() {
    let test_env = TestEnvironment::default();
    test_env.add_config("git.colocate = true");
    let work_dir = test_env.work_dir("repo");
    init_git_repo(work_dir.root(), false);
    let output = work_dir.run_jj(["git", "init", "--git-repo", "."]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Done importing changes from the underlying Git repo.
    Initialized repo in "."
    [EOF]
    "#);

    let jj_path = work_dir.root().join(".jj");
    let repo_path = jj_path.join("repo");
    let store_path = repo_path.join("store");
    assert!(work_dir.root().is_dir());
    assert!(jj_path.is_dir());
    assert!(jj_path.join("working_copy").is_dir());
    assert!(repo_path.is_dir());
    assert!(store_path.is_dir());
    assert!(
        read_git_target(&work_dir)
            .replace('\\', "/")
            .ends_with("../../../.git")
    );

    // Check that the Git repo's HEAD got checked out
    insta::assert_snapshot!(get_log_output(&work_dir), @"
    @  f3fe58bc88cc
    ○  e80a42cccd06 my-bookmark My commit message
    ◆  000000000000
    [EOF]
    ");
    insta::assert_snapshot!(get_colocation_status(&work_dir), @"
    Workspace is currently colocated with Git.
    Last imported/exported Git HEAD: e80a42cccd069007c7a2bb427ac7f1d10b408633
    [EOF]
    ");

    // Check that the Git repo's HEAD moves
    work_dir.run_jj(["new"]).success();
    insta::assert_snapshot!(get_log_output(&work_dir), @"
    @  bacc067e7740
    ○  f3fe58bc88cc
    ○  e80a42cccd06 my-bookmark My commit message
    ◆  000000000000
    [EOF]
    ");
    insta::assert_snapshot!(get_colocation_status(&work_dir), @"
    Workspace is currently colocated with Git.
    Last imported/exported Git HEAD: f3fe58bc88ccfb820b930a21297d8e48bf76ac2a
    [EOF]
    ");
}

#[test]
fn test_git_init_colocated_via_git_repo_path_gitlink() {
    let test_env = TestEnvironment::default();
    test_env.add_config("git.colocate = true");
    // <jj_work_dir>/.git -> <git_repo_path>
    let git_repo_path = test_env.env_root().join("git-repo");
    let git_repo = init_git_repo(&git_repo_path, false);
    let jj_work_dir = test_env.work_dir("").create_dir("repo");
    git::create_gitlink(jj_work_dir.root(), git_repo.path());

    assert!(jj_work_dir.root().join(".git").is_file());
    let output = jj_work_dir.run_jj(["git", "init", "--git-repo", "."]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Done importing changes from the underlying Git repo.
    Initialized repo in "."
    [EOF]
    "#);
    insta::assert_snapshot!(read_git_target(&jj_work_dir), @"../../../.git");

    // Check that the Git repo's HEAD got checked out
    insta::assert_snapshot!(get_log_output(&jj_work_dir), @"
    @  f3fe58bc88cc
    ○  e80a42cccd06 my-bookmark My commit message
    ◆  000000000000
    [EOF]
    ");
    insta::assert_snapshot!(get_colocation_status(&jj_work_dir), @"
    Workspace is currently colocated with Git.
    Last imported/exported Git HEAD: e80a42cccd069007c7a2bb427ac7f1d10b408633
    [EOF]
    ");

    // Check that the Git repo's HEAD moves
    jj_work_dir.run_jj(["new"]).success();
    insta::assert_snapshot!(get_log_output(&jj_work_dir), @"
    @  bacc067e7740
    ○  f3fe58bc88cc
    ○  e80a42cccd06 my-bookmark My commit message
    ◆  000000000000
    [EOF]
    ");
    insta::assert_snapshot!(get_colocation_status(&jj_work_dir), @"
    Workspace is currently colocated with Git.
    Last imported/exported Git HEAD: f3fe58bc88ccfb820b930a21297d8e48bf76ac2a
    [EOF]
    ");
}

#[cfg(unix)]
#[test]
fn test_git_init_colocated_via_git_repo_path_symlink_directory() -> TestResult {
    let test_env = TestEnvironment::default();
    test_env.add_config("git.colocate = true");
    // <jj_work_dir>/.git -> <git_repo_path>
    let git_repo_path = test_env.env_root().join("git-repo");
    init_git_repo(&git_repo_path, false);
    let jj_work_dir = test_env.work_dir("").create_dir("repo");
    std::os::unix::fs::symlink(git_repo_path.join(".git"), jj_work_dir.root().join(".git"))?;
    let output = jj_work_dir.run_jj(["git", "init", "--git-repo", "."]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Done importing changes from the underlying Git repo.
    Initialized repo in "."
    [EOF]
    "#);
    insta::assert_snapshot!(read_git_target(&jj_work_dir), @"../../../.git");

    // Check that the Git repo's HEAD got checked out
    insta::assert_snapshot!(get_log_output(&jj_work_dir), @"
    @  f3fe58bc88cc
    ○  e80a42cccd06 my-bookmark My commit message
    ◆  000000000000
    [EOF]
    ");
    insta::assert_snapshot!(get_colocation_status(&jj_work_dir), @"
    Workspace is currently colocated with Git.
    Last imported/exported Git HEAD: e80a42cccd069007c7a2bb427ac7f1d10b408633
    [EOF]
    ");

    // Check that the Git repo's HEAD moves
    jj_work_dir.run_jj(["new"]).success();
    insta::assert_snapshot!(get_log_output(&jj_work_dir), @"
    @  bacc067e7740
    ○  f3fe58bc88cc
    ○  e80a42cccd06 my-bookmark My commit message
    ◆  000000000000
    [EOF]
    ");
    insta::assert_snapshot!(get_colocation_status(&jj_work_dir), @"
    Workspace is currently colocated with Git.
    Last imported/exported Git HEAD: f3fe58bc88ccfb820b930a21297d8e48bf76ac2a
    [EOF]
    ");
    Ok(())
}

#[cfg(unix)]
#[test]
fn test_git_init_colocated_via_git_repo_path_symlink_directory_without_bare_config() -> TestResult {
    let test_env = TestEnvironment::default();
    test_env.add_config("git.colocate = true");
    // <jj_work_dir>/.git -> <git_repo_path>
    let git_repo_path = test_env.env_root().join("git-repo.git");
    let jj_work_dir = test_env.work_dir("repo");
    // Set up git repo without core.bare set (as the "repo" tool would do.)
    // The core.bare config is deduced from the directory name.
    let git_repo = init_git_repo(jj_work_dir.root(), false);
    git::remove_config_value(git_repo, "config", "bare");

    std::fs::rename(jj_work_dir.root().join(".git"), &git_repo_path)?;
    std::os::unix::fs::symlink(&git_repo_path, jj_work_dir.root().join(".git"))?;
    let output = jj_work_dir.run_jj(["git", "init", "--git-repo", "."]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Done importing changes from the underlying Git repo.
    Initialized repo in "."
    [EOF]
    "#);
    insta::assert_snapshot!(read_git_target(&jj_work_dir), @"../../../.git");

    // Check that the Git repo's HEAD got checked out
    insta::assert_snapshot!(get_log_output(&jj_work_dir), @"
    @  f3fe58bc88cc
    ○  e80a42cccd06 my-bookmark My commit message
    ◆  000000000000
    [EOF]
    ");
    insta::assert_snapshot!(get_colocation_status(&jj_work_dir), @"
    Workspace is currently colocated with Git.
    Last imported/exported Git HEAD: e80a42cccd069007c7a2bb427ac7f1d10b408633
    [EOF]
    ");

    // Check that the Git repo's HEAD moves
    jj_work_dir.run_jj(["new"]).success();
    insta::assert_snapshot!(get_log_output(&jj_work_dir), @"
    @  bacc067e7740
    ○  f3fe58bc88cc
    ○  e80a42cccd06 my-bookmark My commit message
    ◆  000000000000
    [EOF]
    ");
    insta::assert_snapshot!(get_colocation_status(&jj_work_dir), @"
    Workspace is currently colocated with Git.
    Last imported/exported Git HEAD: f3fe58bc88ccfb820b930a21297d8e48bf76ac2a
    [EOF]
    ");
    Ok(())
}

#[cfg(unix)]
#[test]
fn test_git_init_colocated_via_git_repo_path_symlink_gitlink() -> TestResult {
    let test_env = TestEnvironment::default();
    test_env.add_config("git.colocate = true");
    // <jj_work_dir>/.git -> <git_workdir_path>/.git -> <git_repo_path>
    let git_repo_path = test_env.env_root().join("git-repo");
    let git_workdir_path = test_env.env_root().join("git-workdir");
    let git_repo = init_git_repo(&git_repo_path, false);
    std::fs::create_dir(&git_workdir_path)?;
    git::create_gitlink(&git_workdir_path, git_repo.path());
    assert!(git_workdir_path.join(".git").is_file());
    let jj_work_dir = test_env.work_dir("").create_dir("repo");
    std::os::unix::fs::symlink(
        git_workdir_path.join(".git"),
        jj_work_dir.root().join(".git"),
    )?;
    let output = jj_work_dir.run_jj(["git", "init", "--git-repo", "."]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Done importing changes from the underlying Git repo.
    Initialized repo in "."
    [EOF]
    "#);
    insta::assert_snapshot!(read_git_target(&jj_work_dir), @"../../../.git");

    // Check that the Git repo's HEAD got checked out
    insta::assert_snapshot!(get_log_output(&jj_work_dir), @"
    @  f3fe58bc88cc
    ○  e80a42cccd06 my-bookmark My commit message
    ◆  000000000000
    [EOF]
    ");
    insta::assert_snapshot!(get_colocation_status(&jj_work_dir), @"
    Workspace is currently colocated with Git.
    Last imported/exported Git HEAD: e80a42cccd069007c7a2bb427ac7f1d10b408633
    [EOF]
    ");

    // Check that the Git repo's HEAD moves
    jj_work_dir.run_jj(["new"]).success();
    insta::assert_snapshot!(get_log_output(&jj_work_dir), @"
    @  bacc067e7740
    ○  f3fe58bc88cc
    ○  e80a42cccd06 my-bookmark My commit message
    ◆  000000000000
    [EOF]
    ");
    insta::assert_snapshot!(get_colocation_status(&jj_work_dir), @"
    Workspace is currently colocated with Git.
    Last imported/exported Git HEAD: f3fe58bc88ccfb820b930a21297d8e48bf76ac2a
    [EOF]
    ");
    Ok(())
}

#[test]
fn test_git_init_colocated_via_git_repo_path_imported_refs() {
    let test_env = TestEnvironment::default();
    test_env.add_config("git.colocate = true");
    test_env.add_config("remotes.origin.auto-track-bookmarks = '*'");

    // Set up remote refs
    test_env
        .run_jj_in(".", ["git", "init", "--no-colocate", "remote"])
        .success();
    let remote_dir = test_env.work_dir("remote");
    remote_dir
        .run_jj(["bookmark", "create", "-r@", "local-remote", "remote-only"])
        .success();
    remote_dir.run_jj(["new"]).success();
    remote_dir.run_jj(["git", "export"]).success();

    let remote_git_path = remote_dir
        .root()
        .join(PathBuf::from_iter([".jj", "repo", "store", "git"]));
    let set_up_local_repo = |local_path: &Path| {
        let git_repo = git::clone(local_path, remote_git_path.to_str().unwrap(), None);
        let git_ref = git_repo
            .find_reference("refs/remotes/origin/local-remote")
            .unwrap();
        git_repo
            .reference(
                "refs/heads/local-remote",
                git_ref.target().id().to_owned(),
                gix::refs::transaction::PreviousValue::MustNotExist,
                "move local-remote bookmark",
            )
            .unwrap();
    };

    // With remotes.origin.auto-track-bookmarks = '*'
    let local_dir = test_env.work_dir("local1");
    set_up_local_repo(local_dir.root());
    let output = local_dir.run_jj(["git", "init", "--git-repo=."]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Done importing changes from the underlying Git repo.
    Initialized repo in "."
    [EOF]
    "#);
    insta::assert_snapshot!(get_bookmark_output(&local_dir), @"
    local-remote: qpvuntsm e8849ae1 (empty) (no description set)
      @git: qpvuntsm e8849ae1 (empty) (no description set)
      @origin: qpvuntsm e8849ae1 (empty) (no description set)
    remote-only: qpvuntsm e8849ae1 (empty) (no description set)
      @git: qpvuntsm e8849ae1 (empty) (no description set)
      @origin: qpvuntsm e8849ae1 (empty) (no description set)
    [EOF]
    ");

    // With remotes.origin.auto-track-bookmarks = '~*'
    test_env.add_config("remotes.origin.auto-track-bookmarks = '~*'");
    let local_dir = test_env.work_dir("local2");
    set_up_local_repo(local_dir.root());
    let output = local_dir.run_jj(["git", "init", "--git-repo=."]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Done importing changes from the underlying Git repo.
    Hint: The following remote bookmarks aren't associated with the existing local bookmarks:
      local-remote@origin
    Hint: Run the following command to keep local bookmarks updated on future pulls:
      jj bookmark track local-remote --remote=origin
    Initialized repo in "."
    [EOF]
    "#);
    insta::assert_snapshot!(get_bookmark_output(&local_dir), @"
    local-remote: qpvuntsm e8849ae1 (empty) (no description set)
      @git: qpvuntsm e8849ae1 (empty) (no description set)
    local-remote@origin: qpvuntsm e8849ae1 (empty) (no description set)
    remote-only@origin: qpvuntsm e8849ae1 (empty) (no description set)
    [EOF]
    ");
}

#[test]
fn test_git_init_colocated_dirty_working_copy() {
    let test_env = TestEnvironment::default();
    test_env.add_config("git.colocate = true");
    let work_dir = test_env.work_dir("repo");
    let git_repo = init_git_repo(work_dir.root(), false);

    let mut index_manager = git::IndexManager::new(&git_repo);

    index_manager.add_file("new-staged-file", b"new content");
    index_manager.add_file("some-file", b"new content");
    index_manager.sync_index();

    work_dir.write_file("unstaged-file", "new content");
    insta::assert_debug_snapshot!(git::status(&git_repo), @r#"
    [
        GitStatus {
            path: "new-staged-file",
            status: Index(
                Addition,
            ),
        },
        GitStatus {
            path: "some-file",
            status: Index(
                Modification,
            ),
        },
        GitStatus {
            path: "unstaged-file",
            status: Worktree(
                Added,
            ),
        },
    ]
    "#);

    let output = work_dir.run_jj(["git", "init", "--git-repo", "."]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Done importing changes from the underlying Git repo.
    Initialized repo in "."
    [EOF]
    "#);

    // Working-copy changes should have been snapshotted.
    let output = work_dir.run_jj(["log", "-s", "--ignore-working-copy"]);
    insta::assert_snapshot!(output, @"
    @  sqpuoqvx test.user@example.com 2001-02-03 08:05:07 6efc2a53
    │  (no description set)
    │  C {some-file => new-staged-file}
    │  M some-file
    │  C {some-file => unstaged-file}
    ○  nntyzxmz someone@example.org 1970-01-01 11:00:00 my-bookmark e80a42cc
    │  My commit message
    │  A some-file
    ◆  zzzzzzzz root() 00000000
    [EOF]
    ");

    // Git index should be consistent with the working copy parent. With the
    // current implementation, the index is unchanged. Since jj created new
    // working copy commit, it's also okay to update the index reflecting the
    // working copy commit or the working copy parent.
    insta::assert_debug_snapshot!(git::status(&git_repo), @r#"
    [
        GitStatus {
            path: ".jj/.gitignore",
            status: Worktree(
                Ignored,
            ),
        },
        GitStatus {
            path: ".jj/repo",
            status: Worktree(
                Ignored,
            ),
        },
        GitStatus {
            path: ".jj/working_copy",
            status: Worktree(
                Ignored,
            ),
        },
        GitStatus {
            path: "new-staged-file",
            status: Index(
                Addition,
            ),
        },
        GitStatus {
            path: "some-file",
            status: Index(
                Modification,
            ),
        },
        GitStatus {
            path: "unstaged-file",
            status: Worktree(
                IntentToAdd,
            ),
        },
    ]
    "#);
}

#[test]
fn test_git_init_external_but_git_dir_exists() {
    let test_env = TestEnvironment::default();
    let git_repo_path = test_env.env_root().join("git-repo");
    let work_dir = test_env.work_dir("repo");
    git::init(&git_repo_path);
    init_git_repo(work_dir.root(), false);

    // Explicitly enable git.colocate (which is also the default)
    test_env.add_config("git.colocate = true");

    // --git-repo takes precedence over git.colocate=true
    let output = work_dir.run_jj(["git", "init", "--git-repo", git_repo_path.to_str().unwrap()]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Initialized repo in "."
    [EOF]
    "#);

    // The local ".git" repository is unrelated, so no commits should be imported
    insta::assert_snapshot!(get_log_output(&work_dir), @"
    @  e8849ae12c70
    ◆  000000000000
    [EOF]
    ");

    // Check that Git HEAD is not set because this isn't a colocated workspace
    work_dir.run_jj(["new"]).success();
    insta::assert_snapshot!(get_log_output(&work_dir), @"
    @  1c1c95df80e5
    ○  e8849ae12c70
    ◆  000000000000
    [EOF]
    ");
    insta::assert_snapshot!(get_colocation_status(&work_dir), @"
    Workspace is currently not colocated with Git.
    Last imported/exported Git HEAD: (none)
    [EOF]
    ");
}

#[test]
fn test_git_init_colocated_via_flag_git_dir_exists() {
    let test_env = TestEnvironment::default();
    test_env.add_config("git.colocate = true");
    let work_dir = test_env.work_dir("repo");
    init_git_repo(work_dir.root(), false);

    let output = test_env.run_jj_in(".", ["git", "init", "--colocate", "repo"]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Done importing changes from the underlying Git repo.
    Initialized repo in "repo"
    [EOF]
    "#);

    // Check that the Git repo's HEAD got checked out
    insta::assert_snapshot!(get_log_output(&work_dir), @"
    @  f3fe58bc88cc
    ○  e80a42cccd06 my-bookmark My commit message
    ◆  000000000000
    [EOF]
    ");
    insta::assert_snapshot!(get_colocation_status(&work_dir), @"
    Workspace is currently colocated with Git.
    Last imported/exported Git HEAD: e80a42cccd069007c7a2bb427ac7f1d10b408633
    [EOF]
    ");

    // Check that the Git repo's HEAD moves
    work_dir.run_jj(["new"]).success();
    insta::assert_snapshot!(get_log_output(&work_dir), @"
    @  bacc067e7740
    ○  f3fe58bc88cc
    ○  e80a42cccd06 my-bookmark My commit message
    ◆  000000000000
    [EOF]
    ");
    insta::assert_snapshot!(get_colocation_status(&work_dir), @"
    Workspace is currently colocated with Git.
    Last imported/exported Git HEAD: f3fe58bc88ccfb820b930a21297d8e48bf76ac2a
    [EOF]
    ");
}

#[test]
fn test_git_init_colocated_via_config_git_dir_exists() {
    let test_env = TestEnvironment::default();
    let work_dir = test_env.work_dir("repo");
    init_git_repo(work_dir.root(), false);

    test_env.add_config("git.colocate = true");

    let output = test_env.run_jj_in(".", ["git", "init", "repo"]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Done importing changes from the underlying Git repo.
    Initialized repo in "repo"
    [EOF]
    "#);

    // Check that the Git repo's HEAD got checked out
    insta::assert_snapshot!(get_log_output(&work_dir), @"
    @  f3fe58bc88cc
    ○  e80a42cccd06 my-bookmark My commit message
    ◆  000000000000
    [EOF]
    ");
    insta::assert_snapshot!(get_colocation_status(&work_dir), @"
    Workspace is currently colocated with Git.
    Last imported/exported Git HEAD: e80a42cccd069007c7a2bb427ac7f1d10b408633
    [EOF]
    ");

    // Check that the Git repo's HEAD moves
    work_dir.run_jj(["new"]).success();
    insta::assert_snapshot!(get_log_output(&work_dir), @"
    @  bacc067e7740
    ○  f3fe58bc88cc
    ○  e80a42cccd06 my-bookmark My commit message
    ◆  000000000000
    [EOF]
    ");
    insta::assert_snapshot!(get_colocation_status(&work_dir), @"
    Workspace is currently colocated with Git.
    Last imported/exported Git HEAD: f3fe58bc88ccfb820b930a21297d8e48bf76ac2a
    [EOF]
    ");
}

#[test]
fn test_git_init_no_colocate() {
    let test_env = TestEnvironment::default();
    let work_dir = test_env.work_dir("repo");

    test_env.add_config("git.colocate = true");

    let output = test_env.run_jj_in(".", ["git", "init", "--no-colocate", "repo"]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Initialized repo in "repo"
    [EOF]
    "#);

    assert!(!work_dir.root().join(".git").exists());
}

#[test]
fn test_git_init_not_colocated_via_false_config() {
    let test_env = TestEnvironment::default();
    let work_dir = test_env.work_dir("repo");
    test_env.add_config("git.colocate = false");

    let output = test_env.run_jj_in(".", ["git", "init", "repo"]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Initialized repo in "repo"
    [EOF]
    "#);

    assert!(!work_dir.root().join(".git").exists());
    assert!(work_dir.root().join(".jj/repo/store/git").is_dir());
}

#[test]
fn test_git_init_colocated_via_flag_overrides_false_config() {
    let test_env = TestEnvironment::default();
    let work_dir = test_env.work_dir("repo");
    init_git_repo(work_dir.root(), false);
    test_env.add_config("git.colocate = false");

    // --colocate flag takes precedence over git.colocate=false
    let output = test_env.run_jj_in(".", ["git", "init", "--colocate", "repo"]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Done importing changes from the underlying Git repo.
    Initialized repo in "repo"
    [EOF]
    "#);

    insta::assert_snapshot!(get_log_output(&work_dir), @"
    @  f3fe58bc88cc
    ○  e80a42cccd06 my-bookmark My commit message
    ◆  000000000000
    [EOF]
    ");
    insta::assert_snapshot!(get_colocation_status(&work_dir), @"
    Workspace is currently colocated with Git.
    Last imported/exported Git HEAD: e80a42cccd069007c7a2bb427ac7f1d10b408633
    [EOF]
    ");
}

#[test]
fn test_git_init_colocated_via_flag_git_dir_not_exists() {
    let test_env = TestEnvironment::default();
    test_env.add_config("git.colocate = true");
    let work_dir = test_env.work_dir("repo");
    let output = test_env.run_jj_in(".", ["git", "init", "--colocate", "repo"]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Initialized repo in "repo"
    [EOF]
    "#);
    // No HEAD ref is available yet
    insta::assert_snapshot!(get_log_output(&work_dir), @"
    @  e8849ae12c70
    ◆  000000000000
    [EOF]
    ");
    insta::assert_snapshot!(get_colocation_status(&work_dir), @"
    Workspace is currently colocated with Git.
    Last imported/exported Git HEAD: (none)
    [EOF]
    ");

    // Create the default bookmark (create both in case we change the default)
    work_dir
        .run_jj(["bookmark", "create", "-r@", "main", "master"])
        .success();

    // If .git/HEAD pointed to the default bookmark, new working-copy commit would
    // be created on top.
    insta::assert_snapshot!(get_log_output(&work_dir), @"
    @  e8849ae12c70 main master
    ◆  000000000000
    [EOF]
    ");
    insta::assert_snapshot!(get_colocation_status(&work_dir), @"
    Workspace is currently colocated with Git.
    Last imported/exported Git HEAD: (none)
    [EOF]
    ");
}

#[test]
fn test_git_init_conditional_config() {
    let test_env = TestEnvironment::default();
    test_env.add_config("git.colocate = true");
    let old_workspace_dir = test_env.work_dir("old");
    let new_workspace_dir = test_env.work_dir("new");

    let run_jj = |work_dir: &TestWorkDir, args: &[&str]| {
        work_dir.run_jj_with(|cmd| {
            cmd.args(args)
                .env_remove("JJ_EMAIL")
                .env_remove("JJ_OP_HOSTNAME")
                .env_remove("JJ_OP_USERNAME")
        })
    };
    let log_template = r#"separate(' ', author.email(), description.first_line()) ++ "\n""#;
    let op_log_template = r#"separate(' ', user, description.first_line()) ++ "\n""#;

    // Override user.email and operation.username conditionally
    test_env.add_config(formatdoc! {"
        user.email = 'base@example.org'
        operation.hostname = 'base'
        operation.username = 'base'
        [[--scope]]
        --when.repositories = [{new_workspace_root}]
        user.email = 'new-repo@example.org'
        operation.username = 'new-repo'
        ",
        new_workspace_root = to_toml_value(new_workspace_dir.root().to_str().unwrap()),
    });

    // Override operation.hostname by repo config, which should be loaded into
    // the command settings, but shouldn't be copied to the new repo.
    run_jj(&test_env.work_dir(""), &["git", "init", "old"]).success();
    run_jj(
        &old_workspace_dir,
        &["config", "set", "--repo", "operation.hostname", "old-repo"],
    )
    .success();
    run_jj(&old_workspace_dir, &["new"]).success();
    let output = run_jj(&old_workspace_dir, &["op", "log", "-T", op_log_template]);
    insta::assert_snapshot!(output, @"
    @  base@old-repo new empty commit
    ○  base@base add workspace 'default'
    ○  @
    [EOF]
    ");

    // Create new repo at the old workspace directory.
    let output = run_jj(&old_workspace_dir, &["git", "init", "../new"]);
    insta::assert_snapshot!(output.normalize_backslash(), @r#"
    ------- stderr -------
    Initialized repo in "../new"
    [EOF]
    "#);
    run_jj(&new_workspace_dir, &["new"]).success();
    let output = run_jj(&new_workspace_dir, &["log", "-T", log_template]);
    insta::assert_snapshot!(output, @"
    @  new-repo@example.org
    ○  new-repo@example.org
    ◆
    [EOF]
    ");
    let output = run_jj(&new_workspace_dir, &["op", "log", "-T", op_log_template]);
    insta::assert_snapshot!(output, @"
    @  new-repo@base new empty commit
    ○  new-repo@base add workspace 'default'
    ○  @
    [EOF]
    ");
}

#[test]
fn test_git_init_bad_wc_path() -> TestResult {
    let test_env = TestEnvironment::default();
    test_env.add_config("git.colocate = true");
    std::fs::write(test_env.env_root().join("existing-file"), b"")?;
    let output = test_env.run_jj_in(".", ["git", "init", "existing-file"]);
    insta::assert_snapshot!(output.strip_stderr_last_line(), @"
    ------- stderr -------
    Error: Failed to create workspace
    [EOF]
    [exit status: 1]
    ");
    Ok(())
}

#[test]
fn test_git_init_with_invalid_gitlink() {
    let test_env = TestEnvironment::default();
    test_env.add_config("git.colocate = true");
    let work_dir = test_env.work_dir("repo");
    work_dir.write_file(".git", "invalid");

    // `jj git init --colocate` first checks for a worktree, make it fail
    // subsequent errors are not specific to colocation
    let output = work_dir.run_jj(["git", "init", "--colocate"]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Error: Failed to access the repository
    Caused by:
    1: Failed to open git repository
    2: "$TEST_ENV/repo/.git" does not appear to be a git repository
    3: Format should be 'gitdir: <path>', but got: "invalid"
    [EOF]
    [exit status: 1]
    "#);

    let output = work_dir.run_jj(["git", "init", "--git-repo", "."]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Error: Failed to access the repository
    Caused by:
    1: Failed to open git repository
    2: "$TEST_ENV/repo/.git" does not appear to be a git repository
    3: Format should be 'gitdir: <path>', but got: "invalid"
    [EOF]
    [exit status: 1]
    "#);
}

#[test]
fn test_git_init_colocate_in_git_worktree() {
    let test_env = TestEnvironment::default();
    test_env.add_config("git.colocate = true");
    let main_repo_path = test_env.env_root().join("main-repo");
    init_git_repo(&main_repo_path, false);

    // Create a Git worktree
    let worktree_path = test_env.env_root().join("worktree");
    let status = std::process::Command::new("git")
        .args([
            "worktree",
            "add",
            worktree_path.to_str().unwrap(),
            "-b",
            "worktree-branch",
        ])
        .current_dir(&main_repo_path)
        .status()
        .expect("git worktree add failed to spawn");
    assert!(status.success(), "git worktree add failed: {status}");

    // Verify .git is a file (gitlink)
    assert!(worktree_path.join(".git").is_file());

    // Try to init colocated jj repo - should fail
    let output = test_env.run_jj_in(
        worktree_path.to_str().unwrap(),
        ["git", "init", "--colocate"],
    );
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Error: Cannot create a colocated jj repo inside a Git worktree.
    Hint: Run `jj git init` in the main Git repository instead, or use `jj workspace add` to create additional jj workspaces.
    [EOF]
    [exit status: 1]
    ");

    // Verify no .jj directory was created
    assert!(!worktree_path.join(".jj").exists());
}

#[test]
fn test_git_init_colocate_gitlink_not_worktree() -> TestResult {
    // Test that a gitlink pointing to a path that contains "worktrees" in a
    // user directory (NOT in the .git/worktrees/<name> pattern) is NOT
    // incorrectly detected as a Git worktree
    let test_env = TestEnvironment::default();
    test_env.add_config("git.colocate = true");

    // Create a bare git repo at a path containing "worktrees" as a directory name
    let git_repo_path = test_env.env_root().join("worktrees").join("my-repo.git");
    std::fs::create_dir_all(&git_repo_path)?;
    init_git_repo(&git_repo_path, true);

    // Create a working directory with a gitlink pointing to that bare repo
    let work_dir = test_env.env_root().join("work");
    std::fs::create_dir_all(&work_dir)?;
    let gitlink_content = format!("gitdir: {}", git_repo_path.to_str().unwrap());
    std::fs::write(work_dir.join(".git"), gitlink_content)?;

    // Verify .git is a file (gitlink)
    assert!(work_dir.join(".git").is_file());

    // jj git init --colocate should succeed (not be blocked as a worktree)
    let output = test_env.run_jj_in(work_dir.to_str().unwrap(), ["git", "init", "--colocate"]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Done importing changes from the underlying Git repo.
    Initialized repo in "."
    [EOF]
    "#);

    // Verify .jj directory was created
    assert!(work_dir.join(".jj").exists());
    Ok(())
}
