//! Tests for squash-merge orchestration — basic cases.
use super::*;
use std::process::Command;

fn init_git_repo(repo: &std::path::Path) {
    Command::new("git")
        .args(["init"])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["config", "user.email", "test@test.com"])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["config", "user.name", "Test"])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "--allow-empty", "-m", "init"])
        .current_dir(repo)
        .output()
        .unwrap();
}

#[tokio::test]
async fn squash_merge_uses_merge_queue_no_conflict_markers_on_master() {
    use std::fs;
    use tempfile::tempdir;

    let tmp = tempdir().unwrap();
    let repo = tmp.path();
    init_git_repo(repo);

    // Create a file that will be conflicted on master.
    fs::write(repo.join("shared.txt"), "line 1\nline 2\n").unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "initial shared file"])
        .current_dir(repo)
        .output()
        .unwrap();

    // Create a feature branch that modifies the file.
    Command::new("git")
        .args(["checkout", "-b", "feature/story-conflict_test"])
        .current_dir(repo)
        .output()
        .unwrap();
    fs::write(
        repo.join("shared.txt"),
        "line 1\nline 2\nfeature addition\n",
    )
    .unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "feature: add line"])
        .current_dir(repo)
        .output()
        .unwrap();

    // Switch to master and make a conflicting change.
    Command::new("git")
        .args(["checkout", "master"])
        .current_dir(repo)
        .output()
        .unwrap();
    fs::write(repo.join("shared.txt"), "line 1\nline 2\nmaster addition\n").unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "master: add line"])
        .current_dir(repo)
        .output()
        .unwrap();

    // Run the squash merge.
    let result = run_squash_merge(repo, "feature/story-conflict_test", "conflict_test").unwrap();

    // Master should NEVER contain conflict markers, regardless of outcome.
    let master_content = fs::read_to_string(repo.join("shared.txt")).unwrap();
    assert!(
        !master_content.contains("<<<<<<<"),
        "master must never contain conflict markers, got:\n{master_content}"
    );
    assert!(
        !master_content.contains(">>>>>>>"),
        "master must never contain conflict markers, got:\n{master_content}"
    );

    // The merge should have had conflicts (returned as Conflict variant).
    assert!(
        matches!(result, super::MergeResult::Conflict { .. }),
        "should detect conflicts; got: {result:?}"
    );

    // Verify no leftover merge-queue branch.
    let branches = Command::new("git")
        .args(["branch", "--list", "merge-queue/*"])
        .current_dir(repo)
        .output()
        .unwrap();
    let branch_list = String::from_utf8_lossy(&branches.stdout);
    assert!(
        branch_list.trim().is_empty(),
        "merge-queue branch should be cleaned up, got: {branch_list}"
    );

    // Verify no leftover merge workspace directory.
    assert!(
        !repo.join(".huskies/merge_workspace").exists(),
        "merge workspace should be cleaned up"
    );
}

#[tokio::test]
async fn squash_merge_clean_merge_succeeds() {
    use std::fs;
    use tempfile::tempdir;

    let tmp = tempdir().unwrap();
    let repo = tmp.path();
    init_git_repo(repo);

    // Create feature branch with a new file.
    Command::new("git")
        .args(["checkout", "-b", "feature/story-clean_test"])
        .current_dir(repo)
        .output()
        .unwrap();
    fs::write(repo.join("new_file.txt"), "new content").unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "add new file"])
        .current_dir(repo)
        .output()
        .unwrap();

    // Switch back to master.
    Command::new("git")
        .args(["checkout", "master"])
        .current_dir(repo)
        .output()
        .unwrap();

    let result = run_squash_merge(repo, "feature/story-clean_test", "clean_test").unwrap();

    assert!(
        matches!(
            result,
            super::MergeResult::Success {
                conflicts_resolved: false,
                ..
            }
        ),
        "clean merge should succeed without conflicts; got: {result:?}"
    );
    assert!(
        repo.join("new_file.txt").exists(),
        "merged file should exist on master"
    );
}

#[tokio::test]
async fn squash_merge_succeeds_on_main_based_repo_with_base_branch_unset() {
    use std::fs;
    use tempfile::tempdir;

    let tmp = tempdir().unwrap();
    let repo = tmp.path();

    // Repo whose default branch is `main` — no `master` branch exists at all,
    // and no `.huskies/project.toml` sets `base_branch`. run_squash_merge must
    // auto-detect `main` instead of assuming `master` (bug 1176).
    Command::new("git")
        .args(["init", "-b", "main"])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["config", "user.email", "test@test.com"])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["config", "user.name", "Test"])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "--allow-empty", "-m", "init"])
        .current_dir(repo)
        .output()
        .unwrap();

    Command::new("git")
        .args(["checkout", "-b", "feature/story-main_test"])
        .current_dir(repo)
        .output()
        .unwrap();
    fs::write(repo.join("new_file.txt"), "new content").unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "add new file"])
        .current_dir(repo)
        .output()
        .unwrap();

    Command::new("git")
        .args(["checkout", "main"])
        .current_dir(repo)
        .output()
        .unwrap();

    let result = run_squash_merge(repo, "feature/story-main_test", "main_test").unwrap();

    assert!(
        matches!(
            result,
            super::MergeResult::Success {
                conflicts_resolved: false,
                ..
            }
        ),
        "clean merge should succeed on a main-based repo; got: {result:?}"
    );
    assert!(
        repo.join("new_file.txt").exists(),
        "merged file should exist on main"
    );
}

#[tokio::test]
async fn squash_merge_nonexistent_branch_fails() {
    use tempfile::tempdir;

    let tmp = tempdir().unwrap();
    let repo = tmp.path();
    init_git_repo(repo);

    let result = run_squash_merge(repo, "feature/story-nope", "nope").unwrap();

    assert!(
        !matches!(result, super::MergeResult::Success { .. }),
        "merge of nonexistent branch should fail; got: {result:?}"
    );
}

#[tokio::test]
async fn squash_merge_succeeds_when_master_diverges() {
    use std::fs;
    use tempfile::tempdir;

    let tmp = tempdir().unwrap();
    let repo = tmp.path();
    init_git_repo(repo);

    // Create an initial file on master.
    fs::write(repo.join("base.txt"), "base content\n").unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "initial"])
        .current_dir(repo)
        .output()
        .unwrap();

    // Create a feature branch with a new file (clean merge, no conflicts).
    Command::new("git")
        .args(["checkout", "-b", "feature/story-diverge_test"])
        .current_dir(repo)
        .output()
        .unwrap();
    fs::write(repo.join("feature.txt"), "feature content\n").unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "feature: add file"])
        .current_dir(repo)
        .output()
        .unwrap();

    // Switch back to master and simulate a filesystem watcher commit
    // (e.g. a pipeline file move) that advances master beyond the point
    // where the merge-queue branch will be created.
    Command::new("git")
        .args(["checkout", "master"])
        .current_dir(repo)
        .output()
        .unwrap();
    let sk_dir = repo.join(".huskies/work/4_merge");
    fs::create_dir_all(&sk_dir).unwrap();
    fs::write(sk_dir.join("diverge_test.md"), "---\nname: test\n---\n").unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "huskies: queue diverge_test for merge"])
        .current_dir(repo)
        .output()
        .unwrap();

    // Run the squash merge.  With the old fast-forward approach, this
    // would fail because master diverged.  With cherry-pick, it succeeds.
    let result = run_squash_merge(repo, "feature/story-diverge_test", "diverge_test").unwrap();

    assert!(
        matches!(result, super::MergeResult::Success { .. }),
        "squash merge should succeed despite diverged master: {:?}",
        result
    );

    // Verify the feature file landed on master.
    assert!(
        repo.join("feature.txt").exists(),
        "feature file should be on master after cherry-pick"
    );
    let feature_content = fs::read_to_string(repo.join("feature.txt")).unwrap();
    assert_eq!(feature_content, "feature content\n");

    // Verify the watcher commit's file is still present.
    assert!(
        sk_dir.join("diverge_test.md").exists(),
        "watcher-committed file should still be on master"
    );

    // Verify cleanup: no merge-queue branch, no merge workspace.
    let branches = Command::new("git")
        .args(["branch", "--list", "merge-queue/*"])
        .current_dir(repo)
        .output()
        .unwrap();
    let branch_list = String::from_utf8_lossy(&branches.stdout);
    assert!(
        branch_list.trim().is_empty(),
        "merge-queue branch should be cleaned up, got: {branch_list}"
    );
    assert!(
        !repo.join(".huskies/merge_workspace").exists(),
        "merge workspace should be cleaned up"
    );
}

#[tokio::test]
async fn squash_merge_empty_diff_fails() {
    use std::fs;
    use tempfile::tempdir;

    let tmp = tempdir().unwrap();
    let repo = tmp.path();
    init_git_repo(repo);

    // Create a file on master.
    fs::write(repo.join("code.txt"), "content\n").unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "add code"])
        .current_dir(repo)
        .output()
        .unwrap();

    // Create a feature branch with NO additional changes (empty diff).
    Command::new("git")
        .args(["checkout", "-b", "feature/story-empty_test"])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["checkout", "master"])
        .current_dir(repo)
        .output()
        .unwrap();

    let result = run_squash_merge(repo, "feature/story-empty_test", "empty_test");

    // Bug 226 / 675: a zero-commit branch must not be treated as success.
    // The pre-flight check (bug 675) returns Err for zero commits ahead;
    // the older code path returned Ok(SquashMergeResult { success: false }).
    // Either form is a failure — just not success.
    match result {
        Ok(r) => assert!(
            !matches!(r, super::MergeResult::Success { .. }),
            "empty diff merge must fail, not silently succeed: {:?}",
            r
        ),
        Err(e) => assert!(
            e.contains("no commits to merge") || e.contains("nothing to commit"),
            "unexpected error: {e}"
        ),
    }

    // Cleanup should still happen (no workspace was created for the Err path).
    assert!(
        !repo.join(".huskies/merge_workspace").exists(),
        "merge workspace should be cleaned up"
    );
}

/// Bug 777: a second `run_squash_merge` call after a successful first one must
/// return `success: true` (idempotent) so the caller advances the story to
/// `5_done` rather than overwriting that state with `merge_failure`.  The
/// pre-flight `ahead == 0` check still catches truly empty feature branches.
#[tokio::test]
async fn idempotent_retry_after_successful_merge_returns_success() {
    use std::fs;
    use tempfile::tempdir;

    let tmp = tempdir().unwrap();
    let repo = tmp.path();
    init_git_repo(repo);

    // Master has an initial file.
    fs::write(repo.join("base.txt"), "base\n").unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "base"])
        .current_dir(repo)
        .output()
        .unwrap();

    // Feature branch adds a new file.
    Command::new("git")
        .args(["checkout", "-b", "feature/story-777_idem"])
        .current_dir(repo)
        .output()
        .unwrap();
    fs::write(repo.join("feat.txt"), "feature\n").unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "add feat"])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["checkout", "master"])
        .current_dir(repo)
        .output()
        .unwrap();

    // First merge: should succeed and land feature's content on master.
    let r1 = run_squash_merge(repo, "feature/story-777_idem", "777_idem")
        .expect("first merge produces Ok");
    // The merge may fail gates in test env (no script/test); only require that
    // the squash applied SOMETHING (cargo gates env-dependent).
    if matches!(r1, super::MergeResult::Success { .. }) {
        // Second merge of the SAME branch: must report success (idempotent),
        // not merge_failure.  Feature branch's content is already on master so
        // the squash produces "nothing to commit" — bug 777 makes this success.
        let r2 = run_squash_merge(repo, "feature/story-777_idem", "777_idem")
            .expect("second merge produces Ok");
        assert!(
            matches!(
                r2,
                super::MergeResult::Success {
                    conflicts_resolved: false,
                    ..
                }
            ),
            "idempotent retry must return Success without conflicts: {r2:?}"
        );
    }
}
