using ClaudeDo.Data.Git;
using ClaudeDo.Worker.Tests.Infrastructure;

namespace ClaudeDo.Worker.Tests.Runner;

public class GitServicePreviewMergeTests : IDisposable
{
    private readonly List<GitRepoFixture> _repos = new();
    private GitRepoFixture NewRepo() { var r = new GitRepoFixture(); _repos.Add(r); return r; }
    public void Dispose() { foreach (var r in _repos) try { r.Dispose(); } catch { } }

    [Fact]
    public async Task PreviewMergeAsync_NonConflicting_ReportsCleanWithChangedCount()
    {
        if (!GitRepoFixture.IsGitAvailable()) return;
        var repo = NewRepo();
        var git = new GitService();
        var baseBranch = await git.GetCurrentBranchAsync(repo.RepoDir);

        GitRepoFixture.RunGit(repo.RepoDir, "checkout", "-b", "feature");
        File.WriteAllText(Path.Combine(repo.RepoDir, "newfile.txt"), "x\n");
        GitRepoFixture.RunGit(repo.RepoDir, "add", "-A");
        GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "feat");
        GitRepoFixture.RunGit(repo.RepoDir, "checkout", baseBranch);

        var preview = await git.PreviewMergeAsync(repo.RepoDir, baseBranch, "feature", CancellationToken.None);

        Assert.True(preview.Supported);
        Assert.True(preview.Clean);
        Assert.Empty(preview.ConflictFiles);

        var count = await git.CountChangedFilesAsync(repo.RepoDir, baseBranch, "feature", CancellationToken.None);
        Assert.Equal(1, count);
    }

    [Fact]
    public async Task PreviewMergeAsync_Conflicting_ReportsFilesAndDoesNotMutateTree()
    {
        if (!GitRepoFixture.IsGitAvailable()) return;
        var repo = NewRepo();
        var git = new GitService();
        var baseBranch = await git.GetCurrentBranchAsync(repo.RepoDir);

        GitRepoFixture.RunGit(repo.RepoDir, "checkout", "-b", "feature");
        File.WriteAllText(Path.Combine(repo.RepoDir, "README.md"), "# from feature\n");
        GitRepoFixture.RunGit(repo.RepoDir, "add", "-A");
        GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "feat readme");
        GitRepoFixture.RunGit(repo.RepoDir, "checkout", baseBranch);
        File.WriteAllText(Path.Combine(repo.RepoDir, "README.md"), "# from base\n");
        GitRepoFixture.RunGit(repo.RepoDir, "add", "-A");
        GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "base readme");

        var headBefore = GitRepoFixture.RunGit(repo.RepoDir, "rev-parse", "HEAD").Trim();

        var preview = await git.PreviewMergeAsync(repo.RepoDir, baseBranch, "feature", CancellationToken.None);

        Assert.True(preview.Supported);
        Assert.False(preview.Clean);
        Assert.Contains("README.md", preview.ConflictFiles);

        Assert.Equal(headBefore, GitRepoFixture.RunGit(repo.RepoDir, "rev-parse", "HEAD").Trim());
        Assert.False(await git.IsMidMergeAsync(repo.RepoDir));
    }

    [Fact]
    public async Task PreviewMergeAsync_Clean_ReturnsTreeOidMaterializableIntoDetachedWorktree()
    {
        if (!GitRepoFixture.IsGitAvailable()) return;
        var repo = NewRepo();
        var git = new GitService();
        var baseBranch = await git.GetCurrentBranchAsync(repo.RepoDir);

        GitRepoFixture.RunGit(repo.RepoDir, "checkout", "-b", "feature");
        File.WriteAllText(Path.Combine(repo.RepoDir, "newfile.txt"), "x\n");
        GitRepoFixture.RunGit(repo.RepoDir, "add", "-A");
        GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "feat");
        GitRepoFixture.RunGit(repo.RepoDir, "checkout", baseBranch);

        var preview = await git.PreviewMergeAsync(repo.RepoDir, baseBranch, "feature", CancellationToken.None);
        Assert.True(preview.Clean);
        Assert.False(string.IsNullOrWhiteSpace(preview.TreeOid));

        // The tree a clean preview would produce can be wrapped in a commit and checked out into
        // a detached, branchless scratch worktree -- the technique the verify-in-preview feature
        // relies on -- without ever touching the real working tree.
        var parentSha = await git.RevParseAsync(repo.RepoDir, baseBranch, CancellationToken.None);
        var commitSha = await git.CommitTreeAsync(repo.RepoDir, preview.TreeOid!, parentSha, "scratch", CancellationToken.None);

        var scratchPath = Path.Combine(Path.GetTempPath(), $"scratch_{Guid.NewGuid():N}");
        await git.WorktreeAddDetachedAsync(repo.RepoDir, scratchPath, commitSha, CancellationToken.None);
        try
        {
            Assert.True(File.Exists(Path.Combine(scratchPath, "newfile.txt")));
        }
        finally
        {
            await git.WorktreeRemoveAsync(repo.RepoDir, scratchPath, force: true, CancellationToken.None);
        }

        Assert.False(Directory.Exists(scratchPath));
        Assert.False(await git.HasChangesAsync(repo.RepoDir, CancellationToken.None));
    }
}
