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

namespace ClaudeDo.Worker.Tests.Repositories;

/// <summary>
/// Covers the invariant that no task may have <c>ParentTaskId</c> pointing to a
/// parent without <c>PlanningPhase.Active|Finalized</c>. Tests the three guard
/// rails: <c>CreateChildAsync</c> validation, <c>DiscardPlanningAsync</c>
/// gating with the optional dequeue path, and the startup repair sweep.
/// </summary>
public sealed class TaskRepositoryOrphanGuardTests : IDisposable
{
    private readonly DbFixture _db = new();
    private readonly ClaudeDoDbContext _ctx;
    private readonly TaskRepository _tasks;
    private readonly ListRepository _lists;

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

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

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

    private TaskEntity MakeTask(string listId, TaskStatus status = TaskStatus.Idle, PlanningPhase phase = PlanningPhase.None) => new()
    {
        Id = Guid.NewGuid().ToString(),
        ListId = listId,
        Title = "T",
        Status = status,
        PlanningPhase = phase,
        CreatedAt = DateTime.UtcNow,
        CommitType = "chore",
    };

    private async Task<TaskEntity> SeedPlanningParentAsync(string listId)
    {
        var parent = MakeTask(listId, status: TaskStatus.Idle, phase: PlanningPhase.Active);
        await _tasks.AddAsync(parent);
        return parent;
    }

    // --- CreateChildAsync validation ---

    [Fact]
    public async Task CreateChildAsync_Succeeds_On_NonPlanning_Parent_For_Improvement_Children()
    {
        // The planning-phase guard was removed: improvement children attach to a
        // non-planning parent, with CreatedBy stamped to the parent id by the caller.
        var listId = await CreateListAsync();
        var parent = MakeTask(listId, phase: PlanningPhase.None);
        await _tasks.AddAsync(parent);

        var child = await _tasks.CreateChildAsync(parent.Id, "child", null, null, createdBy: parent.Id);
        Assert.Equal(parent.Id, child.ParentTaskId);
        Assert.Equal(parent.Id, child.CreatedBy);
        Assert.Equal(TaskStatus.Idle, child.Status);
    }

    [Fact]
    public async Task CreateChildAsync_Succeeds_When_Parent_Is_Active()
    {
        var listId = await CreateListAsync();
        var parent = await SeedPlanningParentAsync(listId);

        var child = await _tasks.CreateChildAsync(parent.Id, "child", null, null);
        Assert.Equal(parent.Id, child.ParentTaskId);
        Assert.Equal(TaskStatus.Idle, child.Status);
    }

    // --- DiscardPlanningAsync gating ---

    [Fact]
    public async Task DiscardPlanning_NotInPlanning_When_Parent_Phase_Is_None()
    {
        var listId = await CreateListAsync();
        var stray = MakeTask(listId, phase: PlanningPhase.None);
        await _tasks.AddAsync(stray);

        var outcome = await _tasks.DiscardPlanningAsync(stray.Id, dequeueQueuedChildren: false);
        Assert.Equal(DiscardPlanningResult.NotInPlanning, outcome.Result);
    }

    [Fact]
    public async Task DiscardPlanning_Succeeds_When_All_Children_Are_Idle()
    {
        var listId = await CreateListAsync();
        var parent = await SeedPlanningParentAsync(listId);
        await _tasks.CreateChildAsync(parent.Id, "a", null, null);
        await _tasks.CreateChildAsync(parent.Id, "b", null, null);

        var outcome = await _tasks.DiscardPlanningAsync(parent.Id, dequeueQueuedChildren: false);

        Assert.Equal(DiscardPlanningResult.Discarded, outcome.Result);
        Assert.Equal(0, _ctx.Tasks.AsNoTracking().Count(t => t.ParentTaskId == parent.Id));
        var reloaded = _ctx.Tasks.AsNoTracking().Single(t => t.Id == parent.Id);
        Assert.Equal(PlanningPhase.None, reloaded.PlanningPhase);
    }

    [Fact]
    public async Task DiscardPlanning_Blocks_On_Queued_Children_Without_Optin()
    {
        var listId = await CreateListAsync();
        var parent = await SeedPlanningParentAsync(listId);
        var child = await _tasks.CreateChildAsync(parent.Id, "c", null, null);
        await SetChildStatusAsync(child.Id, TaskStatus.Queued);

        var outcome = await _tasks.DiscardPlanningAsync(parent.Id, dequeueQueuedChildren: false);

        Assert.Equal(DiscardPlanningResult.BlockedByQueuedChildren, outcome.Result);
        Assert.Equal(1, outcome.QueuedChildrenCount);
        // Parent and child are untouched.
        Assert.Equal(PlanningPhase.Active, _ctx.Tasks.AsNoTracking().Single(t => t.Id == parent.Id).PlanningPhase);
        Assert.Equal(TaskStatus.Queued, _ctx.Tasks.AsNoTracking().Single(t => t.Id == child.Id).Status);
    }

    [Fact]
    public async Task DiscardPlanning_With_Dequeue_Succeeds_And_Drops_Idle_Children()
    {
        var listId = await CreateListAsync();
        var parent = await SeedPlanningParentAsync(listId);
        var child = await _tasks.CreateChildAsync(parent.Id, "c", null, null);
        await SetChildStatusAsync(child.Id, TaskStatus.Queued);

        var outcome = await _tasks.DiscardPlanningAsync(parent.Id, dequeueQueuedChildren: true);

        Assert.Equal(DiscardPlanningResult.Discarded, outcome.Result);
        // Child was dequeued to Idle and then deleted as part of the discard.
        Assert.False(_ctx.Tasks.AsNoTracking().Any(t => t.Id == child.Id));
    }

    [Fact]
    public async Task DiscardPlanning_Blocks_On_Running_Children_Even_With_Dequeue_Optin()
    {
        var listId = await CreateListAsync();
        var parent = await SeedPlanningParentAsync(listId);
        var child = await _tasks.CreateChildAsync(parent.Id, "c", null, null);
        await SetChildStatusAsync(child.Id, TaskStatus.Running);

        var outcome = await _tasks.DiscardPlanningAsync(parent.Id, dequeueQueuedChildren: true);

        Assert.Equal(DiscardPlanningResult.BlockedByRunningChildren, outcome.Result);
        Assert.Equal(1, outcome.RunningChildrenCount);
        Assert.Equal(PlanningPhase.Active, _ctx.Tasks.AsNoTracking().Single(t => t.Id == parent.Id).PlanningPhase);
    }

    [Fact]
    public async Task DiscardPlanning_Leaves_Terminal_Children_Attached()
    {
        var listId = await CreateListAsync();
        var parent = await SeedPlanningParentAsync(listId);
        var done   = await _tasks.CreateChildAsync(parent.Id, "done",   null, null);
        var failed = await _tasks.CreateChildAsync(parent.Id, "failed", null, null);
        await SetChildStatusAsync(done.Id,   TaskStatus.Done);
        await SetChildStatusAsync(failed.Id, TaskStatus.Failed);

        var outcome = await _tasks.DiscardPlanningAsync(parent.Id, dequeueQueuedChildren: false);

        Assert.Equal(DiscardPlanningResult.Discarded, outcome.Result);
        Assert.Equal(parent.Id, _ctx.Tasks.AsNoTracking().Single(t => t.Id == done.Id).ParentTaskId);
        Assert.Equal(parent.Id, _ctx.Tasks.AsNoTracking().Single(t => t.Id == failed.Id).ParentTaskId);
    }

    // --- Dequeue sweep ---

    [Fact]
    public async Task Dequeue_Dequeues_Queued_Child_When_Parent_Is_Not_Planning()
    {
        var listId = await CreateListAsync();
        // Parent is plain (not planning), child attached -> stuck queued.
        var parent = MakeTask(listId, phase: PlanningPhase.None);
        await _tasks.AddAsync(parent);
        var predecessor = MakeTask(listId, status: TaskStatus.Idle);
        await _tasks.AddAsync(predecessor);
        var child = MakeTask(listId, status: TaskStatus.Queued);
        child.ParentTaskId = parent.Id;
        child.BlockedByTaskId = predecessor.Id;
        await _tasks.AddAsync(child);

        var dequeued = await _tasks.DequeueOrphanedChildrenAsync();

        Assert.Equal(1, dequeued);
        var reloaded = _ctx.Tasks.AsNoTracking().Single(t => t.Id == child.Id);
        Assert.Equal(TaskStatus.Idle, reloaded.Status);
        Assert.Null(reloaded.BlockedByTaskId);
        Assert.Equal(parent.Id, reloaded.ParentTaskId); // lineage stays
    }

    [Fact]
    public async Task Dequeue_Leaves_Queued_Improvement_Children_Alone()
    {
        // Improvement children (CreatedBy == ParentTaskId) queue under a non-planning
        // parent on purpose — the orphan sweep must NOT reset them.
        var listId = await CreateListAsync();
        var parent = MakeTask(listId, phase: PlanningPhase.None);
        await _tasks.AddAsync(parent);
        var child = MakeTask(listId, status: TaskStatus.Queued);
        child.ParentTaskId = parent.Id;
        child.CreatedBy = parent.Id;
        await _tasks.AddAsync(child);

        var dequeued = await _tasks.DequeueOrphanedChildrenAsync();

        Assert.Equal(0, dequeued);
        Assert.Equal(TaskStatus.Queued, _ctx.Tasks.AsNoTracking().Single(t => t.Id == child.Id).Status);
    }

    [Fact]
    public async Task Dequeue_Leaves_Idle_Children_Of_NonPlanning_Parent_Alone()
    {
        var listId = await CreateListAsync();
        var parent = MakeTask(listId, phase: PlanningPhase.None);
        await _tasks.AddAsync(parent);
        var child = MakeTask(listId, status: TaskStatus.Idle);
        child.ParentTaskId = parent.Id;
        await _tasks.AddAsync(child);

        var dequeued = await _tasks.DequeueOrphanedChildrenAsync();
        Assert.Equal(0, dequeued);
    }

    [Fact]
    public async Task Dequeue_Leaves_Valid_Children_Untouched()
    {
        var listId = await CreateListAsync();
        var parent = await SeedPlanningParentAsync(listId);
        var child = await _tasks.CreateChildAsync(parent.Id, "c", null, null);

        var dequeued = await _tasks.DequeueOrphanedChildrenAsync();
        Assert.Equal(0, dequeued);
        Assert.Equal(parent.Id, _ctx.Tasks.AsNoTracking().Single(t => t.Id == child.Id).ParentTaskId);
    }

    // --- Planning lineage restoration ---

    [Fact]
    public async Task RestoreLineage_ReAttaches_Unambiguous_Chain_And_Dequeues_Queued_Members()
    {
        var listId = await CreateListAsync();
        // Parent that once had a planning session but lost the link.
        var parent = MakeTask(listId, phase: PlanningPhase.None);
        await _tasks.AddAsync(parent);

        // Chain: head (idle, no blocked_by, someone is blocked by it) + 2 queued successors.
        var head = MakeTask(listId, status: TaskStatus.Idle);
        head.BlockedByTaskId = null;
        await _tasks.AddAsync(head);

        var mid = MakeTask(listId, status: TaskStatus.Queued);
        mid.BlockedByTaskId = head.Id;
        await _tasks.AddAsync(mid);

        var tail = MakeTask(listId, status: TaskStatus.Queued);
        tail.BlockedByTaskId = mid.Id;
        await _tasks.AddAsync(tail);

        var restored = await _tasks.RestorePlanningLineageAsync(parent.Id);

        Assert.Equal(3, restored);
        Assert.Equal(PlanningPhase.Finalized, _ctx.Tasks.AsNoTracking().Single(t => t.Id == parent.Id).PlanningPhase);
        Assert.All(new[] { head.Id, mid.Id, tail.Id }, id =>
            Assert.Equal(parent.Id, _ctx.Tasks.AsNoTracking().Single(t => t.Id == id).ParentTaskId));
        Assert.Equal(TaskStatus.Idle, _ctx.Tasks.AsNoTracking().Single(t => t.Id == mid.Id).Status);
        Assert.Equal(TaskStatus.Idle, _ctx.Tasks.AsNoTracking().Single(t => t.Id == tail.Id).Status);
        // blocked_by intact for chain order.
        Assert.Equal(head.Id, _ctx.Tasks.AsNoTracking().Single(t => t.Id == mid.Id).BlockedByTaskId);
        Assert.Equal(mid.Id,  _ctx.Tasks.AsNoTracking().Single(t => t.Id == tail.Id).BlockedByTaskId);
    }

    [Fact]
    public async Task RestoreLineage_Skips_When_Multiple_Chains_Exist()
    {
        var listId = await CreateListAsync();
        var parent = MakeTask(listId, phase: PlanningPhase.None);
        await _tasks.AddAsync(parent);

        // Two independent chains in the same list -> ambiguous.
        var headA = MakeTask(listId, status: TaskStatus.Idle); await _tasks.AddAsync(headA);
        var midA  = MakeTask(listId, status: TaskStatus.Queued); midA.BlockedByTaskId = headA.Id; await _tasks.AddAsync(midA);
        var headB = MakeTask(listId, status: TaskStatus.Idle); await _tasks.AddAsync(headB);
        var midB  = MakeTask(listId, status: TaskStatus.Queued); midB.BlockedByTaskId = headB.Id; await _tasks.AddAsync(midB);

        var restored = await _tasks.RestorePlanningLineageAsync(parent.Id);
        Assert.Equal(0, restored);
        Assert.Equal(PlanningPhase.None, _ctx.Tasks.AsNoTracking().Single(t => t.Id == parent.Id).PlanningPhase);
    }

    [Fact]
    public async Task RestoreLineage_Skips_When_Parent_Already_Has_Planning_Phase()
    {
        var listId = await CreateListAsync();
        var parent = await SeedPlanningParentAsync(listId);

        var restored = await _tasks.RestorePlanningLineageAsync(parent.Id);
        Assert.Equal(0, restored);
    }

    private async Task SetChildStatusAsync(string id, TaskStatus status)
    {
        var t = await _ctx.Tasks.FindAsync(id) ?? throw new InvalidOperationException();
        t.Status = status;
        await _ctx.SaveChangesAsync();
        _ctx.Entry(t).State = Microsoft.EntityFrameworkCore.EntityState.Detached;
    }
}
