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

namespace ClaudeDo.Worker.Tests.Queue;

/// OverrideSlotService.RunNow's own precheck — distinct from QueueService.EnsureNotInQueueSlot,
/// which only guards a task already tracked in its in-memory queue-slot dict. This covers a
/// task the DB already shows Running (e.g. the queue picker's atomic claim landed) regardless
/// of local slot bookkeeping. The real race-safety net is TaskRunner.RunAsync claiming Running
/// before creating any resources; this precheck just fails fast for the common case.
public sealed class OverrideSlotServiceTests : IDisposable
{
    private readonly DbFixture _db = new();
    private readonly string _tempDir;
    private readonly WorkerConfig _cfg;

    public OverrideSlotServiceTests()
    {
        _tempDir = Path.Combine(Path.GetTempPath(), $"cd_override_{Guid.NewGuid():N}");
        Directory.CreateDirectory(_tempDir);
        _cfg = new WorkerConfig { SandboxRoot = _tempDir, LogRoot = _tempDir };
    }

    public void Dispose() { _db.Dispose(); try { Directory.Delete(_tempDir, true); } catch { } }

    private OverrideSlotService BuildService(RunCancellationRegistry? runCancels = null)
    {
        var dbFactory = _db.CreateFactory();
        var state = TaskStateServiceBuilder.Build(dbFactory).State;
        var wt = new WorktreeManager(new ClaudeDo.Data.Git.GitService(), dbFactory, _cfg, NullLogger<WorktreeManager>.Instance);
        var runner = new TaskRunner(new FakeClaudeProcess(), dbFactory, new HubBroadcaster(new CapturingHubContext()), wt,
            new ClaudeArgsBuilder(), _cfg, NullLogger<TaskRunner>.Instance, state, new TaskRunTokenRegistry(),
            new AttachmentStore(), new FakeSessionSkillSeeder(), new FakeTranscriptUsageReader());
        runCancels ??= new RunCancellationRegistry(NullLogger<RunCancellationRegistry>.Instance);
        return new OverrideSlotService(dbFactory, runner, NullLogger<OverrideSlotService>.Instance, runCancels);
    }

    [Fact]
    public async Task RunNow_TaskAlreadyRunningInDb_ThrowsWithoutDispatching()
    {
        string listId = Guid.NewGuid().ToString(), taskId = Guid.NewGuid().ToString();
        using (var ctx = _db.CreateContext())
        {
            ctx.Lists.Add(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow });
            ctx.Tasks.Add(new TaskEntity
            {
                Id = taskId, ListId = listId, Title = "T", Status = TaskStatus.Running,
                StartedAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow,
            });
            await ctx.SaveChangesAsync();
        }

        var service = BuildService();

        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => service.RunNow(taskId));
        Assert.Contains("already running", ex.Message);
        Assert.Null(service.CurrentSlot);
    }

    [Fact]
    public async Task RunNow_TaskQueued_DoesNotThrow()
    {
        string listId = Guid.NewGuid().ToString(), taskId = Guid.NewGuid().ToString();
        using (var ctx = _db.CreateContext())
        {
            ctx.Lists.Add(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow });
            ctx.Tasks.Add(new TaskEntity
            {
                Id = taskId, ListId = listId, Title = "T", Status = TaskStatus.Queued,
                CreatedAt = DateTime.UtcNow,
            });
            await ctx.SaveChangesAsync();
        }

        var service = BuildService();

        // The dispatch itself races with FakeClaudeProcess's immediate completion in the
        // background, so this only asserts the precheck doesn't reject a claimable task.
        await service.RunNow(taskId);
    }

    // Regression for the RunCancellationRegistry.Register-return double-dispatch bug: the
    // queue picker's atomic Queued->Running claim can land between RunNow's DB precheck and
    // its registry registration. If the queue side already holds the registration, RunNow
    // must fail loudly instead of silently registering over it (or being ignored and then
    // unregistering the winner's CTS during its own cleanup).
    [Fact]
    public async Task RunNow_LosesRegistrationRaceToQueue_ThrowsAndLeavesWinnersCtsUntouched()
    {
        string listId = Guid.NewGuid().ToString(), taskId = Guid.NewGuid().ToString();
        using (var ctx = _db.CreateContext())
        {
            ctx.Lists.Add(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow });
            ctx.Tasks.Add(new TaskEntity
            {
                Id = taskId, ListId = listId, Title = "T", Status = TaskStatus.Queued,
                CreatedAt = DateTime.UtcNow,
            });
            await ctx.SaveChangesAsync();
        }

        var runCancels = new RunCancellationRegistry(NullLogger<RunCancellationRegistry>.Instance);
        var service = BuildService(runCancels);

        // Simulate the queue picker having already won the registration race for this task id.
        using var winnerCts = new CancellationTokenSource();
        Assert.True(runCancels.Register(taskId, winnerCts));

        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => service.RunNow(taskId));
        Assert.Contains("lost the double-dispatch race", ex.Message);
        Assert.Null(service.CurrentSlot);

        // The loser must not have cancelled or displaced the winner's registration.
        Assert.False(winnerCts.IsCancellationRequested);
        Assert.True(runCancels.TryCancel(taskId));
        Assert.True(winnerCts.IsCancellationRequested);
    }
}
