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

namespace ClaudeDo.Worker.Tests.Services;

public class WorktreeMaintenanceServiceTests : IDisposable
{
    private readonly List<DbFixture> _dbs = new();
    private readonly List<GitRepoFixture> _repos = new();

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

    private DbFixture NewDb() { var d = new DbFixture(); _dbs.Add(d); return d; }
    private GitRepoFixture NewRepo() { var r = new GitRepoFixture(); _repos.Add(r); return r; }

    public void Dispose()
    {
        foreach (var d in _dbs) try { d.Dispose(); } catch { }
        foreach (var r in _repos) try { r.Dispose(); } catch { }
    }

    private static (ListEntity list, TaskEntity task) MakeEntities(string workingDir, ClaudeDo.Data.Models.TaskStatus status = ClaudeDo.Data.Models.TaskStatus.Done)
    {
        var list = new ListEntity
        {
            Id = Guid.NewGuid().ToString(),
            Name = "test",
            WorkingDir = workingDir,
            DefaultCommitType = "feat",
            CreatedAt = DateTime.UtcNow,
        };
        var task = MakeTaskForList(list.Id, status);
        return (list, task);
    }

    private static TaskEntity MakeTaskForList(string listId, ClaudeDo.Data.Models.TaskStatus status)
        => new TaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            ListId = listId,
            Title = "t",
            Description = null,
            Status = status,
            CreatedAt = DateTime.UtcNow,
        };

    private static async Task<string> CreateWorktreeAsync(GitService git, string repoDir, string taskId)
    {
        var wtPath = Path.Combine(Path.GetTempPath(), $"wt_{Guid.NewGuid():N}");
        var baseCommit = await git.RevParseHeadAsync(repoDir);
        await git.WorktreeAddAsync(repoDir, $"test/{taskId}", wtPath, baseCommit);
        return wtPath;
    }

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

        var repo = NewRepo();
        var git = new GitService();
        var db = NewDb();

        var (list, activeTask) = MakeEntities(repo.RepoDir);
        var mergedTask = MakeTaskForList(list.Id, ClaudeDo.Data.Models.TaskStatus.Done);
        var discardedTask = MakeTaskForList(list.Id, ClaudeDo.Data.Models.TaskStatus.Done);

        var activeWt = await CreateWorktreeAsync(git, repo.RepoDir, activeTask.Id);
        var mergedWt = await CreateWorktreeAsync(git, repo.RepoDir, mergedTask.Id);
        var discardedWt = await CreateWorktreeAsync(git, repo.RepoDir, discardedTask.Id);

        using (var ctx = db.CreateContext())
        {
            await new ListRepository(ctx).AddAsync(list);
            var taskRepo = new TaskRepository(ctx);
            await taskRepo.AddAsync(activeTask);
            await taskRepo.AddAsync(mergedTask);
            await taskRepo.AddAsync(discardedTask);
            var wtRepo = new WorktreeRepository(ctx);
            await wtRepo.AddAsync(new WorktreeEntity
            {
                TaskId = activeTask.Id, Path = activeWt, BranchName = $"test/{activeTask.Id}",
                BaseCommit = repo.BaseCommit, State = WorktreeState.Active, CreatedAt = DateTime.UtcNow,
            });
            await wtRepo.AddAsync(new WorktreeEntity
            {
                TaskId = mergedTask.Id, Path = mergedWt, BranchName = $"test/{mergedTask.Id}",
                BaseCommit = repo.BaseCommit, State = WorktreeState.Merged, CreatedAt = DateTime.UtcNow,
            });
            await wtRepo.AddAsync(new WorktreeEntity
            {
                TaskId = discardedTask.Id, Path = discardedWt, BranchName = $"test/{discardedTask.Id}",
                BaseCommit = repo.BaseCommit, State = WorktreeState.Discarded, CreatedAt = DateTime.UtcNow,
            });
        }

        var svc = new WorktreeMaintenanceService(
            db.CreateFactory(), git, NullLogger<WorktreeMaintenanceService>.Instance);

        var result = await svc.CleanupFinishedAsync();

        Assert.Equal(2, result.Removed);
        Assert.True(Directory.Exists(activeWt));
        Assert.False(Directory.Exists(mergedWt));
        Assert.False(Directory.Exists(discardedWt));

        using var checkCtx = db.CreateContext();
        var remaining = await new WorktreeRepository(checkCtx).GetAllAsync();
        Assert.Single(remaining);
        Assert.Equal(activeTask.Id, remaining[0].TaskId);
    }

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

        var repo = NewRepo();
        var git = new GitService();
        var db = NewDb();

        var (list, task) = MakeEntities(repo.RepoDir, status: ClaudeDo.Data.Models.TaskStatus.Running);
        var wt = await CreateWorktreeAsync(git, repo.RepoDir, task.Id);

        using (var ctx = db.CreateContext())
        {
            await new ListRepository(ctx).AddAsync(list);
            await new TaskRepository(ctx).AddAsync(task);
            await new WorktreeRepository(ctx).AddAsync(new WorktreeEntity
            {
                TaskId = task.Id, Path = wt, BranchName = $"test/{task.Id}",
                BaseCommit = repo.BaseCommit, State = WorktreeState.Active, CreatedAt = DateTime.UtcNow,
            });
        }

        var svc = new WorktreeMaintenanceService(
            db.CreateFactory(), git, NullLogger<WorktreeMaintenanceService>.Instance);

        var result = await svc.ResetAllAsync();

        Assert.True(result.Blocked);
        Assert.Equal(1, result.RunningTasks);
        Assert.Equal(0, result.Removed);
        Assert.True(Directory.Exists(wt));

        // Cleanup
        try { await git.WorktreeRemoveAsync(repo.RepoDir, wt, force: true); } catch { }
    }

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

        var repo = NewRepo();
        var git = new GitService();
        var db = NewDb();

        var (list, t1) = MakeEntities(repo.RepoDir, status: ClaudeDo.Data.Models.TaskStatus.Done);
        var t2 = MakeTaskForList(list.Id, ClaudeDo.Data.Models.TaskStatus.Idle);

        var wt1 = await CreateWorktreeAsync(git, repo.RepoDir, t1.Id);
        var wt2 = await CreateWorktreeAsync(git, repo.RepoDir, t2.Id);

        using (var ctx = db.CreateContext())
        {
            await new ListRepository(ctx).AddAsync(list);
            var taskRepo = new TaskRepository(ctx);
            await taskRepo.AddAsync(t1);
            await taskRepo.AddAsync(t2);
            var wtRepo = new WorktreeRepository(ctx);
            await wtRepo.AddAsync(new WorktreeEntity
            {
                TaskId = t1.Id, Path = wt1, BranchName = $"test/{t1.Id}",
                BaseCommit = repo.BaseCommit, State = WorktreeState.Active, CreatedAt = DateTime.UtcNow,
            });
            await wtRepo.AddAsync(new WorktreeEntity
            {
                TaskId = t2.Id, Path = wt2, BranchName = $"test/{t2.Id}",
                BaseCommit = repo.BaseCommit, State = WorktreeState.Kept, CreatedAt = DateTime.UtcNow,
            });
        }

        var svc = new WorktreeMaintenanceService(
            db.CreateFactory(), git, NullLogger<WorktreeMaintenanceService>.Instance);

        var result = await svc.ResetAllAsync();

        Assert.False(result.Blocked);
        Assert.Equal(2, result.Removed);
        Assert.Equal(2, result.TasksAffected);
        Assert.False(Directory.Exists(wt1));
        Assert.False(Directory.Exists(wt2));

        using var checkCtx = db.CreateContext();
        var remaining = await new WorktreeRepository(checkCtx).GetAllAsync();
        Assert.Empty(remaining);
    }

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

        var repo = NewRepo();
        var git = new GitService();
        var db = NewDb();

        var (listA, taskA) = MakeEntities(repo.RepoDir);
        var (listB, taskB) = MakeEntities(repo.RepoDir);

        var wtA = await CreateWorktreeAsync(git, repo.RepoDir, taskA.Id);
        var wtB = await CreateWorktreeAsync(git, repo.RepoDir, taskB.Id);

        using (var ctx = db.CreateContext())
        {
            await new ListRepository(ctx).AddAsync(listA);
            await new ListRepository(ctx).AddAsync(listB);
            var taskRepo = new TaskRepository(ctx);
            await taskRepo.AddAsync(taskA);
            await taskRepo.AddAsync(taskB);
            var wtRepo = new WorktreeRepository(ctx);
            await wtRepo.AddAsync(new WorktreeEntity
            {
                TaskId = taskA.Id, Path = wtA, BranchName = $"test/{taskA.Id}",
                BaseCommit = repo.BaseCommit, State = WorktreeState.Merged, CreatedAt = DateTime.UtcNow,
            });
            await wtRepo.AddAsync(new WorktreeEntity
            {
                TaskId = taskB.Id, Path = wtB, BranchName = $"test/{taskB.Id}",
                BaseCommit = repo.BaseCommit, State = WorktreeState.Merged, CreatedAt = DateTime.UtcNow,
            });
        }

        var svc = new WorktreeMaintenanceService(
            db.CreateFactory(), git, NullLogger<WorktreeMaintenanceService>.Instance);

        var result = await svc.CleanupFinishedAsync(listA.Id, CancellationToken.None);

        Assert.Equal(1, result.Removed);
        Assert.False(Directory.Exists(wtA));
        Assert.True(Directory.Exists(wtB));

        using var checkCtx = db.CreateContext();
        var remaining = await new WorktreeRepository(checkCtx).GetAllAsync();
        Assert.Single(remaining);
        Assert.Equal(taskB.Id, remaining[0].TaskId);
    }

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

        var repo = NewRepo();
        var git = new GitService();
        var db = NewDb();

        var (listA, taskA) = MakeEntities(repo.RepoDir);
        var (listB, taskB) = MakeEntities(repo.RepoDir);
        var wtA = await CreateWorktreeAsync(git, repo.RepoDir, taskA.Id);
        var wtB = await CreateWorktreeAsync(git, repo.RepoDir, taskB.Id);

        using (var ctx = db.CreateContext())
        {
            await new ListRepository(ctx).AddAsync(listA);
            await new ListRepository(ctx).AddAsync(listB);
            await new TaskRepository(ctx).AddAsync(taskA);
            await new TaskRepository(ctx).AddAsync(taskB);
            var wtRepo = new WorktreeRepository(ctx);
            await wtRepo.AddAsync(new WorktreeEntity
            {
                TaskId = taskA.Id, Path = wtA, BranchName = $"test/{taskA.Id}",
                BaseCommit = repo.BaseCommit, State = WorktreeState.Active, CreatedAt = DateTime.UtcNow,
            });
            await wtRepo.AddAsync(new WorktreeEntity
            {
                TaskId = taskB.Id, Path = wtB, BranchName = $"test/{taskB.Id}",
                BaseCommit = repo.BaseCommit, State = WorktreeState.Merged, CreatedAt = DateTime.UtcNow,
            });
        }

        var svc = new WorktreeMaintenanceService(
            db.CreateFactory(), git, NullLogger<WorktreeMaintenanceService>.Instance);

        var rows = await svc.GetOverviewAsync(null, CancellationToken.None);

        Assert.Equal(2, rows.Count);
        Assert.Contains(rows, r => r.TaskId == taskA.Id && r.PathExistsOnDisk);
        Assert.Contains(rows, r => r.TaskId == taskB.Id && r.PathExistsOnDisk);
    }

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

        var repo = NewRepo();
        var git = new GitService();
        var db = NewDb();

        var (listA, taskA) = MakeEntities(repo.RepoDir);
        var (listB, taskB) = MakeEntities(repo.RepoDir);
        var wtA = await CreateWorktreeAsync(git, repo.RepoDir, taskA.Id);
        var wtB = await CreateWorktreeAsync(git, repo.RepoDir, taskB.Id);

        using (var ctx = db.CreateContext())
        {
            await new ListRepository(ctx).AddAsync(listA);
            await new ListRepository(ctx).AddAsync(listB);
            await new TaskRepository(ctx).AddAsync(taskA);
            await new TaskRepository(ctx).AddAsync(taskB);
            var wtRepo = new WorktreeRepository(ctx);
            await wtRepo.AddAsync(new WorktreeEntity
            {
                TaskId = taskA.Id, Path = wtA, BranchName = $"test/{taskA.Id}",
                BaseCommit = repo.BaseCommit, State = WorktreeState.Active, CreatedAt = DateTime.UtcNow,
            });
            await wtRepo.AddAsync(new WorktreeEntity
            {
                TaskId = taskB.Id, Path = wtB, BranchName = $"test/{taskB.Id}",
                BaseCommit = repo.BaseCommit, State = WorktreeState.Active, CreatedAt = DateTime.UtcNow,
            });
        }

        var svc = new WorktreeMaintenanceService(
            db.CreateFactory(), git, NullLogger<WorktreeMaintenanceService>.Instance);

        var rows = await svc.GetOverviewAsync(listA.Id, CancellationToken.None);

        Assert.Single(rows);
        Assert.Equal(taskA.Id, rows[0].TaskId);
    }

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

        var repo = NewRepo();
        var git = new GitService();
        var db = NewDb();

        var (list, task) = MakeEntities(repo.RepoDir);
        var wt = await CreateWorktreeAsync(git, repo.RepoDir, task.Id);

        try { await git.WorktreeRemoveAsync(repo.RepoDir, wt, force: true); } catch { }
        if (Directory.Exists(wt)) Directory.Delete(wt, recursive: true);

        using (var ctx = db.CreateContext())
        {
            await new ListRepository(ctx).AddAsync(list);
            await new TaskRepository(ctx).AddAsync(task);
            await new WorktreeRepository(ctx).AddAsync(new WorktreeEntity
            {
                TaskId = task.Id, Path = wt, BranchName = $"test/{task.Id}",
                BaseCommit = repo.BaseCommit, State = WorktreeState.Active, CreatedAt = DateTime.UtcNow,
            });
        }

        var svc = new WorktreeMaintenanceService(
            db.CreateFactory(), git, NullLogger<WorktreeMaintenanceService>.Instance);

        var rows = await svc.GetOverviewAsync(null, CancellationToken.None);

        Assert.Single(rows);
        Assert.False(rows[0].PathExistsOnDisk);
    }

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

        var repo = NewRepo();
        var git = new GitService();
        var db = NewDb();

        var (list, task) = MakeEntities(repo.RepoDir, status: ClaudeDo.Data.Models.TaskStatus.Done);
        var wt = await CreateWorktreeAsync(git, repo.RepoDir, task.Id);

        using (var ctx = db.CreateContext())
        {
            await new ListRepository(ctx).AddAsync(list);
            await new TaskRepository(ctx).AddAsync(task);
            await new WorktreeRepository(ctx).AddAsync(new WorktreeEntity
            {
                TaskId = task.Id, Path = wt, BranchName = $"test/{task.Id}",
                BaseCommit = repo.BaseCommit, State = WorktreeState.Active, CreatedAt = DateTime.UtcNow,
            });
        }

        var svc = new WorktreeMaintenanceService(
            db.CreateFactory(), git, NullLogger<WorktreeMaintenanceService>.Instance);

        var result = await svc.ForceRemoveAsync(task.Id, CancellationToken.None);

        Assert.True(result.Removed);
        Assert.Null(result.Reason);
        Assert.False(Directory.Exists(wt));

        using var checkCtx = db.CreateContext();
        var remaining = await new WorktreeRepository(checkCtx).GetAllAsync();
        Assert.Empty(remaining);
    }

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

        var repo = NewRepo();
        var git = new GitService();
        var db = NewDb();

        var (list, task) = MakeEntities(repo.RepoDir, status: ClaudeDo.Data.Models.TaskStatus.Running);
        var wt = await CreateWorktreeAsync(git, repo.RepoDir, task.Id);

        using (var ctx = db.CreateContext())
        {
            await new ListRepository(ctx).AddAsync(list);
            await new TaskRepository(ctx).AddAsync(task);
            await new WorktreeRepository(ctx).AddAsync(new WorktreeEntity
            {
                TaskId = task.Id, Path = wt, BranchName = $"test/{task.Id}",
                BaseCommit = repo.BaseCommit, State = WorktreeState.Active, CreatedAt = DateTime.UtcNow,
            });
        }

        var svc = new WorktreeMaintenanceService(
            db.CreateFactory(), git, NullLogger<WorktreeMaintenanceService>.Instance);

        var result = await svc.ForceRemoveAsync(task.Id, CancellationToken.None);

        Assert.False(result.Removed);
        Assert.Equal("task is currently running", result.Reason);
        Assert.True(Directory.Exists(wt));

        try { await git.WorktreeRemoveAsync(repo.RepoDir, wt, force: true); } catch { }
    }

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

        var repo = NewRepo();
        var git = new GitService();
        var db = NewDb();

        var (list, task) = MakeEntities(repo.RepoDir);
        var phantomPath = Path.Combine(Path.GetTempPath(), $"wt_phantom_{Guid.NewGuid():N}");

        using (var ctx = db.CreateContext())
        {
            await new ListRepository(ctx).AddAsync(list);
            await new TaskRepository(ctx).AddAsync(task);
            await new WorktreeRepository(ctx).AddAsync(new WorktreeEntity
            {
                TaskId = task.Id, Path = phantomPath, BranchName = $"test/{task.Id}-phantom",
                BaseCommit = repo.BaseCommit, State = WorktreeState.Active, CreatedAt = DateTime.UtcNow,
            });
        }

        var svc = new WorktreeMaintenanceService(
            db.CreateFactory(), git, NullLogger<WorktreeMaintenanceService>.Instance);

        var result = await svc.ForceRemoveAsync(task.Id, CancellationToken.None);

        Assert.True(result.Removed);

        using var checkCtx = db.CreateContext();
        var remaining = await new WorktreeRepository(checkCtx).GetAllAsync();
        Assert.Empty(remaining);
    }

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

        var repo = NewRepo();
        var git = new GitService();
        var db = NewDb();

        var (list, t1) = MakeEntities(repo.RepoDir, status: ClaudeDo.Data.Models.TaskStatus.Done);
        var t2 = MakeTaskForList(list.Id, ClaudeDo.Data.Models.TaskStatus.Idle);

        var wt1 = await CreateWorktreeAsync(git, repo.RepoDir, t1.Id);
        var wt2 = await CreateWorktreeAsync(git, repo.RepoDir, t2.Id);

        using (var ctx = db.CreateContext())
        {
            await new ListRepository(ctx).AddAsync(list);
            var taskRepo = new TaskRepository(ctx);
            await taskRepo.AddAsync(t1);
            await taskRepo.AddAsync(t2);
            var wtRepo = new WorktreeRepository(ctx);
            await wtRepo.AddAsync(new WorktreeEntity
            {
                TaskId = t1.Id, Path = wt1, BranchName = $"test/{t1.Id}",
                BaseCommit = repo.BaseCommit, State = WorktreeState.Active, CreatedAt = DateTime.UtcNow,
            });
            await wtRepo.AddAsync(new WorktreeEntity
            {
                TaskId = t2.Id, Path = wt2, BranchName = $"test/{t2.Id}",
                BaseCommit = repo.BaseCommit, State = WorktreeState.Kept, CreatedAt = DateTime.UtcNow,
            });
        }

        var hub = new CapturingHubContext();
        var svc = new WorktreeMaintenanceService(
            db.CreateFactory(), git, NullLogger<WorktreeMaintenanceService>.Instance, new HubBroadcaster(hub));

        var result = await svc.ResetAllAsync();

        Assert.Equal(2, result.Removed);
        var progressCalls = hub.Proxy.Calls.Where(c => c.Method == "OperationProgress").ToList();
        Assert.Equal(2, progressCalls.Count);
        Assert.All(progressCalls, c =>
        {
            Assert.Equal(WorktreeMaintenanceService.OpKey, c.Args[0]);
            Assert.Equal(WorktreeMaintenanceService.PhaseMaintaining, c.Args[1]);
            Assert.Equal(2, c.Args[3]);
        });
        Assert.Equal(1, progressCalls[0].Args[2]);
        Assert.Equal(2, progressCalls[1].Args[2]);
    }
}
