//! Merge tests: `pull` outcome classification, in-place `merge`,
//! and the isolated worktree-based merge orchestrator.

use std::process::Command;

use crate::tests::{clone_for_test, git_available, unique_temp_dir, TempRepo};
use crate::{ConflictKind, GitError, GitRepo, MergeOutcome};

#[test]
fn pull_classifies_up_to_date_then_fast_forward() {
    if !git_available() {
        return;
    }
    // A bare repo standing in for the "remote".
    let remote = unique_temp_dir("pull-remote");
    Command::new("git")
        .args(["init", "--bare", "--initial-branch=main"])
        .arg(&remote)
        .output()
        .expect("init bare remote");

    // Clone A seeds the remote with an initial commit.
    let (a_dir, a) = clone_for_test(&remote, "pull-a");
    std::fs::write(a_dir.join("a.op"), "1").unwrap();
    a.stage_all().unwrap();
    a.commit("init").unwrap();
    a.run(&["push", "-u", "origin", "main"]).unwrap();

    // Clone B has nothing to pull → AlreadyUpToDate.
    let (b_dir, b) = clone_for_test(&remote, "pull-b");
    assert_eq!(b.pull().expect("pull"), MergeOutcome::AlreadyUpToDate);

    // A pushes a new commit; B's pull fast-forwards.
    std::fs::write(a_dir.join("a.op"), "2").unwrap();
    a.stage_all().unwrap();
    a.commit("update").unwrap();
    a.push().unwrap();
    assert_eq!(b.pull().expect("pull"), MergeOutcome::FastForward);

    for dir in [remote, a_dir, b_dir] {
        let _ = std::fs::remove_dir_all(dir);
    }
}

#[test]
fn pull_classifies_a_divergent_merge() {
    if !git_available() {
        return;
    }
    let remote = unique_temp_dir("merge-remote");
    Command::new("git")
        .args(["init", "--bare", "--initial-branch=main"])
        .arg(&remote)
        .output()
        .expect("init bare remote");

    // A seeds the remote with a base commit.
    let (a_dir, a) = clone_for_test(&remote, "merge-a");
    std::fs::write(a_dir.join("base.op"), "0").unwrap();
    a.stage_all().unwrap();
    a.commit("base").unwrap();
    a.run(&["push", "-u", "origin", "main"]).unwrap();

    // B clones at the base commit.
    let (b_dir, b) = clone_for_test(&remote, "merge-b");

    // A advances the remote with a change to one file.
    std::fs::write(a_dir.join("a-side.op"), "a").unwrap();
    a.stage_all().unwrap();
    a.commit("a side").unwrap();
    a.push().unwrap();

    // B commits a *different* file locally without pulling first —
    // now the local branch and the remote have diverged.
    std::fs::write(b_dir.join("b-side.op"), "b").unwrap();
    b.stage_all().unwrap();
    b.commit("b side").unwrap();

    // The pull cannot fast-forward; it must create a (clean,
    // non-conflicting) merge commit.
    assert_eq!(b.pull().expect("pull"), MergeOutcome::Merge);

    for dir in [remote, a_dir, b_dir] {
        let _ = std::fs::remove_dir_all(dir);
    }
}

#[test]
fn pull_with_local_commits_and_unchanged_upstream_is_up_to_date() {
    if !git_available() {
        return;
    }
    let remote = unique_temp_dir("ahead-remote");
    Command::new("git")
        .args(["init", "--bare", "--initial-branch=main"])
        .arg(&remote)
        .output()
        .expect("init bare remote");

    let (dir, repo) = clone_for_test(&remote, "ahead-clone");
    std::fs::write(dir.join("base.op"), "0").unwrap();
    repo.stage_all().unwrap();
    repo.commit("base").unwrap();
    repo.run(&["push", "-u", "origin", "main"]).unwrap();

    // Commit locally WITHOUT pushing — the local branch is now ahead
    // of `origin/main`, which has not moved.
    std::fs::write(dir.join("local.op"), "1").unwrap();
    repo.stage_all().unwrap();
    repo.commit("local only").unwrap();

    // A pull has nothing to integrate (upstream is already an
    // ancestor of HEAD). It must report AlreadyUpToDate — NOT Merge.
    assert_eq!(repo.pull().expect("pull"), MergeOutcome::AlreadyUpToDate);

    for d in [remote, dir] {
        let _ = std::fs::remove_dir_all(d);
    }
}

#[test]
fn merge_fast_forwards_then_reports_up_to_date() {
    let Some(tr) = TempRepo::new("merge-ff") else {
        return;
    };
    tr.write("a.op", "1");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("base").unwrap();
    tr.repo.create_and_switch_branch("feature").unwrap();
    tr.write("a.op", "2");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("feature work").unwrap();
    tr.repo.switch_branch("main").unwrap();

    // `main` has not moved since `base` → merging `feature` is a
    // fast-forward; merging it again is a no-op.
    assert_eq!(
        tr.repo.merge("feature").expect("merge"),
        MergeOutcome::FastForward
    );
    assert_eq!(
        tr.repo.merge("feature").expect("merge"),
        MergeOutcome::AlreadyUpToDate
    );
}

#[test]
fn merge_creates_a_commit_on_divergence() {
    let Some(tr) = TempRepo::new("merge-div") else {
        return;
    };
    tr.write("base.op", "0");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("base").unwrap();
    tr.repo.create_and_switch_branch("feature").unwrap();
    tr.write("feature.op", "f");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("feature").unwrap();
    tr.repo.switch_branch("main").unwrap();
    tr.write("main.op", "m");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("main").unwrap();

    // Diverged, but the two branches touch different files → a
    // clean merge commit, no conflict, no merge left in progress.
    assert_eq!(
        tr.repo.merge("feature").expect("merge"),
        MergeOutcome::Merge
    );
    assert!(!tr.repo.is_merging());
}

#[test]
fn conflicting_merge_reports_conflict_then_aborts() {
    let Some(tr) = TempRepo::new("merge-conflict") else {
        return;
    };
    tr.write("doc.op", "base");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("base").unwrap();
    tr.repo.create_and_switch_branch("feature").unwrap();
    tr.write("doc.op", "feature version");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("feature").unwrap();
    tr.repo.switch_branch("main").unwrap();
    tr.write("doc.op", "main version");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("main").unwrap();

    // Both branches changed `doc.op` → the merge conflicts.
    assert_eq!(
        tr.repo.merge("feature").expect("merge"),
        MergeOutcome::Conflict
    );
    assert!(tr.repo.is_merging());
    assert_eq!(
        tr.repo.conflicted_files().expect("conflicts"),
        vec!["doc.op".to_string()]
    );

    // A second merge — of *any* ref — must be refused while the
    // first merge is unresolved, rather than misreporting the
    // leftover conflict (or short-circuiting to a false success).
    let err = tr.repo.merge("feature").expect_err("must refuse");
    assert!(matches!(err, GitError::MergeInProgress));
    // `HEAD` itself would hit the up-to-date short-circuit — that
    // path must be refused too.
    let err_self = tr.repo.merge("HEAD").expect_err("must refuse");
    assert!(matches!(err_self, GitError::MergeInProgress));
    // `pull` must hit the same gate *before* any network fetch — in
    // this no-upstream repo, fetching first would instead surface a
    // confusing `@{u}` error, so `MergeInProgress` proves the gate
    // runs up front.
    let err_pull = tr.repo.pull().expect_err("pull must refuse");
    assert!(matches!(err_pull, GitError::MergeInProgress));

    // Aborting restores the clean pre-merge state.
    tr.repo.abort_merge().expect("abort");
    assert!(!tr.repo.is_merging());
    assert!(tr.repo.status().expect("status").is_clean());
}

/// Number of registered worktrees (the main tree counts as one).
fn worktree_count(repo: &GitRepo) -> usize {
    repo.run(&["worktree", "list"])
        .expect("worktree list")
        .lines()
        .filter(|l| !l.trim().is_empty())
        .count()
}

#[test]
fn worktree_merge_fast_forwards_into_a_clean_live_tree() {
    let Some(tr) = TempRepo::new("wt-merge-ff") else {
        return;
    };
    tr.write("base.op", "0");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("base").unwrap();
    tr.repo.create_and_switch_branch("feature").unwrap();
    tr.write("feature.op", "f");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("feature").unwrap();
    tr.repo.switch_branch("main").unwrap();

    // `main` is a strict ancestor of `feature` → an exact ff.
    let report = tr
        .repo
        .merge_branch_isolated("feature", |_, _, _, _| None)
        .expect("merge");
    assert_eq!(report.outcome, MergeOutcome::FastForward);
    assert!(report.merged_commit.is_some());
    assert!(report.conflicts.is_empty());
    // The live tree advanced — `feature.op` is now present.
    assert!(tr.dir.join("feature.op").exists());
    assert!(!tr.repo.is_merging());
    // The throwaway worktree was cleaned up.
    assert_eq!(worktree_count(&tr.repo), 1);
}

#[test]
fn worktree_merge_creates_a_commit_on_divergence() {
    let Some(tr) = TempRepo::new("wt-merge-div") else {
        return;
    };
    tr.write("base.op", "0");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("base").unwrap();
    tr.repo.create_and_switch_branch("feature").unwrap();
    tr.write("feature.op", "f");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("feature").unwrap();
    tr.repo.switch_branch("main").unwrap();
    tr.write("main.op", "m");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("main").unwrap();

    // Diverged but touching different files → a clean merge commit.
    let report = tr
        .repo
        .merge_branch_isolated("feature", |_, _, _, _| None)
        .expect("merge");
    assert_eq!(report.outcome, MergeOutcome::Merge);
    assert!(report.merged_commit.is_some());
    assert!(report.conflicts.is_empty());
    // Both files are present in the live tree and it is not merging.
    assert!(tr.dir.join("feature.op").exists());
    assert!(tr.dir.join("main.op").exists());
    assert!(!tr.repo.is_merging());
    assert_eq!(worktree_count(&tr.repo), 1);
}

#[test]
fn worktree_merge_quarantines_conflicts_and_keeps_the_live_tree_pristine() {
    let Some(tr) = TempRepo::new("wt-merge-conflict") else {
        return;
    };
    tr.write("doc.op", "base");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("base").unwrap();
    tr.repo.create_and_switch_branch("feature").unwrap();
    tr.write("doc.op", "feature version");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("feature").unwrap();
    tr.repo.switch_branch("main").unwrap();
    tr.write("doc.op", "main version");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("main").unwrap();

    // Both branches changed `doc.op` → the merge conflicts, but the
    // conflict is quarantined to the throwaway worktree.
    let report = tr
        .repo
        .merge_branch_isolated("feature", |_, _, _, _| None)
        .expect("merge");
    assert_eq!(report.outcome, MergeOutcome::Conflict);
    assert!(report.merged_commit.is_none());
    assert_eq!(report.conflicts.len(), 1);
    assert_eq!(report.conflicts.files[0].path, "doc.op");
    assert_eq!(report.conflicts.files[0].kind, ConflictKind::BothModified);

    // The live tree is pristine — no `MERGE_HEAD`, no markers, and
    // `doc.op` still holds exactly the committed `main` content.
    assert!(!tr.repo.is_merging());
    assert!(tr.repo.status().expect("status").is_clean());
    assert_eq!(
        std::fs::read_to_string(tr.dir.join("doc.op")).unwrap(),
        "main version"
    );
    assert_eq!(worktree_count(&tr.repo), 1);
}

#[test]
fn worktree_merge_reports_up_to_date_for_an_ancestor() {
    let Some(tr) = TempRepo::new("wt-merge-utd") else {
        return;
    };
    tr.write("base.op", "0");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("base").unwrap();
    tr.repo.create_branch("feature").unwrap();
    tr.write("main.op", "m");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("ahead").unwrap();

    // `feature` points at an ancestor of `main` → nothing to do, and
    // no worktree is created.
    let report = tr
        .repo
        .merge_branch_isolated("feature", |_, _, _, _| None)
        .expect("merge");
    assert_eq!(report.outcome, MergeOutcome::AlreadyUpToDate);
    assert!(report.merged_commit.is_none());
    assert_eq!(worktree_count(&tr.repo), 1);
}

#[test]
fn worktree_merge_refuses_a_dirty_live_tree() {
    let Some(tr) = TempRepo::new("wt-merge-dirty") else {
        return;
    };
    tr.write("base.op", "0");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("base").unwrap();
    tr.repo.create_and_switch_branch("feature").unwrap();
    tr.write("feature.op", "f");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("feature").unwrap();
    tr.repo.switch_branch("main").unwrap();
    // An uncommitted change in the live tree.
    tr.write("scratch.op", "wip");

    let err = tr
        .repo
        .merge_branch_isolated("feature", |_, _, _, _| None)
        .expect_err("must refuse a dirty tree");
    assert!(matches!(err, GitError::WorkingTreeDirty));
    // Refused before any worktree was created.
    assert_eq!(worktree_count(&tr.repo), 1);
}

#[test]
fn worktree_merge_resolver_auto_completes_a_conflict() {
    let Some(tr) = TempRepo::new("wt-merge-resolve") else {
        return;
    };
    tr.write("doc.op", "base");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("base").unwrap();
    tr.repo.create_and_switch_branch("feature").unwrap();
    tr.write("doc.op", "feature version");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("feature").unwrap();
    tr.repo.switch_branch("main").unwrap();
    tr.write("doc.op", "main version");
    tr.repo.stage_all().unwrap();
    tr.repo.commit("main").unwrap();

    // The resolver supplies merged content for the conflicted `.op`
    // file — the otherwise-conflicting merge then completes cleanly.
    let report = tr
        .repo
        .merge_branch_isolated("feature", |path, _base, _ours, _theirs| {
            (path == "doc.op").then(|| "resolved".to_string())
        })
        .expect("merge");
    assert_eq!(report.outcome, MergeOutcome::Merge);
    assert!(report.conflicts.is_empty());
    assert!(!tr.repo.is_merging());
    assert_eq!(
        std::fs::read_to_string(tr.dir.join("doc.op")).unwrap(),
        "resolved",
        "the resolver's content landed in the live tree"
    );
    assert_eq!(worktree_count(&tr.repo), 1);
}
