using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Worker.Planning;
using ClaudeDo.Worker.Tests.Infrastructure;
using Microsoft.EntityFrameworkCore;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Worker.Tests.Planning;

public sealed class PlanningChainCoordinatorTests : IDisposable
{
    private readonly DbFixture _db = new();
    private readonly TestDbContextFactory _factory;
    private readonly PlanningChainCoordinator _sut;
    private readonly string _listId;
    private int _numberSeed;

    public PlanningChainCoordinatorTests()
    {
        _factory = _db.CreateFactory();
        _sut = TaskStateServiceBuilder.Build(_factory).Chain;
        _listId = Guid.NewGuid().ToString();
        using var ctx = _factory.CreateDbContext();
        ctx.Lists.Add(new ListEntity
        {
            Id = _listId,
            Name = "Test",
            CreatedAt = DateTime.UtcNow,
            DefaultCommitType = "chore",
        });
        ctx.SaveChanges();
    }

    public void Dispose() => _db.Dispose();

    private async Task SeedPlanningFamilyAsync(string parentId, int childCount, TaskStatus childStatus = TaskStatus.Idle)
    {
        await using var ctx = _factory.CreateDbContext();
        ctx.Tasks.Add(new TaskEntity
        {
            Id = parentId,
            ListId = _listId,
            Title = "Parent",
            CreatedAt = DateTime.UtcNow,
            Status = TaskStatus.Idle,
            PlanningPhase = PlanningPhase.Finalized,
            Number = ++_numberSeed,
        });
        for (int i = 0; i < childCount; i++)
        {
            ctx.Tasks.Add(new TaskEntity
            {
                Id = $"{parentId}-c{i}",
                ListId = _listId,
                Title = $"Child {i}",
                CreatedAt = DateTime.UtcNow,
                Status = childStatus,
                ParentTaskId = parentId,
                SortOrder = i,
                Number = ++_numberSeed,
            });
        }
        await ctx.SaveChangesAsync();
    }

    private async Task<List<TaskEntity>> GetChildrenAsync(string parentId)
    {
        await using var ctx = _factory.CreateDbContext();
        return await ctx.Tasks
            .AsNoTracking()
            .Where(t => t.ParentTaskId == parentId)
            .OrderBy(t => t.SortOrder)
            .ToListAsync();
    }

    [Fact]
    public async Task SetupChain_FirstChildQueuedUnblocked_RestQueuedBlockedByPredecessor()
    {
        await SeedPlanningFamilyAsync("P", 3);

        var count = await _sut.SetupChainAsync("P", enqueue: true, default);

        Assert.Equal(3, count);
        var kids = await GetChildrenAsync("P");
        Assert.Equal(TaskStatus.Queued, kids[0].Status);
        Assert.Null(kids[0].BlockedByTaskId);
        Assert.Equal(TaskStatus.Queued, kids[1].Status);
        Assert.Equal(kids[0].Id, kids[1].BlockedByTaskId);
        Assert.Equal(TaskStatus.Queued, kids[2].Status);
        Assert.Equal(kids[1].Id, kids[2].BlockedByTaskId);
    }

    [Fact]
    public async Task SetupChain_AcceptsIdleChildren()
    {
        await SeedPlanningFamilyAsync("P", 2, childStatus: TaskStatus.Idle);

        var count = await _sut.SetupChainAsync("P", enqueue: true, default);

        Assert.Equal(2, count);
    }

    [Fact]
    public async Task OnChildDone_UnblocksTheSuccessor()
    {
        await SeedPlanningFamilyAsync("P", 3);
        await _sut.SetupChainAsync("P", enqueue: true, default);

        // Mark the head child Done before announcing.
        await using (var ctx = _factory.CreateDbContext())
        {
            var first = await ctx.Tasks.FirstAsync(t => t.Id == "P-c0");
            first.Status = TaskStatus.Done;
            await ctx.SaveChangesAsync();
        }

        var advanced = await _sut.OnChildFinishedAsync("P-c0", TaskStatus.Done, default);

        Assert.Equal("P-c1", advanced);
        var kids = await GetChildrenAsync("P");
        Assert.Equal(TaskStatus.Done, kids[0].Status);
        // c1 was Queued+BlockedBy=c0; UnblockAsync clears the block.
        Assert.Equal(TaskStatus.Queued, kids[1].Status);
        Assert.Null(kids[1].BlockedByTaskId);
        // c2 still blocked on c1.
        Assert.Equal(TaskStatus.Queued, kids[2].Status);
        Assert.Equal(kids[1].Id, kids[2].BlockedByTaskId);
    }

    [Fact]
    public async Task OnChildFailed_CancelsPendingSuccessors_ChainIsNotWedged()
    {
        await SeedPlanningFamilyAsync("P", 3);
        await _sut.SetupChainAsync("P", enqueue: true, default);

        await using (var ctx = _factory.CreateDbContext())
        {
            var first = await ctx.Tasks.FirstAsync(t => t.Id == "P-c0");
            first.Status = TaskStatus.Failed;
            await ctx.SaveChangesAsync();
        }

        var advanced = await _sut.OnChildFinishedAsync("P-c0", TaskStatus.Failed, default);

        // No "advancement" — the chain does not continue on failure.
        Assert.Null(advanced);
        var kids = await GetChildrenAsync("P");
        Assert.Equal(TaskStatus.Failed, kids[0].Status);
        // Successors must be Cancelled, not left stuck as Queued.
        Assert.Equal(TaskStatus.Cancelled, kids[1].Status);
        Assert.Equal(TaskStatus.Cancelled, kids[2].Status);
    }

    [Fact]
    public async Task OnChildFailed_MidChain_CancelsAllDownstreamSuccessors()
    {
        // Chain: c0 → c1 → c2 → c3. c1 fails mid-chain; c2 and c3 must be cancelled.
        await SeedPlanningFamilyAsync("P", 4);
        await _sut.SetupChainAsync("P", enqueue: true, default);

        // Mark c0 Done so c1 was unblocked; c1 ran and failed.
        await using (var ctx = _factory.CreateDbContext())
        {
            var c0 = await ctx.Tasks.FirstAsync(t => t.Id == "P-c0");
            c0.Status = TaskStatus.Done;
            c0.BlockedByTaskId = null;
            var c1 = await ctx.Tasks.FirstAsync(t => t.Id == "P-c1");
            c1.Status = TaskStatus.Failed;
            c1.BlockedByTaskId = null;
            await ctx.SaveChangesAsync();
        }

        // Announce that c1 finished as Failed — the coordinator must cascade to c2/c3.
        var advanced = await _sut.OnChildFinishedAsync("P-c1", TaskStatus.Failed, default);

        Assert.Null(advanced);
        var kids = await GetChildrenAsync("P");
        Assert.Equal(TaskStatus.Done, kids[0].Status);
        Assert.Equal(TaskStatus.Failed, kids[1].Status);
        Assert.Equal(TaskStatus.Cancelled, kids[2].Status);
        Assert.Equal(TaskStatus.Cancelled, kids[3].Status);
    }

    [Fact]
    public async Task OnChildFailed_MidChain_IdleSuccessor_DoesNotWedgeTail()
    {
        // Chain: c0 → c1 → c2 → c3. c1 is parked back to Idle (e.g. a manual reset)
        // while still wired into the chain (BlockedByTaskId = c0). c0 then fails:
        // CancelAsync(c1) is a no-op because Idle isn't a cancellable state, so the
        // cascade must keep walking past c1 instead of leaving c2/c3 wedged forever.
        await SeedPlanningFamilyAsync("P", 4);
        await _sut.SetupChainAsync("P", enqueue: true, default);

        await using (var ctx = _factory.CreateDbContext())
        {
            var c0 = await ctx.Tasks.FirstAsync(t => t.Id == "P-c0");
            c0.Status = TaskStatus.Failed;
            var c1 = await ctx.Tasks.FirstAsync(t => t.Id == "P-c1");
            c1.Status = TaskStatus.Idle;
            await ctx.SaveChangesAsync();
        }

        var advanced = await _sut.OnChildFinishedAsync("P-c0", TaskStatus.Failed, default);

        Assert.Null(advanced);
        var kids = await GetChildrenAsync("P");
        Assert.Equal(TaskStatus.Failed, kids[0].Status);
        // c1 was already parked Idle; the coordinator must not resurrect it.
        Assert.Equal(TaskStatus.Idle, kids[1].Status);
        // c2/c3 must not stay wedged as Queued+blocked forever.
        Assert.Equal(TaskStatus.Cancelled, kids[2].Status);
        Assert.Equal(TaskStatus.Cancelled, kids[3].Status);
    }

    [Fact]
    public async Task OnChildDone_LastChild_ReturnsNull()
    {
        await SeedPlanningFamilyAsync("P", 2);
        await _sut.SetupChainAsync("P", enqueue: true, default);

        await using (var ctx = _factory.CreateDbContext())
        {
            foreach (var t in ctx.Tasks.Where(t => t.ParentTaskId == "P"))
                t.Status = TaskStatus.Done;
            await ctx.SaveChangesAsync();
        }

        var advanced = await _sut.OnChildFinishedAsync("P-c1", TaskStatus.Done, default);

        Assert.Null(advanced);
    }

    [Fact]
    public async Task SetupChain_RejectsRunningChildren()
    {
        await SeedPlanningFamilyAsync("P", 2);
        await using (var ctx = _factory.CreateDbContext())
        {
            var first = await ctx.Tasks.FirstAsync(t => t.Id == "P-c0");
            first.Status = TaskStatus.Running;
            await ctx.SaveChangesAsync();
        }

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => _sut.SetupChainAsync("P", enqueue: true, default));
    }

    [Fact]
    public async Task SetupChain_AcceptsPartiallyQueuedChildren_IsIdempotent()
    {
        // Mirrors the BoxDataReader scenario: chain was partially set up earlier,
        // user re-runs "Queue subtasks sequentially" — should re-establish the chain
        // without throwing.
        await SeedPlanningFamilyAsync("P", 4);
        await using (var ctx = _factory.CreateDbContext())
        {
            // Pre-state: c0,c1 Idle; c2,c3 already Queued+blocked.
            var c2 = await ctx.Tasks.FirstAsync(t => t.Id == "P-c2");
            c2.Status = TaskStatus.Queued;
            c2.BlockedByTaskId = "P-c1";
            var c3 = await ctx.Tasks.FirstAsync(t => t.Id == "P-c3");
            c3.Status = TaskStatus.Queued;
            c3.BlockedByTaskId = "P-c2";
            await ctx.SaveChangesAsync();
        }

        var count = await _sut.SetupChainAsync("P", enqueue: true, default);

        Assert.Equal(4, count);
        var kids = await GetChildrenAsync("P");
        Assert.Equal(TaskStatus.Queued, kids[0].Status);
        Assert.Null(kids[0].BlockedByTaskId);
        Assert.Equal(TaskStatus.Queued, kids[1].Status);
        Assert.Equal("P-c0", kids[1].BlockedByTaskId);
        Assert.Equal(TaskStatus.Queued, kids[2].Status);
        Assert.Equal("P-c1", kids[2].BlockedByTaskId);
        Assert.Equal(TaskStatus.Queued, kids[3].Status);
        Assert.Equal("P-c2", kids[3].BlockedByTaskId);
    }

    [Fact]
    public async Task SetupChain_SkipsTerminalChildren_DoesNotResurrectThem()
    {
        await SeedPlanningFamilyAsync("P", 4);
        await using (var ctx = _factory.CreateDbContext())
        {
            var c0 = await ctx.Tasks.FirstAsync(t => t.Id == "P-c0");
            c0.Status = TaskStatus.Done;
            var c1 = await ctx.Tasks.FirstAsync(t => t.Id == "P-c1");
            c1.Status = TaskStatus.Failed;
            await ctx.SaveChangesAsync();
        }

        var count = await _sut.SetupChainAsync("P", enqueue: true, default);

        // Only the two non-terminal tail children get chained.
        Assert.Equal(2, count);
        var kids = await GetChildrenAsync("P");
        Assert.Equal(TaskStatus.Done, kids[0].Status);
        Assert.Equal(TaskStatus.Failed, kids[1].Status);
        // First non-terminal becomes the chain head (unblocked).
        Assert.Equal(TaskStatus.Queued, kids[2].Status);
        Assert.Null(kids[2].BlockedByTaskId);
        Assert.Equal(TaskStatus.Queued, kids[3].Status);
        Assert.Equal("P-c2", kids[3].BlockedByTaskId);
    }

    [Fact]
    public async Task QueuePlan_WhenParentFinalized_BuildsChain()
    {
        await SeedPlanningFamilyAsync("P", 2);

        var count = await _sut.QueuePlanAsync("P", default);

        Assert.Equal(2, count);
        var kids = await GetChildrenAsync("P");
        Assert.Equal(TaskStatus.Queued, kids[0].Status);
        Assert.Null(kids[0].BlockedByTaskId);
        Assert.Equal(TaskStatus.Queued, kids[1].Status);
        Assert.Equal("P-c0", kids[1].BlockedByTaskId);
    }

    [Fact]
    public async Task QueuePlan_WhenParentNotFinalized_Throws_AndLeavesChildrenIdle()
    {
        await SeedPlanningFamilyAsync("P", 2);
        await using (var ctx = _factory.CreateDbContext())
        {
            var parent = await ctx.Tasks.FirstAsync(t => t.Id == "P");
            parent.PlanningPhase = PlanningPhase.Active;
            await ctx.SaveChangesAsync();
        }

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => _sut.QueuePlanAsync("P", default));

        var kids = await GetChildrenAsync("P");
        Assert.All(kids, k => Assert.Equal(TaskStatus.Idle, k.Status));
    }

    [Fact]
    public async Task SetupChain_LinkOnly_LeavesChildrenIdle_ButEstablishesChain()
    {
        // Finalize path: children must stay Idle (nothing auto-queues) but still
        // get the blocked-by chain so a later "Queue plan" runs them in order.
        await SeedPlanningFamilyAsync("P", 3);

        var count = await _sut.SetupChainAsync("P", enqueue: false, default);

        Assert.Equal(3, count);
        var kids = await GetChildrenAsync("P");
        Assert.All(kids, k => Assert.Equal(TaskStatus.Idle, k.Status));
        Assert.Null(kids[0].BlockedByTaskId);
        Assert.Equal(kids[0].Id, kids[1].BlockedByTaskId);
        Assert.Equal(kids[1].Id, kids[2].BlockedByTaskId);
    }
}
