//! Tests for squash-merge orchestration — advanced and regression 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_md_only_changes_fails() {
    use std::fs;
    use tempfile::tempdir;

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

    // Create a feature branch that only moves a .huskies/ file.
    Command::new("git")
        .args(["checkout", "-b", "feature/story-md_only_test"])
        .current_dir(repo)
        .output()
        .unwrap();
    let sk_dir = repo.join(".huskies/work/2_current");
    fs::create_dir_all(&sk_dir).unwrap();
    fs::write(sk_dir.join("md_only_test.md"), "---\nname: Test\n---\n").unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "move story file"])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["checkout", "master"])
        .current_dir(repo)
        .output()
        .unwrap();

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

    // The squash merge will commit the .huskies/ file, but should fail because
    // there are no code changes outside .huskies/.
    assert!(
        !matches!(result, super::MergeResult::Success { .. }),
        "merge with only .huskies/ changes must fail: {:?}",
        result
    );

    // Cleanup should still happen.
    assert!(
        !repo.join(".huskies/merge_workspace").exists(),
        "merge workspace should be cleaned up"
    );
}

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

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

    // Initial file with a shared base.
    fs::write(repo.join("module.rs"), "// module\npub fn existing() {}\n").unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "initial module"])
        .current_dir(repo)
        .output()
        .unwrap();

    // Feature branch: appends feature_fn to the file.
    Command::new("git")
        .args(["checkout", "-b", "feature/story-238_additive"])
        .current_dir(repo)
        .output()
        .unwrap();
    fs::write(
        repo.join("module.rs"),
        "// module\npub fn existing() {}\npub fn feature_fn() {}\n",
    )
    .unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "add feature_fn"])
        .current_dir(repo)
        .output()
        .unwrap();

    // Simulate another branch already merged into master: appends master_fn.
    Command::new("git")
        .args(["checkout", "master"])
        .current_dir(repo)
        .output()
        .unwrap();
    fs::write(
        repo.join("module.rs"),
        "// module\npub fn existing() {}\npub fn master_fn() {}\n",
    )
    .unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "add master_fn (another branch merged)"])
        .current_dir(repo)
        .output()
        .unwrap();

    // Squash-merge the feature branch — conflicts because both appended to the same location.
    let result = run_squash_merge(repo, "feature/story-238_additive", "238_additive").unwrap();

    // Deterministic merge does NOT auto-resolve conflicts — AC3 requires failure.
    assert!(
        matches!(result, super::MergeResult::Conflict { .. }),
        "additive conflict should produce Conflict variant; got: {result:?}"
    );

    // Master must not have been modified (merge aborted).
    let content = fs::read_to_string(repo.join("module.rs")).unwrap();
    assert!(
        !content.contains("<<<<<<<"),
        "master must not contain conflict markers"
    );
    assert!(
        !content.contains(">>>>>>>"),
        "master must not contain conflict markers"
    );

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

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

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

    // Add a script/test that always fails (quality gate).  This must be on
    // master before the feature branch forks so it doesn't cause its own conflict.
    let script_dir = repo.join("script");
    fs::create_dir_all(&script_dir).unwrap();
    fs::write(script_dir.join("test"), "#!/bin/sh\nexit 1\n").unwrap();
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(
            script_dir.join("test"),
            std::fs::Permissions::from_mode(0o755),
        )
        .unwrap();
    }
    fs::write(repo.join("code.txt"), "// base\n").unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "initial with failing script/test"])
        .current_dir(repo)
        .output()
        .unwrap();

    // Feature branch: appends feature content (creates future conflict point).
    Command::new("git")
        .args(["checkout", "-b", "feature/story-238_gates_fail"])
        .current_dir(repo)
        .output()
        .unwrap();
    fs::write(repo.join("code.txt"), "// base\nfeature_addition\n").unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "feature addition"])
        .current_dir(repo)
        .output()
        .unwrap();

    // Master: append different content at same location (creates conflict).
    Command::new("git")
        .args(["checkout", "master"])
        .current_dir(repo)
        .output()
        .unwrap();
    fs::write(repo.join("code.txt"), "// base\nmaster_addition\n").unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "master addition"])
        .current_dir(repo)
        .output()
        .unwrap();

    // Squash-merge: conflict detected → aborted immediately (no gate run).
    let result = run_squash_merge(repo, "feature/story-238_gates_fail", "238_gates_fail").unwrap();

    // Merge is aborted at conflict detection; gates are never reached.
    assert!(
        matches!(result, super::MergeResult::Conflict { .. }),
        "conflicting merge must produce Conflict variant; got: {result:?}"
    );
    assert!(
        !result.output().is_empty(),
        "output must contain conflict details"
    );

    // Master must NOT have been updated (cherry-pick was blocked by gate failure).
    let content = fs::read_to_string(repo.join("code.txt")).unwrap();
    assert!(
        !content.contains("<<<<<<<"),
        "master must not contain conflict markers"
    );
    // master_addition was the last commit on master; feature_addition must NOT be there.
    assert!(
        !content.contains("feature_addition"),
        "feature code must not land on master when gates fail"
    );

    // Cleanup must still happen.
    assert!(
        !repo.join(".huskies/merge_workspace").exists(),
        "merge workspace must be cleaned up even on gate failure"
    );
}

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

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

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

    // Simulate a stale merge workspace left from a previous failed merge.
    let stale_ws = repo.join(".huskies/merge_workspace");
    fs::create_dir_all(&stale_ws).unwrap();
    fs::write(stale_ws.join("leftover.txt"), "stale").unwrap();

    // Run the merge — it should clean up the stale workspace first.
    let result = run_squash_merge(repo, "feature/story-stale_test", "stale_test").unwrap();

    assert!(
        matches!(result, super::MergeResult::Success { .. }),
        "merge should succeed after cleaning up stale workspace: {:?}",
        result
    );
    assert!(
        !stale_ws.exists(),
        "stale merge workspace should be cleaned up"
    );
}

#[cfg(unix)]
#[test]
fn squash_merge_runs_component_setup_from_project_toml() {
    use std::fs;
    use tempfile::tempdir;

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

    // Add a .huskies/project.toml with a component whose setup writes a
    // sentinel file so we can confirm the command ran.
    let sk_dir = repo.join(".huskies");
    fs::create_dir_all(&sk_dir).unwrap();
    fs::write(
        sk_dir.join("project.toml"),
        "[[component]]\nname = \"sentinel\"\npath = \".\"\nsetup = [\"touch setup_ran.txt\"]\n",
    )
    .unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "add project.toml with component setup"])
        .current_dir(repo)
        .output()
        .unwrap();

    // Create feature branch with a change.
    Command::new("git")
        .args(["checkout", "-b", "feature/story-216_setup_test"])
        .current_dir(repo)
        .output()
        .unwrap();
    fs::write(repo.join("feature.txt"), "change").unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "feature work"])
        .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-216_setup_test", "216_setup_test").unwrap();

    // The output must mention component setup, proving the new code path ran.
    assert!(
        result.output().contains("component setup"),
        "merge output must mention component setup when project.toml has components, got:\n{}",
        result.output()
    );
    // The sentinel command must appear in the output.
    assert!(
        result.output().contains("sentinel"),
        "merge output must name the component, got:\n{}",
        result.output()
    );
}

/// AC6: the regen+commit step runs on `project_root` (master) only.
/// After a successful merge where the source-map changes, `git log --name-only`
/// shows a follow-up commit whose diff contains ONLY `.huskies/source-map.json`.
#[tokio::test]
async fn regen_commit_on_master_touches_only_source_map() {
    use std::fs;
    use tempfile::tempdir;

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

    // Put a stale source-map.json on master so regen will produce a different result.
    let sk_dir = repo.join(".huskies");
    fs::create_dir_all(&sk_dir).unwrap();
    fs::write(sk_dir.join("source-map.json"), "{\"stale\": true}\n").unwrap();

    // Add a tracked Rust file so the regenerator has something to index.
    fs::create_dir_all(repo.join("src")).unwrap();
    fs::write(
        repo.join("src/lib.rs"),
        "//! Library.\n\n/// Says hello.\npub fn hello() {}\n",
    )
    .unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "initial with stale source-map"])
        .current_dir(repo)
        .output()
        .unwrap();

    // Feature branch: add a new file.
    Command::new("git")
        .args(["checkout", "-b", "feature/story-1065_regen_test"])
        .current_dir(repo)
        .output()
        .unwrap();
    fs::write(
        repo.join("src/extra.rs"),
        "//! Extra.\n\n/// Extra fn.\npub fn extra() {}\n",
    )
    .unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "add extra.rs"])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["checkout", "master"])
        .current_dir(repo)
        .output()
        .unwrap();

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

    assert!(
        matches!(result, super::MergeResult::Success { .. }),
        "clean merge must succeed; got: {result:?}"
    );

    // Find the regen commit if one was created.
    let log_out = Command::new("git")
        .args(["log", "--oneline", "--name-only"])
        .current_dir(repo)
        .output()
        .unwrap();
    let log = String::from_utf8_lossy(&log_out.stdout);

    // If a regen commit exists, its diff must contain ONLY the source-map path.
    if log.contains("huskies: regen source-map.json") {
        // Extract files changed in the regen commit.
        let show_out = Command::new("git")
            .args(["show", "--name-only", "--format=", "HEAD"])
            .current_dir(repo)
            .output()
            .unwrap();
        let show = String::from_utf8_lossy(&show_out.stdout);

        // If HEAD is the regen commit, its files list must be exactly one entry.
        let head_msg = Command::new("git")
            .args(["log", "-1", "--format=%s"])
            .current_dir(repo)
            .output()
            .unwrap();
        let head_subject = String::from_utf8_lossy(&head_msg.stdout);
        if head_subject.trim() == "huskies: regen source-map.json" {
            let changed_files: Vec<&str> = show.lines().filter(|l| !l.is_empty()).collect();
            assert_eq!(
                changed_files,
                vec![".huskies/source-map.json"],
                "regen commit must touch ONLY .huskies/source-map.json; got: {changed_files:?}"
            );
        }
    }
}

#[cfg(unix)]
#[test]
fn squash_merge_succeeds_without_components_in_project_toml() {
    use std::fs;
    use tempfile::tempdir;

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

    // No .huskies/project.toml — no component setup.
    fs::write(repo.join("file.txt"), "initial").unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(repo)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "initial commit"])
        .current_dir(repo)
        .output()
        .unwrap();

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

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

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

    // No pnpm or frontend references should appear in the output.
    assert!(
        !result.output().contains("pnpm"),
        "output must not mention pnpm, got:\n{}",
        result.output()
    );
    assert!(
        !result.output().contains("frontend/"),
        "output must not mention frontend/, got:\n{}",
        result.output()
    );
}
