using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Config;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Runner;
using ClaudeDo.Worker.Lifecycle;
using ClaudeDo.Worker.Tests.Infrastructure;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging.Abstractions;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Worker.Tests.Services;

public class TaskResetServiceTests : IDisposable
{
    private readonly List<DbFixture> _dbs = new();
    private readonly List<GitRepoFixture> _repos = new();
    private readonly List<(string repoDir, string wtPath)> _worktreeCleanups = new();

    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 (repoDir, wtPath) in _worktreeCleanups)
        {
            try { GitRepoFixture.RunGit(repoDir, "worktree", "remove", "--force", wtPath); } catch { }
        }
        foreach (var d in _dbs) try { d.Dispose(); } catch { }
        foreach (var r in _repos) try { r.Dispose(); } catch { }
    }

    private static (TaskResetService svc, RecordingClientProxy proxy) BuildService(DbFixture db, WorktreeManager wtMgr)
    {
        var fakeHub = new RecordingHubContext();
        var broadcaster = new HubBroadcaster(fakeHub);
        var built = TaskStateServiceBuilder.Build(db.CreateFactory());
        var svc = new TaskResetService(
            db.CreateFactory(),
            wtMgr,
            broadcaster,
            built.State,
            NullLogger<TaskResetService>.Instance);
        return (svc, fakeHub.Proxy);
    }

    private static WorktreeManager BuildWorktreeManager(DbFixture db)
    {
        var cfg = new WorkerConfig { WorktreeRootStrategy = "sibling" };
        return new WorktreeManager(
            new ClaudeDo.Data.Git.GitService(),
            db.CreateFactory(),
            cfg,
            NullLogger<WorktreeManager>.Instance);
    }

    [Fact]
    public async Task ResetAsync_FailedTaskWithWorktree_ClearsEverything_AndPreservesRunHistory()
    {
        if (!GitRepoFixture.IsGitAvailable()) return;

        var repo = NewRepo();
        var db = NewDb();
        var wtMgr = BuildWorktreeManager(db);

        var list = new ListEntity
        {
            Id = Guid.NewGuid().ToString(),
            Name = "reset-test",
            WorkingDir = repo.RepoDir,
            DefaultCommitType = "feat",
            CreatedAt = DateTime.UtcNow,
        };
        var task = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            ListId = list.Id,
            Title = "test task",
            Status = TaskStatus.Failed,
            StartedAt = DateTime.UtcNow.AddMinutes(-5),
            FinishedAt = DateTime.UtcNow.AddMinutes(-1),
            Result = "some error",
            CreatedAt = DateTime.UtcNow,
        };

        using (var ctx = db.CreateContext())
        {
            await new ListRepository(ctx).AddAsync(list);
            await new TaskRepository(ctx).AddAsync(task);
        }

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

        using (var ctx = db.CreateContext())
        {
            await new TaskRunRepository(ctx).AddAsync(new TaskRunEntity
            {
                Id = Guid.NewGuid().ToString(),
                TaskId = task.Id,
                RunNumber = 1,
                IsRetry = false,
                Prompt = "do the thing",
                SessionId = "s1",
            });
        }

        var (svc, proxy) = BuildService(db, wtMgr);
        await svc.ResetAsync(task.Id, CancellationToken.None);

        // Task must be Manual with cleared timestamps/result
        using (var ctx = db.CreateContext())
        {
            var updated = await new TaskRepository(ctx).GetByIdAsync(task.Id);
            Assert.NotNull(updated);
            Assert.Equal(TaskStatus.Idle, updated!.Status);
            Assert.Null(updated.Result);
            Assert.Null(updated.StartedAt);
            Assert.Null(updated.FinishedAt);
        }

        // Worktree directory must be gone
        Assert.False(Directory.Exists(wtCtx.WorktreePath));

        // Worktree DB row must be Discarded
        using (var ctx = db.CreateContext())
        {
            var wt = await new WorktreeRepository(ctx).GetByTaskIdAsync(task.Id);
            Assert.NotNull(wt);
            Assert.Equal(WorktreeState.Discarded, wt!.State);
        }

        // task_runs row must still be present
        using (var ctx = db.CreateContext())
        {
            var runs = await new TaskRunRepository(ctx).GetByTaskIdAsync(task.Id);
            Assert.Single(runs);
            Assert.Equal("s1", runs[0].SessionId);
        }

        // Broadcaster must have fired TaskUpdated AND WorktreeUpdated
        Assert.Contains(proxy.Calls, i => i.Method == "TaskUpdated" && i.Args[0] is string s && s == task.Id);
        Assert.Contains(proxy.Calls, i => i.Method == "WorktreeUpdated" && i.Args[0] is string s && s == task.Id);
    }

    [Fact]
    public async Task ResetAsync_BroadcastsWorkerLog_ContainingTaskNumber()
    {
        if (!GitRepoFixture.IsGitAvailable()) return;

        var repo = NewRepo();
        var db = NewDb();
        var wtMgr = BuildWorktreeManager(db);

        var list = new ListEntity
        {
            Id = Guid.NewGuid().ToString(),
            Name = "reset-number-test",
            WorkingDir = repo.RepoDir,
            DefaultCommitType = "feat",
            CreatedAt = DateTime.UtcNow,
        };
        var task = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            ListId = list.Id,
            Title = "numbered task",
            Number = 321,
            Status = TaskStatus.Failed,
            StartedAt = DateTime.UtcNow.AddMinutes(-5),
            FinishedAt = DateTime.UtcNow.AddMinutes(-1),
            Result = "some error",
            CreatedAt = DateTime.UtcNow,
        };

        using (var ctx = db.CreateContext())
        {
            // Bypass TaskRepository.AddAsync — it allocates its own Number via
            // TaskNumberAllocator and would silently overwrite the one this test sets.
            ctx.Lists.Add(list);
            ctx.Tasks.Add(task);
            await ctx.SaveChangesAsync();
        }

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

        var (svc, proxy) = BuildService(db, wtMgr);
        await svc.ResetAsync(task.Id, CancellationToken.None);

        Assert.Contains(proxy.Calls, c => c.Method == "WorkerLog"
            && c.Args[0] is string s && s.Contains("#321"));
    }

    [Fact]
    public async Task ResetAsync_RunningTask_Throws_AndDoesNotMutate()
    {
        var db = NewDb();
        var wtMgr = BuildWorktreeManager(db);

        var list = new ListEntity
        {
            Id = Guid.NewGuid().ToString(),
            Name = "no-op list",
            WorkingDir = null,
            DefaultCommitType = "feat",
            CreatedAt = DateTime.UtcNow,
        };
        var startedAt = DateTime.UtcNow.AddMinutes(-2);
        var task = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            ListId = list.Id,
            Title = "running task",
            Status = TaskStatus.Running,
            StartedAt = startedAt,
            CreatedAt = DateTime.UtcNow,
        };

        using (var ctx = db.CreateContext())
        {
            await new ListRepository(ctx).AddAsync(list);
            await new TaskRepository(ctx).AddAsync(task);
        }

        var (svc, proxy) = BuildService(db, wtMgr);

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => svc.ResetAsync(task.Id, CancellationToken.None));

        // Task must be unchanged
        using (var ctx = db.CreateContext())
        {
            var unchanged = await new TaskRepository(ctx).GetByIdAsync(task.Id);
            Assert.NotNull(unchanged);
            Assert.Equal(TaskStatus.Running, unchanged!.Status);
            Assert.Equal(startedAt, unchanged.StartedAt);
        }

        // No broadcaster invocations
        Assert.Empty(proxy.Calls);
    }
}

#region Test doubles

internal sealed record HubCall(string Method, object?[] Args);

internal sealed class RecordingClientProxy : IClientProxy
{
    public readonly List<HubCall> Calls = new();

    public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default)
    {
        Calls.Add(new HubCall(method, args));
        return Task.CompletedTask;
    }
}

internal sealed class RecordingHubClients : IHubClients
{
    public RecordingClientProxy AllProxy { get; } = new();
    public IClientProxy All => AllProxy;
    public IClientProxy AllExcept(IReadOnlyList<string> excludedConnectionIds) => AllProxy;
    public IClientProxy Client(string connectionId) => AllProxy;
    public IClientProxy Clients(IReadOnlyList<string> connectionIds) => AllProxy;
    public IClientProxy Group(string groupName) => AllProxy;
    public IClientProxy GroupExcept(string groupName, IReadOnlyList<string> excludedConnectionIds) => AllProxy;
    public IClientProxy Groups(IReadOnlyList<string> groupNames) => AllProxy;
    public IClientProxy User(string userId) => AllProxy;
    public IClientProxy Users(IReadOnlyList<string> userIds) => AllProxy;
}

internal sealed class RecordingHubContext : IHubContext<ClaudeDo.Worker.Hub.WorkerHub>
{
    private readonly RecordingHubClients _clients = new();
    public RecordingClientProxy Proxy => _clients.AllProxy;
    public IHubClients Clients => _clients;
    public IGroupManager Groups => throw new NotImplementedException();
}

#endregion
