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

namespace ClaudeDo.Worker.Tests.Runner;

// Covers the empty-repo (unborn HEAD) hardening: HasHeadCommitAsync, EnsureHeadCommitAsync
// (plumbing-only bootstrap commit), the friendly RevParseHeadAsync unborn message, and the
// CommitAsync git-identity fallback. All tests run real git against throwaway repos.
public class GitServiceBootstrapTests : IDisposable
{
    private readonly List<string> _tempDirs = new();
    private readonly GitService _git = new();

    private static bool GitAvailable => GitRepoFixture.IsGitAvailable();

    // A repo straight out of `git init` — no commits, and deliberately NO identity
    // configured: the bootstrap must not depend on machine-level git config.
    private string InitEmptyRepo()
    {
        var dir = Path.Combine(Path.GetTempPath(), $"claudedo_emptyrepo_{Guid.NewGuid():N}");
        Directory.CreateDirectory(dir);
        GitRepoFixture.RunGit(dir, "init", "-b", "main");
        _tempDirs.Add(dir);
        return dir;
    }

    [Fact]
    public async Task HasHeadCommit_FalseOnEmptyRepo_TrueAfterCommit()
    {
        if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }

        var dir = InitEmptyRepo();
        Assert.False(await _git.HasHeadCommitAsync(dir));

        GitRepoFixture.RunGit(dir, "-c", "user.name=t", "-c", "user.email=t@t", "commit", "--allow-empty", "-m", "x");
        Assert.True(await _git.HasHeadCommitAsync(dir));
    }

    [Fact]
    public async Task EnsureHeadCommit_BootstrapsEmptyRepo_Once()
    {
        if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }

        var dir = InitEmptyRepo();

        Assert.True(await _git.EnsureHeadCommitAsync(dir));

        // HEAD now resolves and RevParseHeadAsync works — the original failure mode is gone.
        var head = await _git.RevParseHeadAsync(dir);
        Assert.Matches("^[0-9a-f]{40,64}$", head);

        // The bootstrap commit is empty: nothing of the user's ends up in it.
        var tree = GitRepoFixture.RunGit(dir, "ls-tree", "HEAD").Trim();
        Assert.Equal(string.Empty, tree);

        // Idempotent: a repo with a HEAD commit is left alone.
        Assert.False(await _git.EnsureHeadCommitAsync(dir));
        Assert.Equal(head, await _git.RevParseHeadAsync(dir));
    }

    [Fact]
    public async Task EnsureHeadCommit_LeavesStagedFilesStaged()
    {
        if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }

        var dir = InitEmptyRepo();
        File.WriteAllText(Path.Combine(dir, "mine.txt"), "user work\n");
        GitRepoFixture.RunGit(dir, "add", "mine.txt");

        Assert.True(await _git.EnsureHeadCommitAsync(dir));

        // Still staged (index vs HEAD shows it as pending), NOT committed.
        var staged = GitRepoFixture.RunGit(dir, "diff", "--cached", "--name-only").Trim();
        Assert.Equal("mine.txt", staged);
        var tree = GitRepoFixture.RunGit(dir, "ls-tree", "HEAD").Trim();
        Assert.Equal(string.Empty, tree);
    }

    [Fact]
    public async Task EnsureHeadCommit_CorruptHead_ThrowsClearError()
    {
        if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }

        var dir = InitEmptyRepo();
        // Detach HEAD onto a commit that doesn't exist — not an unborn branch, a broken repo.
        File.WriteAllText(Path.Combine(dir, ".git", "HEAD"), new string('f', 40) + "\n");

        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => _git.EnsureHeadCommitAsync(dir));
        Assert.Contains(dir, ex.Message);
    }

    [Fact]
    public async Task RevParseHead_EmptyRepo_ReportsNoCommitsInsteadOfRawGitError()
    {
        if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }

        var dir = InitEmptyRepo();
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => _git.RevParseHeadAsync(dir));
        Assert.Contains("no commits yet", ex.Message);
        Assert.Contains(dir, ex.Message);
        Assert.DoesNotContain("ambiguous argument", ex.Message);
    }

    [Fact]
    public async Task CommitAsync_MissingIdentity_FallsBackToClaudeDoIdentity()
    {
        if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }

        var dir = InitEmptyRepo();
        GitRepoFixture.RunGit(dir, "-c", "user.name=t", "-c", "user.email=t@t", "commit", "--allow-empty", "-m", "seed");
        // Empty local identity overrides any machine-level config, so the first commit
        // attempt fails with git's identity error no matter where this test runs.
        GitRepoFixture.RunGit(dir, "config", "user.name", "");
        GitRepoFixture.RunGit(dir, "config", "user.email", "");

        File.WriteAllText(Path.Combine(dir, "work.txt"), "content\n");
        await _git.AddAllAsync(dir);

        await _git.CommitAsync(dir, "feat(test): identity fallback");

        var author = GitRepoFixture.RunGit(dir, "log", "-1", "--format=%an <%ae>").Trim();
        Assert.Equal("ClaudeDo <claudedo@local>", author);
    }

    public void Dispose()
    {
        foreach (var dir in _tempDirs)
        {
            try
            {
                if (!Directory.Exists(dir)) continue;
                foreach (var file in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories))
                    File.SetAttributes(file, FileAttributes.Normal);
                Directory.Delete(dir, true);
            }
            catch { /* best effort */ }
        }
    }
}
