using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Queue;
using ClaudeDo.Worker.Tests.Infrastructure;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Worker.Tests.Queue;

public sealed class QueuePickerTests : IDisposable
{
    private readonly DbFixture _db = new();
    private readonly ClaudeDoDbContext _ctx;
    private readonly TaskRepository _tasks;
    private readonly ListRepository _lists;
    private readonly QueuePicker _picker;

    public QueuePickerTests()
    {
        _ctx = _db.CreateContext();
        _tasks = new TaskRepository(_ctx);
        _lists = new ListRepository(_ctx);
        _picker = new QueuePicker(_db.CreateFactory());
    }

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

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

    private async Task<TaskEntity> SeedAsync(
        string listId,
        TaskStatus status = TaskStatus.Queued,
        DateTime? createdAt = null,
        DateTime? scheduledFor = null,
        string? blockedBy = null,
        bool taskAgentTag = false,
        int? sortOrder = null,
        bool isManual = false,
        string? dependsOn = null)
    {
        var task = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            ListId = listId,
            Title = "T",
            Status = status,
            CreatedAt = createdAt ?? DateTime.UtcNow,
            ScheduledFor = scheduledFor,
            BlockedByTaskId = blockedBy,
            DependsOnTaskId = dependsOn,
            CommitType = "feat",
            IsManual = isManual,
        };
        await _tasks.AddAsync(task);
        if (sortOrder is not null)
        {
            task.SortOrder = sortOrder.Value;
            await _tasks.UpdateAsync(task);
        }
        return task;
    }

    [Fact]
    public async Task ClaimNextAsync_Skips_ManualTasks()
    {
        var listId = await CreateListAsync();
        await SeedAsync(listId, isManual: true);

        Assert.Null(await _picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None));
    }

    [Fact]
    public async Task ClaimNextAsync_Skips_ManualTask_ButClaimsTheNextOne()
    {
        var listId = await CreateListAsync();
        await SeedAsync(listId, createdAt: DateTime.UtcNow.AddMinutes(-5), isManual: true);
        var claudeTask = await SeedAsync(listId, createdAt: DateTime.UtcNow);

        var picked = await _picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None);

        Assert.NotNull(picked);
        Assert.Equal(claudeTask.Id, picked!.Id);
    }

    [Fact]
    public async Task ClaimNextAsync_Skips_TasksWithBlockedByTaskId()
    {
        var listId = await CreateListAsync(listAgentTag: true);
        var blocker = await SeedAsync(listId);
        await SeedAsync(listId, blockedBy: blocker.Id);

        // Only `blocker` is unblocked → it should be claimed; the second pick is null.
        var first = await _picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None);
        Assert.NotNull(first);
        Assert.Equal(blocker.Id, first!.Id);

        var second = await _picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None);
        Assert.Null(second);
    }

    [Fact]
    public async Task ClaimNextAsync_Skips_TasksWithUnmetDependsOn()
    {
        var listId = await CreateListAsync();
        var dependency = await SeedAsync(listId, status: TaskStatus.Idle);
        await SeedAsync(listId, dependsOn: dependency.Id);

        // The dependency is only Idle (not Done), so both stay unclaimed: the dependent is
        // gated on depends_on_task_id, and the dependency itself was never queued.
        Assert.Null(await _picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None));
    }

    [Fact]
    public async Task ClaimNextAsync_Claims_TaskWhoseDependsOnIsDone()
    {
        var listId = await CreateListAsync();
        var dependency = await SeedAsync(listId, status: TaskStatus.Done);
        var dependent = await SeedAsync(listId, dependsOn: dependency.Id);

        var picked = await _picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None);

        Assert.NotNull(picked);
        Assert.Equal(dependent.Id, picked!.Id);
    }

    [Fact]
    public async Task ClaimNextAsync_Skips_TaskWhoseDependsOnFailed_ButKeepsClaimingOthers()
    {
        var listId = await CreateListAsync();
        var dependency = await SeedAsync(listId, status: TaskStatus.Failed);
        await SeedAsync(listId, dependsOn: dependency.Id);
        var independent = await SeedAsync(listId, createdAt: DateTime.UtcNow.AddMinutes(1));

        // A Failed dependency does not cascade-cancel or auto-unblock the dependent (unlike the
        // planning chain's BlockedByTaskId) -- it just stays Queued and skipped forever.
        var first = await _picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None);
        Assert.NotNull(first);
        Assert.Equal(independent.Id, first!.Id);

        var second = await _picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None);
        Assert.Null(second);
    }

    [Fact]
    public async Task ClaimNextAsync_Picks_TasksWithoutAgentTag()
    {
        // Queueing a task is itself the explicit "run me" signal — no tag gate.
        var listId = await CreateListAsync(listAgentTag: false);
        var task = await SeedAsync(listId);

        var picked = await _picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None);
        Assert.NotNull(picked);
        Assert.Equal(task.Id, picked!.Id);
    }

    [Fact]
    public async Task ClaimNextAsync_Skips_FutureScheduledFor()
    {
        var listId = await CreateListAsync(listAgentTag: true);
        await SeedAsync(listId, scheduledFor: DateTime.UtcNow.AddHours(1));

        var picked = await _picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None);
        Assert.Null(picked);
    }

    [Fact]
    public async Task ClaimNextAsync_Skips_NonQueuedStatuses()
    {
        var listId = await CreateListAsync(listAgentTag: true);
        await SeedAsync(listId, status: TaskStatus.Idle);
        await SeedAsync(listId, status: TaskStatus.Running);
        await SeedAsync(listId, status: TaskStatus.Done);
        await SeedAsync(listId, status: TaskStatus.Failed);
        await SeedAsync(listId, status: TaskStatus.Cancelled);

        var picked = await _picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None);
        Assert.Null(picked);
    }

    [Fact]
    public async Task ClaimNextAsync_Picks_ByUserSortOrder_ThenCreatedAt()
    {
        var listId = await CreateListAsync(listAgentTag: true);

        // Created in order first, second; reorder so second is sort-order 0.
        var first = await SeedAsync(listId, createdAt: DateTime.UtcNow.AddMinutes(-10));
        var second = await SeedAsync(listId, createdAt: DateTime.UtcNow);
        await _tasks.ReorderAsync(listId, new[] { second.Id, first.Id });

        var picked = await _picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None);
        Assert.NotNull(picked);
        Assert.Equal(second.Id, picked!.Id);
    }

    [Fact]
    public async Task ClaimNextAsync_FlipsToRunning_WithStartedAt()
    {
        var listId = await CreateListAsync(listAgentTag: true);
        var task = await SeedAsync(listId);

        var before = DateTime.UtcNow;
        var picked = await _picker.ClaimNextAsync(before, CancellationToken.None);
        Assert.NotNull(picked);

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

    [Fact]
    public async Task ClaimNextAsync_TwoParallelPickers_OnlyOneClaimsRow()
    {
        var listId = await CreateListAsync(listAgentTag: true);
        await SeedAsync(listId);

        // Two pickers, same DB factory, racing each other.
        var picker1 = new QueuePicker(_db.CreateFactory());
        var picker2 = new QueuePicker(_db.CreateFactory());

        var t1 = Task.Run(() => picker1.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None));
        var t2 = Task.Run(() => picker2.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None));

        var results = await Task.WhenAll(t1, t2);

        var nonNull = results.Where(r => r is not null).ToList();
        Assert.Single(nonNull);
    }
}
