    [Fact]
    public async Task GetTaskWorktree_NoWorktree_Throws()
    {
        var listId = await SeedListAsync();
        var task = await SeedTaskAsync(listId);
        var sut = BuildSut(CreateQueue());

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => sut.GetTaskWorktree(task.Id, CancellationToken.None));
    }

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

        var (task, _, wt) = await SeedWorktreeAsync();
        var sut = BuildSut(CreateQueue());

        var info = await sut.GetTaskWorktree(task.Id, CancellationToken.None);

        Assert.Equal(wt.BranchName, info.Branch);
        Assert.Equal(wt.BaseCommit, info.BaseCommit);
        Assert.Equal(0, info.Ahead);
        Assert.False(info.IsDirty);
    }

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

        var (task, _, _) = await SeedWorktreeAsync();
        var sut = BuildSut(CreateQueue());

        var info = await sut.GetTaskWorktree(task.Id, CancellationToken.None);

        Assert.Null(info.MergeCommit);
    }

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

        var (task, list, wt) = await SeedWorktreeAsync(TaskStatus.Done);
        File.WriteAllText(Path.Combine(wt.WorktreePath, "added.txt"), "new\n");
        GitRepoFixture.RunGit(wt.WorktreePath, "add", "-A");
        GitRepoFixture.RunGit(wt.WorktreePath, "commit", "-m", "feat: add file");

        var target = await new GitService().GetCurrentBranchAsync(list.WorkingDir!, CancellationToken.None);
        var sut = BuildSut(CreateQueue());
        await sut.MergeTask(task.Id, target, true, false, false, cancellationToken: CancellationToken.None);

        var info = await sut.GetTaskWorktree(task.Id, CancellationToken.None);

        var expected = GitRepoFixture.RunGit(list.WorkingDir!, "rev-parse", "HEAD").Trim();
        Assert.Equal(expected, info.MergeCommit);
    }

    [Fact]
    public async Task GetTaskDiff_NoWorktree_Throws()
    {
        var listId = await SeedListAsync();
        var task = await SeedTaskAsync(listId);
        var sut = BuildSut(CreateQueue());

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => sut.GetTaskDiff(task.Id, false, null, CancellationToken.None));
    }

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

        var (task, list, wt) = await SeedWorktreeAsync();
        File.WriteAllText(Path.Combine(wt.WorktreePath, "added.txt"), "content");

        var cfg = new WorkerConfig { WorktreeRootStrategy = "sibling" };
        var mgr = new WorktreeManager(new GitService(), _db.CreateFactory(), cfg, NullLogger<WorktreeManager>.Instance);
        await mgr.CommitIfChangedAsync(wt, task, list, CancellationToken.None);

        var sut = BuildSut(CreateQueue());
        var diff = await sut.GetTaskDiff(task.Id, false, null, CancellationToken.None);

        Assert.Contains("added.txt", diff.Files);
        Assert.False(diff.Truncated);
    }

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

        var (task, list, wt) = await SeedWorktreeAsync();
        File.WriteAllText(Path.Combine(wt.WorktreePath, "added.txt"), "content");
        File.WriteAllText(Path.Combine(wt.WorktreePath, "other.txt"), "other content");

        var cfg = new WorkerConfig { WorktreeRootStrategy = "sibling" };
        var mgr = new WorktreeManager(new GitService(), _db.CreateFactory(), cfg, NullLogger<WorktreeManager>.Instance);
        await mgr.CommitIfChangedAsync(wt, task, list, CancellationToken.None);

        var sut = BuildSut(CreateQueue());
        var diff = await sut.GetTaskDiff(task.Id, false, ["added.txt"], CancellationToken.None);

        Assert.Equal(new[] { "added.txt" }, diff.Files);
        Assert.DoesNotContain("other.txt", diff.Content);
    }

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

        var (task, list, wt) = await SeedWorktreeAsync();
        File.WriteAllText(Path.Combine(wt.WorktreePath, "added.txt"), "content");
        File.WriteAllText(Path.Combine(wt.WorktreePath, "other.txt"), "other content");

        var cfg = new WorkerConfig { WorktreeRootStrategy = "sibling" };
        var mgr = new WorktreeManager(new GitService(), _db.CreateFactory(), cfg, NullLogger<WorktreeManager>.Instance);
        await mgr.CommitIfChangedAsync(wt, task, list, CancellationToken.None);

        var sut = BuildSut(CreateQueue());
        var diff = await sut.GetTaskDiff(task.Id, true, ["other.txt"], CancellationToken.None);

        Assert.Equal(new[] { "other.txt" }, diff.Files);
        Assert.DoesNotContain("added.txt", diff.Content);
    }

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

        var repo = new GitRepoFixture();
        _repos.Add(repo);
        var listId = Guid.NewGuid().ToString();
        await _lists.AddAsync(new ListEntity { Id = listId, Name = "L", WorkingDir = repo.RepoDir, CreatedAt = DateTime.UtcNow });
        var task = await SeedTaskAsync(listId, status: TaskStatus.WaitingForReview);
        // No WorktreeEntity for this task -- it's a worktree-less list-handler host task
        // (Mission Control's "Let Claude handle it"): the handler merged its own commit
        // straight into the list's working dir after HandlerBaseCommit was stamped.

        File.WriteAllText(Path.Combine(repo.RepoDir, "handled.txt"), "content");
        GitRepoFixture.RunGit(repo.RepoDir, "add", "-A");
        GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "handled task merge");
        var headCommit = GitRepoFixture.RunGit(repo.RepoDir, "rev-parse", "HEAD").Trim();

        using (var ctx = _db.CreateContext())
        {
            var t = await ctx.Tasks.FindAsync(task.Id);
            t!.HandlerBaseCommit = repo.BaseCommit;
            t.HandlerHeadCommit = headCommit;
            await ctx.SaveChangesAsync();
        }

        var sut = BuildSut(CreateQueue());
        var diff = await sut.GetTaskDiff(task.Id, false, null, CancellationToken.None);

        Assert.Contains("handled.txt", diff.Files);
        Assert.False(diff.Truncated);
    }

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

        var repo = new GitRepoFixture();
        _repos.Add(repo);
        var listId = Guid.NewGuid().ToString();
        await _lists.AddAsync(new ListEntity { Id = listId, Name = "L", WorkingDir = repo.RepoDir, CreatedAt = DateTime.UtcNow });
        var task = await SeedTaskAsync(listId, status: TaskStatus.WaitingForReview);

        File.WriteAllText(Path.Combine(repo.RepoDir, "handled.txt"), "content");
        GitRepoFixture.RunGit(repo.RepoDir, "add", "-A");
        GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "handled task merge");
        var headCommit = GitRepoFixture.RunGit(repo.RepoDir, "rev-parse", "HEAD").Trim();

        using (var ctx = _db.CreateContext())
        {
            var t = await ctx.Tasks.FindAsync(task.Id);
            t!.HandlerBaseCommit = repo.BaseCommit;
            t.HandlerHeadCommit = headCommit;
            await ctx.SaveChangesAsync();
        }

        var sut = BuildSut(CreateQueue());
        var diff = await sut.GetTaskDiff(task.Id, true, null, CancellationToken.None);

        Assert.Contains("handled.txt", diff.Content);
    }

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

        var original = ExternalMcpService.ProgressReportInterval;
        ExternalMcpService.ProgressReportInterval = TimeSpan.Zero;
        try
        {
            var (task, list, wt) = await SeedWorktreeAsync();
            File.WriteAllText(Path.Combine(wt.WorktreePath, "added.txt"), "content");
            var cfg = new WorkerConfig { WorktreeRootStrategy = "sibling" };
            var mgr = new WorktreeManager(new GitService(), _db.CreateFactory(), cfg, NullLogger<WorktreeManager>.Instance);
            await mgr.CommitIfChangedAsync(wt, task, list, CancellationToken.None);

            var sut = BuildSut(CreateQueue());
            var progress = new SyncProgress<ProgressNotificationValue>();

            var diff = await sut.GetTaskDiff(task.Id, false, null, CancellationToken.None, progress);

            Assert.Contains("added.txt", diff.Files);
            Assert.NotEmpty(progress.Reports);
            Assert.All(progress.Reports, r => Assert.Contains("computing diff", r.Message));
        }
        finally
        {
            ExternalMcpService.ProgressReportInterval = original;
        }
    }

    [Fact]
    public async Task ListWorktrees_Empty_ReturnsEmpty()
    {
        var sut = BuildSut(CreateQueue());

        var rows = await sut.ListWorktrees(CancellationToken.None);

        Assert.Empty(rows);
    }

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

        var (task, _, _) = await SeedWorktreeAsync();
        var sut = BuildSut(CreateQueue());

        var rows = await sut.ListWorktrees(CancellationToken.None);

        Assert.Contains(rows, r => r.TaskId == task.Id);
    }

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

        var original = ExternalMcpService.ProgressReportInterval;
        ExternalMcpService.ProgressReportInterval = TimeSpan.Zero;
        try
        {
            await SeedWorktreeAsync();
            var sut = BuildSut(CreateQueue());
            var progress = new SyncProgress<ProgressNotificationValue>();

            var rows = await sut.ListWorktrees(CancellationToken.None, progress);

            Assert.NotEmpty(rows);
            Assert.NotEmpty(progress.Reports);
        }
        finally
        {
            ExternalMcpService.ProgressReportInterval = original;
        }
    }

    [Fact]
    public async Task CleanupTaskWorktree_NotFound_Throws()
    {
        var sut = BuildSut(CreateQueue());

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => sut.CleanupTaskWorktree("does-not-exist", false, CancellationToken.None));
    }

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

        var (task, _, _) = await SeedWorktreeAsync(TaskStatus.Running);
        var sut = BuildSut(CreateQueue());

        var ex = await Assert.ThrowsAsync<InvalidOperationException>(
            () => sut.CleanupTaskWorktree(task.Id, false, CancellationToken.None));
        Assert.Contains("running", ex.Message, StringComparison.OrdinalIgnoreCase);
    }

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

        var (task, _, wt) = await SeedWorktreeAsync(TaskStatus.Done);
        var sut = BuildSut(CreateQueue());

        var result = await sut.CleanupTaskWorktree(task.Id, false, CancellationToken.None);

        Assert.True(result.Removed);
        Assert.False(Directory.Exists(wt.WorktreePath));
    }

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

        var original = ExternalMcpService.ProgressReportInterval;
        ExternalMcpService.ProgressReportInterval = TimeSpan.Zero;
        try
        {
            var (task, _, wt) = await SeedWorktreeAsync(TaskStatus.Done);
            var sut = BuildSut(CreateQueue());
            var progress = new SyncProgress<ProgressNotificationValue>();

            var result = await sut.CleanupTaskWorktree(task.Id, false, CancellationToken.None, progress);

            Assert.True(result.Removed);
            Assert.False(Directory.Exists(wt.WorktreePath));
            Assert.NotEmpty(progress.Reports);
        }
        finally
        {
            ExternalMcpService.ProgressReportInterval = original;
        }
    }
