using ClaudeDo.Data;
using ClaudeDo.Data.Git;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Config;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Runner;
using ClaudeDo.Worker.Tests.Infrastructure;
using Microsoft.Extensions.Logging.Abstractions;

namespace ClaudeDo.Worker.Tests.Runner;

public class WorktreeManagerTests : IDisposable
{
    private readonly List<GitRepoFixture> _fixtures = new();
    private readonly List<DbFixture> _dbFixtures = new();
    private readonly List<string> _tempDirs = new();
    private readonly List<(string repoDir, string wtPath)> _worktreeCleanups = new();

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

    private GitRepoFixture CreateRepo()
    {
        var f = new GitRepoFixture();
        _fixtures.Add(f);
        return f;
    }

    private async Task<(WorktreeManager mgr, DbFixture db)> CreateManagerAsync(
        TaskEntity task, ListEntity list, string strategy = "sibling", string? centralRoot = null)
    {
        var db = new DbFixture();
        _dbFixtures.Add(db);

        // Seed the DB with list and task so FK constraints pass.
        using var seedCtx = db.CreateContext();
        var listRepo = new ListRepository(seedCtx);
        var taskRepo = new TaskRepository(seedCtx);
        await listRepo.AddAsync(list);
        await taskRepo.AddAsync(task);

        var cfg = new WorkerConfig
        {
            WorktreeRootStrategy = strategy,
        };
        if (centralRoot is not null)
            cfg.CentralWorktreeRoot = centralRoot;

        var mgr = new WorktreeManager(
            new GitService(), db.CreateFactory(), cfg, NullLogger<WorktreeManager>.Instance);
        return (mgr, db);
    }

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

        var repo = CreateRepo();
        var (task, list) = MakeEntities(repo.RepoDir);
        var (mgr, db) = await CreateManagerAsync(task, list);

        var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
        _worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));

        Assert.NotNull(ctx);
        Assert.True(Directory.Exists(ctx.WorktreePath));
        Assert.Equal($"claudedo/{task.Id.Replace("-", "")}", ctx.BranchName);
        Assert.Equal(repo.BaseCommit, ctx.BaseCommit);

        using var readCtx = db.CreateContext();
        var wtRepo = new WorktreeRepository(readCtx);
        var row = await wtRepo.GetByTaskIdAsync(task.Id);
        Assert.NotNull(row);
        Assert.Equal(WorktreeState.Active, row!.State);
        Assert.Equal(ctx.BaseCommit, row.BaseCommit);
        Assert.Null(row.HeadCommit);
    }

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

        var repo = CreateRepo();
        var (task, list) = MakeEntities(repo.RepoDir);
        var db = new DbFixture();
        _dbFixtures.Add(db);
        using (var seedCtx = db.CreateContext())
        {
            await new ListRepository(seedCtx).AddAsync(list);
            await new TaskRepository(seedCtx).AddAsync(task);
        }

        var fakeHub = new CapturingHubContext();
        var broadcaster = new HubBroadcaster(fakeHub);
        var cfg = new WorkerConfig { WorktreeRootStrategy = "sibling" };
        var mgr = new WorktreeManager(
            new GitService(), db.CreateFactory(), cfg, NullLogger<WorktreeManager>.Instance, broadcaster);

        var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
        _worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));

        var progressCalls = fakeHub.Proxy.Calls.Where(c => c.Method == "OperationProgress").ToList();
        Assert.Single(progressCalls);
        Assert.Equal(task.Id, (string?)progressCalls[0].Args[0]);
        Assert.Equal(WorktreeManager.PhaseCreatingWorktree, (string?)progressCalls[0].Args[1]);
    }

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

        var repo = CreateRepo();
        var (task, list) = MakeEntities(repo.RepoDir);
        var db = new DbFixture();
        _dbFixtures.Add(db);
        using (var seedCtx = db.CreateContext())
        {
            await new ListRepository(seedCtx).AddAsync(list);
            await new TaskRepository(seedCtx).AddAsync(task);
        }

        // Pre-create the branch so `git worktree add -b` hits the "already exists" self-heal path.
        var branchName = $"claudedo/{task.Id.Replace("-", "")}";
        GitRepoFixture.RunGit(repo.RepoDir, "branch", branchName, repo.BaseCommit);

        var fakeHub = new CapturingHubContext();
        var broadcaster = new HubBroadcaster(fakeHub);
        var cfg = new WorkerConfig { WorktreeRootStrategy = "sibling" };
        var mgr = new WorktreeManager(
            new GitService(), db.CreateFactory(), cfg, NullLogger<WorktreeManager>.Instance, broadcaster);

        var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
        _worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));

        // One message for the initial attempt, one for the self-heal retry section -- not a tick stream.
        var progressCalls = fakeHub.Proxy.Calls
            .Where(c => c.Method == "OperationProgress"
                && (string?)c.Args[0] == task.Id
                && (string?)c.Args[1] == WorktreeManager.PhaseCreatingWorktree)
            .ToList();
        Assert.Equal(2, progressCalls.Count);
    }

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

        var repo = CreateRepo();
        var (task, list) = MakeEntities(repo.RepoDir);
        var (mgr, db) = await CreateManagerAsync(task, list);

        var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
        _worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));

        var committed = await mgr.CommitIfChangedAsync(ctx, task, list, CancellationToken.None);

        Assert.False(committed);
        using var readCtx = db.CreateContext();
        var wtRepo = new WorktreeRepository(readCtx);
        var row = await wtRepo.GetByTaskIdAsync(task.Id);
        Assert.Null(row!.HeadCommit);
    }

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

        var repo = CreateRepo();
        var (task, list) = MakeEntities(repo.RepoDir);
        var (mgr, db) = await CreateManagerAsync(task, list);

        var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
        _worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));

        File.WriteAllText(Path.Combine(ctx.WorktreePath, "hello.txt"), "hello world");

        var committed = await mgr.CommitIfChangedAsync(ctx, task, list, CancellationToken.None);

        Assert.True(committed);
        using var readCtx = db.CreateContext();
        var wtRepo = new WorktreeRepository(readCtx);
        var row = await wtRepo.GetByTaskIdAsync(task.Id);
        Assert.NotNull(row!.HeadCommit);
        Assert.NotEqual(ctx.BaseCommit, row.HeadCommit);
        Assert.NotNull(row.DiffStat);
        Assert.Contains("hello.txt", row.DiffStat);
    }

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

        // Fresh `git init`, zero commits, no identity configured — the cross-PC failure
        // that surfaced as "git rev-parse HEAD failed: ambiguous argument 'HEAD'".
        var repoDir = Path.Combine(Path.GetTempPath(), $"claudedo_emptyrepo_{Guid.NewGuid():N}");
        Directory.CreateDirectory(repoDir);
        GitRepoFixture.RunGit(repoDir, "init", "-b", "main");
        _tempDirs.Add(repoDir);

        var (task, list) = MakeEntities(repoDir);
        var (mgr, db) = await CreateManagerAsync(task, list);

        var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
        _worktreeCleanups.Add((repoDir, ctx.WorktreePath));

        Assert.True(Directory.Exists(ctx.WorktreePath));
        // The base commit is the auto-created bootstrap commit.
        var head = GitRepoFixture.RunGit(repoDir, "rev-parse", "HEAD").Trim();
        Assert.Equal(head, ctx.BaseCommit);

        using var readCtx = db.CreateContext();
        var row = await new WorktreeRepository(readCtx).GetByTaskIdAsync(task.Id);
        Assert.Equal(WorktreeState.Active, row!.State);
    }

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

        var tmpDir = Path.Combine(Path.GetTempPath(), $"claudedo_nogit_{Guid.NewGuid():N}");
        Directory.CreateDirectory(tmpDir);
        _tempDirs.Add(tmpDir);

        var (task, list) = MakeEntities(tmpDir);

        var db = new DbFixture();
        _dbFixtures.Add(db);
        using (var seedCtx = db.CreateContext())
        {
            var listRepo = new ListRepository(seedCtx);
            var taskRepo = new TaskRepository(seedCtx);
            await listRepo.AddAsync(list);
            await taskRepo.AddAsync(task);
        }

        var cfg = new WorkerConfig { WorktreeRootStrategy = "sibling" };
        var mgr = new WorktreeManager(
            new GitService(), db.CreateFactory(), cfg, NullLogger<WorktreeManager>.Instance);

        var ex = await Assert.ThrowsAsync<InvalidOperationException>(
            () => mgr.CreateAsync(task, list, CancellationToken.None));
        Assert.Contains("not a git repository", ex.Message);

        using var readCtx = db.CreateContext();
        var wtRepo = new WorktreeRepository(readCtx);
        var row = await wtRepo.GetByTaskIdAsync(task.Id);
        Assert.Null(row);
    }

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

        var repo = CreateRepo();
        var (task, list) = MakeEntities(repo.RepoDir);

        // Seed the list but NOT the task: the worktrees-row insert references
        // tasks(task_id) and fails the FK after `git worktree add` has succeeded.
        var db = new DbFixture();
        _dbFixtures.Add(db);
        using (var seedCtx = db.CreateContext())
            await new ListRepository(seedCtx).AddAsync(list);

        var cfg = new WorkerConfig { WorktreeRootStrategy = "sibling" };
        var mgr = new WorktreeManager(
            new GitService(), db.CreateFactory(), cfg, NullLogger<WorktreeManager>.Instance);

        await Assert.ThrowsAnyAsync<Exception>(
            () => mgr.CreateAsync(task, list, CancellationToken.None));

        var branchName = $"claudedo/{task.Id.Replace("-", "")}";
        var branchList = GitRepoFixture.RunGit(repo.RepoDir, "branch", "--list", branchName);
        Assert.True(string.IsNullOrWhiteSpace(branchList),
            $"orphaned branch {branchName} should be cleaned up, got: {branchList}");

        var worktreeList = GitRepoFixture.RunGit(repo.RepoDir, "worktree", "list");
        Assert.DoesNotContain(task.Id, worktreeList);
    }

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

        var repo = CreateRepo();
        var workingDirWithSeparator = repo.RepoDir + Path.DirectorySeparatorChar;
        var (task, list) = MakeEntities(workingDirWithSeparator);
        var (mgr, db) = await CreateManagerAsync(task, list);

        var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
        _worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));

        var repoDirFull = Path.GetFullPath(repo.RepoDir);
        Assert.False(
            ctx.WorktreePath.StartsWith(repoDirFull + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase),
            $"worktree path {ctx.WorktreePath} should not be nested inside the repo dir {repoDirFull}");
        Assert.True(Directory.Exists(ctx.WorktreePath));
    }

    private static (TaskEntity task, ListEntity list) MakeEntities(string workingDir)
    {
        var listId = Guid.NewGuid().ToString();
        var taskId = Guid.NewGuid().ToString();
        var task = new TaskEntity
        {
            Id = taskId,
            ListId = listId,
            Title = "test task",
            Description = "a description",
            CommitType = "chore",
            CreatedAt = DateTime.UtcNow,
        };
        var list = new ListEntity
        {
            Id = listId,
            Name = "Test List",
            WorkingDir = workingDir,
            CreatedAt = DateTime.UtcNow,
        };
        return (task, list);
    }

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

        var repo = CreateRepo();
        var (task, list) = MakeEntities(repo.RepoDir);
        var (mgr, db) = await CreateManagerAsync(task, list);

        var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
        _worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));
        var worktreePath = ctx.WorktreePath;

        WorktreeEntity wt;
        using (var readCtx = db.CreateContext())
            wt = (await new WorktreeRepository(readCtx).GetByTaskIdAsync(task.Id))!;

        await mgr.DiscardAsync(wt, list.WorkingDir!, CancellationToken.None);

        Assert.False(Directory.Exists(worktreePath), "worktree directory should be gone");

        var branchList = GitRepoFixture.RunGit(repo.RepoDir, "branch", "--list", ctx.BranchName);
        Assert.True(string.IsNullOrWhiteSpace(branchList),
            $"branch {ctx.BranchName} should be deleted, got: {branchList}");

        using var readCtx2 = db.CreateContext();
        var row = await new WorktreeRepository(readCtx2).GetByTaskIdAsync(task.Id);
        Assert.NotNull(row);
        Assert.Equal(WorktreeState.Discarded, row!.State);
    }

    public void Dispose()
    {
        foreach (var (repoDir, wtPath) in _worktreeCleanups)
        {
            try { GitRepoFixture.RunGit(repoDir, "worktree", "remove", "--force", wtPath); } catch { }
        }
        foreach (var f in _fixtures) f.Dispose();
        foreach (var db in _dbFixtures) db.Dispose();
        foreach (var d in _tempDirs)
        {
            try { Directory.Delete(d, true); } catch { }
        }
    }
}
