using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Tests.Infrastructure;
using Microsoft.AspNetCore.SignalR;
using Xunit;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Worker.Tests.Hub;

/// Covers the guarded "manual done toggle" (SetTaskDone/UnsetTaskDone) and "remove from queue"
/// (DequeueTask) hub methods added to replace the UI's raw EF writes -- each checks the expected
/// starting status server-side so a concurrent picker claim can't be silently overwritten.
public sealed class TaskDoneDequeueHubTests : IDisposable
{
    private readonly DbFixture _db = new();
    private readonly ClaudeDoDbContext _ctx;
    private readonly TaskRepository _tasks;
    private readonly ListRepository _lists;
    private readonly RecordingClientProxy _proxy = new();

    public TaskDoneDequeueHubTests()
    {
        _ctx = _db.CreateContext();
        _tasks = new TaskRepository(_ctx);
        _lists = new ListRepository(_ctx);
    }

    public void Dispose()
    {
        _ctx.Dispose();
        _db.Dispose();
    }

    private WorkerHub CreateHub()
    {
        var factory = _db.CreateFactory();
        var built = TaskStateServiceBuilder.Build(factory);
        var hub = new WorkerHub(
            null!, null!, null!, null!, null!, factory, null!, null!, null!,
            null!, null!, null!, null!, null!, null!, null!, built.State, null!, null!,
            null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
            new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
        hub.Clients = new FakeHubCallerClients(_proxy);
        hub.Context = new FakeHubCallerContext();
        return hub;
    }

    private async Task<string> SeedListAsync()
    {
        var listId = Guid.NewGuid().ToString();
        await _lists.AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow });
        return listId;
    }

    private async Task<TaskEntity> SeedTaskAsync(
        string listId, TaskStatus status, string? blockedByTaskId = null, string title = "T")
    {
        var task = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            ListId = listId,
            Title = title,
            Status = status,
            BlockedByTaskId = blockedByTaskId,
            CreatedAt = DateTime.UtcNow,
        };
        await _tasks.AddAsync(task);
        return task;
    }

    // ── SetTaskDone ──

    [Fact]
    public async Task SetTaskDone_FromIdle_TransitionsToDone()
    {
        var listId = await SeedListAsync();
        var task = await SeedTaskAsync(listId, TaskStatus.Idle);

        var hub = CreateHub();
        await hub.SetTaskDone(task.Id);

        var reloaded = await _tasks.GetByIdAsync(task.Id);
        Assert.Equal(TaskStatus.Done, reloaded!.Status);
    }

    [Fact]
    public async Task SetTaskDone_FromRunning_Throws_AndLeavesStatusUnchanged()
    {
        var listId = await SeedListAsync();
        var task = await SeedTaskAsync(listId, TaskStatus.Running);

        var hub = CreateHub();
        await Assert.ThrowsAsync<HubException>(() => hub.SetTaskDone(task.Id));

        var reloaded = await _tasks.GetByIdAsync(task.Id);
        Assert.Equal(TaskStatus.Running, reloaded!.Status);
    }

    // Regression: the guard was Idle-only, so a finished task (typically a list-handler run,
    // which has no worktree to merge) could no longer be ticked off from the task card.
    [Fact]
    public async Task SetTaskDone_FromWaitingForReview_TransitionsToDone()
    {
        var listId = await SeedListAsync();
        var task = await SeedTaskAsync(listId, TaskStatus.WaitingForReview);

        var hub = CreateHub();
        await hub.SetTaskDone(task.Id);

        var reloaded = await _tasks.GetByIdAsync(task.Id);
        Assert.Equal(TaskStatus.Done, reloaded!.Status);
    }

    // ── UnsetTaskDone ──

    [Fact]
    public async Task UnsetTaskDone_FromDone_TransitionsToIdle()
    {
        var listId = await SeedListAsync();
        var task = await SeedTaskAsync(listId, TaskStatus.Done);

        var hub = CreateHub();
        await hub.UnsetTaskDone(task.Id);

        var reloaded = await _tasks.GetByIdAsync(task.Id);
        Assert.Equal(TaskStatus.Idle, reloaded!.Status);
    }

    [Fact]
    public async Task UnsetTaskDone_FromRunning_Throws_AndLeavesStatusUnchanged()
    {
        var listId = await SeedListAsync();
        var task = await SeedTaskAsync(listId, TaskStatus.Running);

        var hub = CreateHub();
        await Assert.ThrowsAsync<HubException>(() => hub.UnsetTaskDone(task.Id));

        var reloaded = await _tasks.GetByIdAsync(task.Id);
        Assert.Equal(TaskStatus.Running, reloaded!.Status);
    }

    // ── DequeueTask ──

    [Fact]
    public async Task DequeueTask_FromQueued_TransitionsToIdle_AndClearsBlockedByTaskId()
    {
        var listId = await SeedListAsync();
        var pred = await SeedTaskAsync(listId, TaskStatus.Queued);
        var task = await SeedTaskAsync(listId, TaskStatus.Queued, blockedByTaskId: pred.Id);

        var hub = CreateHub();
        await hub.DequeueTask(task.Id);

        var reloaded = await _tasks.GetByIdAsync(task.Id);
        Assert.Equal(TaskStatus.Idle, reloaded!.Status);
        Assert.Null(reloaded.BlockedByTaskId);
    }

    [Fact]
    public async Task DequeueTask_FromRunning_Throws_AndLeavesStatusUnchanged()
    {
        // Simulates the picker having already claimed the task between the UI reading its
        // row and the dequeue call landing -- the DB status must not be clobbered.
        var listId = await SeedListAsync();
        var task = await SeedTaskAsync(listId, TaskStatus.Running);

        var hub = CreateHub();
        await Assert.ThrowsAsync<HubException>(() => hub.DequeueTask(task.Id));

        var reloaded = await _tasks.GetByIdAsync(task.Id);
        Assert.Equal(TaskStatus.Running, reloaded!.Status);
    }
}
