using System.Linq;
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.ViewModels.Modals;
using Microsoft.EntityFrameworkCore;
using Xunit;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Ui.Tests.ViewModels;

public class MergeHelperSelectionModalViewModelTests : IDisposable
{
    private readonly string _dbPath;

    public MergeHelperSelectionModalViewModelTests()
    {
        _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_mh_test_{Guid.NewGuid():N}.db");
        using var ctx = NewContext();
        ctx.Database.EnsureCreated();
    }

    public void Dispose()
    {
        try { File.Delete(_dbPath); } catch { }
        try { File.Delete(_dbPath + "-wal"); } catch { }
        try { File.Delete(_dbPath + "-shm"); } catch { }
    }

    private ClaudeDoDbContext NewContext()
    {
        var opts = new DbContextOptionsBuilder<ClaudeDoDbContext>()
            .UseSqlite($"Data Source={_dbPath}")
            .Options;
        return new ClaudeDoDbContext(opts);
    }


    private MergeHelperSelectionModalViewModel BuildVm()
        => new(new TestDbFactory(NewContext));

    private async Task SeedAllStatusesAsync()
    {
        await using var db = NewContext();
        db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
        db.Lists.Add(new ListEntity { Id = "L2", Name = "Home", CreatedAt = DateTime.UtcNow });
        var order = 0;
        void Add(string id, string listId, TaskStatus status) =>
            db.Tasks.Add(new TaskEntity
            {
                Number = TestTaskNumbers.Next(),
                Id = id, ListId = listId, Title = $"task {id}",
                Status = status, CreatedAt = DateTime.UtcNow, SortOrder = order++,
            });

        Add("t-idle", "L1", TaskStatus.Idle);
        Add("t-queued", "L1", TaskStatus.Queued);
        Add("t-running", "L1", TaskStatus.Running);
        Add("t-review", "L1", TaskStatus.WaitingForReview);
        Add("t-children", "L1", TaskStatus.WaitingForChildren);
        Add("t-failed", "L1", TaskStatus.Failed);
        Add("t-done", "L1", TaskStatus.Done);
        Add("t-cancelled", "L1", TaskStatus.Cancelled);
        Add("t-other-list", "L2", TaskStatus.Idle);
        await db.SaveChangesAsync();
    }

    [Fact]
    public async Task Load_ExcludesTerminalStatuses_AndTicksActionableByDefault()
    {
        await SeedAllStatusesAsync();
        var vm = BuildVm();
        vm.Configure("L1", "Work");
        await vm.LoadAsync();

        Assert.DoesNotContain(vm.Tasks, t => t.Id is "t-done" or "t-cancelled");
        Assert.DoesNotContain(vm.Tasks, t => t.Id == "t-other-list");
        Assert.Equal(6, vm.Tasks.Count);

        Assert.True(vm.Tasks.Single(t => t.Id == "t-idle").IsSelected);
        Assert.True(vm.Tasks.Single(t => t.Id == "t-queued").IsSelected);
        Assert.True(vm.Tasks.Single(t => t.Id == "t-review").IsSelected);
        Assert.True(vm.Tasks.Single(t => t.Id == "t-failed").IsSelected);
        Assert.False(vm.Tasks.Single(t => t.Id == "t-running").IsSelected);
        Assert.False(vm.Tasks.Single(t => t.Id == "t-children").IsSelected);
    }

    [Fact]
    public async Task ReconcileTick_KeepsBothTicksAndUnticks_AndDefaultsOnlyNewRows()
    {
        await SeedAllStatusesAsync();
        var vm = BuildVm();
        vm.Configure("L1", "Work");
        await vm.LoadAsync();

        // The user's own choices, both directions, against the defaults.
        vm.Tasks.Single(t => t.Id == "t-idle").IsSelected = false;      // default true -> off
        vm.Tasks.Single(t => t.Id == "t-running").IsSelected = true;    // default false -> on

        await using (var db = NewContext())
        {
            db.Tasks.Add(new TaskEntity
            {
                Number = TestTaskNumbers.Next(),
                Id = "t-new", ListId = "L1", Title = "arrived later",
                Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow, SortOrder = 99,
            });
            await db.SaveChangesAsync();
        }

        await vm.ReconcileTickAsync();

        Assert.False(vm.Tasks.Single(t => t.Id == "t-idle").IsSelected);
        Assert.True(vm.Tasks.Single(t => t.Id == "t-running").IsSelected);
        Assert.True(vm.Tasks.Single(t => t.Id == "t-queued").IsSelected);   // untouched, keeps default
        Assert.True(vm.Tasks.Single(t => t.Id == "t-new").IsSelected);      // new row gets the default
    }

    [Fact]
    public async Task ReconcileTick_DoesNotCascadeParentStateOverAnUntickedChild()
    {
        await using (var db = NewContext())
        {
            db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
            db.Tasks.Add(new TaskEntity
            {
                Number = TestTaskNumbers.Next(),
                Id = "parent", ListId = "L1", Title = "parent",
                Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow, SortOrder = 0,
            });
            db.Tasks.Add(new TaskEntity
            {
                Number = TestTaskNumbers.Next(),
                Id = "child1", ListId = "L1", Title = "child1", ParentTaskId = "parent",
                Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow, SortOrder = 1,
            });
            db.Tasks.Add(new TaskEntity
            {
                Number = TestTaskNumbers.Next(),
                Id = "child2", ListId = "L1", Title = "child2", ParentTaskId = "parent",
                Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow, SortOrder = 2,
            });
            await db.SaveChangesAsync();
        }

        var vm = BuildVm();
        vm.Configure("L1", "Work");
        await vm.LoadAsync();

        vm.Tasks.Single(t => t.Id == "child1").IsSelected = false;   // parent stays ticked

        await vm.ReconcileTickAsync();

        Assert.True(vm.Tasks.Single(t => t.Id == "parent").IsSelected);
        Assert.False(vm.Tasks.Single(t => t.Id == "child1").IsSelected);
        Assert.True(vm.Tasks.Single(t => t.Id == "child2").IsSelected);
    }

    [Fact]
    public async Task Load_PerListScope_FiltersToThatList()
    {
        await SeedAllStatusesAsync();
        var vm = BuildVm();
        vm.Configure("L2", "Home");
        await vm.LoadAsync();

        Assert.Single(vm.Tasks);
        Assert.Equal("t-other-list", vm.Tasks[0].Id);
        Assert.Contains("Home", vm.ScopeLabel);
    }

    [Fact]
    public async Task CanConfirm_FollowsRowSelection()
    {
        await SeedAllStatusesAsync();
        var vm = BuildVm();
        vm.Configure("L1", "Work");
        await vm.LoadAsync();

        Assert.True(vm.CanConfirm);

        vm.SelectNoneCommand.Execute(null);
        Assert.False(vm.CanConfirm);
        Assert.All(vm.Tasks, t => Assert.False(t.IsSelected));

        vm.Tasks[0].IsSelected = true;      // single row re-enables via PropertyChanged hook
        Assert.True(vm.CanConfirm);

        vm.SelectAllCommand.Execute(null);
        Assert.All(vm.Tasks, t => Assert.True(t.IsSelected));
    }

    [Fact]
    public async Task Confirm_ReturnsSelectedIds_InRowOrder()
    {
        await SeedAllStatusesAsync();
        var vm = BuildVm();
        vm.Configure("L1", "Work");
        await vm.LoadAsync();

        vm.SelectNoneCommand.Execute(null);
        vm.Tasks.Single(t => t.Id == "t-review").IsSelected = true;
        vm.Tasks.Single(t => t.Id == "t-idle").IsSelected = true;

        var closed = false;
        vm.CloseAction = () => closed = true;
        vm.ConfirmCommand.Execute(null);

        var result = await vm.Result.Task;
        Assert.NotNull(result);
        // Row order (SortOrder): t-idle was seeded before t-review.
        Assert.Equal(new[] { "t-idle", "t-review" }, result);
        Assert.True(closed);
    }

    [Fact]
    public async Task Cancel_ReturnsNull()
    {
        await SeedAllStatusesAsync();
        var vm = BuildVm();
        vm.Configure("L1", "Work");
        await vm.LoadAsync();

        var closed = false;
        vm.CloseAction = () => closed = true;
        vm.CancelCommand.Execute(null);

        Assert.Null(await vm.Result.Task);
        Assert.True(closed);
    }

    [Fact]
    public async Task Load_NestsChildUnderParent_InOrder()
    {
        await using (var db = NewContext())
        {
            db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
            db.Tasks.Add(new TaskEntity { Number = TestTaskNumbers.Next(), Id = "parent", ListId = "L1", Title = "parent", Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow, SortOrder = 0 });
            db.Tasks.Add(new TaskEntity { Number = TestTaskNumbers.Next(), Id = "sibling", ListId = "L1", Title = "sibling", Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow, SortOrder = 1 });
            db.Tasks.Add(new TaskEntity { Number = TestTaskNumbers.Next(), Id = "child", ListId = "L1", Title = "child", Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow, SortOrder = 2, ParentTaskId = "parent" });
            await db.SaveChangesAsync();
        }

        var vm = BuildVm();
        vm.Configure("L1", "Work");
        await vm.LoadAsync();

        // Child is nested directly after its parent, ahead of the later sibling.
        Assert.Equal(new[] { "parent", "child", "sibling" }, vm.Tasks.Select(t => t.Id));
        Assert.True(vm.Tasks.Single(t => t.Id == "child").IsChild);
        Assert.False(vm.Tasks.Single(t => t.Id == "child").IsOrphanChild);
        Assert.False(vm.Tasks.Single(t => t.Id == "parent").IsChild);
    }

    [Fact]
    public async Task Load_ChildOfFilteredOutParent_RendersTopLevelAsOrphan()
    {
        await using (var db = NewContext())
        {
            db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
            db.Tasks.Add(new TaskEntity { Number = TestTaskNumbers.Next(), Id = "parent-done", ListId = "L1", Title = "parent", Status = TaskStatus.Done, CreatedAt = DateTime.UtcNow, SortOrder = 0 });
            db.Tasks.Add(new TaskEntity { Number = TestTaskNumbers.Next(), Id = "child", ListId = "L1", Title = "child", Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow, SortOrder = 1, ParentTaskId = "parent-done" });
            await db.SaveChangesAsync();
        }

        var vm = BuildVm();
        vm.Configure("L1", "Work");
        await vm.LoadAsync();

        // The parent is Done and filtered out; the child must not disappear, and must be
        // flagged as an orphan instead of rendering as a (broken) child.
        Assert.Single(vm.Tasks);
        var row = vm.Tasks[0];
        Assert.Equal("child", row.Id);
        Assert.False(row.IsChild);
        Assert.True(row.IsOrphanChild);
    }

    [Fact]
    public async Task Toggle_Parent_CascadesSelectionToChildren()
    {
        await using (var db = NewContext())
        {
            db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
            db.Tasks.Add(new TaskEntity { Number = TestTaskNumbers.Next(), Id = "parent", ListId = "L1", Title = "parent", Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow, SortOrder = 0 });
            db.Tasks.Add(new TaskEntity { Number = TestTaskNumbers.Next(), Id = "child1", ListId = "L1", Title = "child1", Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow, SortOrder = 1, ParentTaskId = "parent" });
            db.Tasks.Add(new TaskEntity { Number = TestTaskNumbers.Next(), Id = "child2", ListId = "L1", Title = "child2", Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow, SortOrder = 2, ParentTaskId = "parent" });
            await db.SaveChangesAsync();
        }

        var vm = BuildVm();
        vm.Configure("L1", "Work");
        await vm.LoadAsync();
        vm.SelectNoneCommand.Execute(null);
        Assert.All(vm.Tasks, t => Assert.False(t.IsSelected));

        vm.Tasks.Single(t => t.Id == "parent").IsSelected = true;
        Assert.True(vm.Tasks.Single(t => t.Id == "child1").IsSelected);
        Assert.True(vm.Tasks.Single(t => t.Id == "child2").IsSelected);

        // A child can be individually deselected afterwards without touching the parent.
        vm.Tasks.Single(t => t.Id == "child1").IsSelected = false;
        Assert.True(vm.Tasks.Single(t => t.Id == "parent").IsSelected);
        Assert.False(vm.Tasks.Single(t => t.Id == "child1").IsSelected);
        Assert.True(vm.Tasks.Single(t => t.Id == "child2").IsSelected);

        vm.Tasks.Single(t => t.Id == "parent").IsSelected = false;
        Assert.False(vm.Tasks.Single(t => t.Id == "child2").IsSelected);
    }

    [Fact]
    public async Task Confirm_ReturnsParentsBeforeChildren()
    {
        await using (var db = NewContext())
        {
            db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
            db.Tasks.Add(new TaskEntity { Number = TestTaskNumbers.Next(), Id = "parent", ListId = "L1", Title = "parent", Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow, SortOrder = 0 });
            db.Tasks.Add(new TaskEntity { Number = TestTaskNumbers.Next(), Id = "child", ListId = "L1", Title = "child", Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow, SortOrder = 1, ParentTaskId = "parent" });
            await db.SaveChangesAsync();
        }

        var vm = BuildVm();
        vm.Configure("L1", "Work");
        await vm.LoadAsync();
        vm.ConfirmCommand.Execute(null);

        var result = await vm.Result.Task;
        Assert.Equal(new[] { "parent", "child" }, result);
    }

    [Fact]
    public async Task Load_NoCandidates_HasTasksFalse_CannotConfirm()
    {
        await using (var db = NewContext())
        {
            db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
            db.Tasks.Add(new TaskEntity { Number = TestTaskNumbers.Next(), Id = "t-done", ListId = "L1", Title = "done", Status = TaskStatus.Done, CreatedAt = DateTime.UtcNow });
            await db.SaveChangesAsync();
        }

        var vm = BuildVm();
        vm.Configure("L1", "Work");
        await vm.LoadAsync();

        Assert.False(vm.HasTasks);
        Assert.False(vm.CanConfirm);
    }

    // The tick reloads by rebuilding every row, so it remembers and replays the user's ticks.
    // A second tick entering while the first is between its reload and its restore would snapshot
    // rows that are back at IsTickedByDefault and write that default back — silently re-ticking
    // whatever the user had deliberately unticked. Only one tick may be in flight.
    [Fact]
    public async Task ReconcileTick_ignores_a_second_tick_while_one_is_still_in_flight()
    {
        await SeedAllStatusesAsync();

        var factory = new CountingDbFactory(NewContext);
        var vm = new MergeHelperSelectionModalViewModel(factory);
        vm.Configure("L1", "Work");
        await vm.LoadAsync();

        var unticked = vm.Tasks.Single(t => t.Id == "t-idle");
        unticked.IsSelected = false;

        var reached = new TaskCompletionSource();
        var proceed = new TaskCompletionSource();
        vm.ReconcileTickTestBarrier = async () =>
        {
            reached.TrySetResult();
            await proceed.Task;
        };

        var first = vm.ReconcileTickAsync();
        await reached.Task;

        var loadsBefore = factory.CreateCalls;
        await vm.ReconcileTickAsync();
        Assert.Equal(loadsBefore, factory.CreateCalls); // the overlapping tick did not reload

        proceed.TrySetResult();
        await first;

        Assert.False(vm.Tasks.Single(t => t.Id == "t-idle").IsSelected);
    }

    // ---- Review mode (ConfigureForReview): all lists, WaitingForReview only ----

    private async Task SeedReviewAcrossListsAsync()
    {
        await using var db = NewContext();
        db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", WorkingDir = "C:\\repo1", CreatedAt = DateTime.UtcNow });
        db.Lists.Add(new ListEntity { Id = "L2", Name = "Home", WorkingDir = "C:\\repo2", CreatedAt = DateTime.UtcNow });
        db.Lists.Add(new ListEntity { Id = "L3", Name = "NoRepo", CreatedAt = DateTime.UtcNow });
        var order = 0;
        void Add(string id, string listId, TaskStatus status, bool isManual = false) =>
            db.Tasks.Add(new TaskEntity
            {
                Number = TestTaskNumbers.Next(),
                Id = id, ListId = listId, Title = $"task {id}", IsManual = isManual,
                Status = status, CreatedAt = DateTime.UtcNow, SortOrder = order++,
            });

        Add("r1", "L1", TaskStatus.WaitingForReview);
        Add("r2", "L1", TaskStatus.WaitingForReview);
        Add("i1", "L1", TaskStatus.Idle);                              // wrong status
        Add("m1", "L1", TaskStatus.WaitingForReview, isManual: true);  // handler/reminder task
        Add("r3", "L2", TaskStatus.WaitingForReview);
        Add("r4", "L3", TaskStatus.WaitingForReview);                  // list without working dir
        await db.SaveChangesAsync();
    }

    [Fact]
    public async Task ReviewMode_LoadsOnlyReviewTasksOfRepoLists_GroupedWithHeaders_AllTicked()
    {
        await SeedReviewAcrossListsAsync();
        var vm = BuildVm();
        vm.ConfigureForReview();
        await vm.LoadAsync();

        // Ordered by list name: Home (L2) before Work (L1); L3 (no repo), i1, m1 excluded.
        Assert.Equal(new[] { "header:L2", "r3", "header:L1", "r1", "r2" }, vm.Tasks.Select(t => t.Id));
        Assert.True(vm.Tasks[0].IsListHeader);
        Assert.Equal("Home", vm.Tasks[0].Title);
        Assert.True(vm.Tasks[2].IsListHeader);
        Assert.Equal("Work", vm.Tasks[2].Title);
        Assert.All(vm.Tasks.Where(t => !t.IsListHeader), t => Assert.True(t.IsSelected));
        Assert.All(vm.Tasks.Where(t => t.IsListHeader), t => Assert.False(t.IsSelected));
        Assert.True(vm.CanConfirm);
    }

    [Fact]
    public async Task ReviewMode_SelectAll_DoesNotTickHeaders()
    {
        await SeedReviewAcrossListsAsync();
        var vm = BuildVm();
        vm.ConfigureForReview();
        await vm.LoadAsync();

        vm.SelectNoneCommand.Execute(null);
        vm.SelectAllCommand.Execute(null);

        Assert.All(vm.Tasks.Where(t => t.IsListHeader), t => Assert.False(t.IsSelected));
        Assert.All(vm.Tasks.Where(t => !t.IsListHeader), t => Assert.True(t.IsSelected));
    }

    [Fact]
    public async Task ReviewMode_ConfirmAndGroupSelectionByList_GroupsInRowOrder()
    {
        await SeedReviewAcrossListsAsync();
        var vm = BuildVm();
        vm.ConfigureForReview();
        await vm.LoadAsync();

        vm.ConfirmCommand.Execute(null);
        var ids = await vm.Result.Task;
        Assert.NotNull(ids);
        Assert.Equal(new[] { "r3", "r1", "r2" }, ids);

        var groups = vm.GroupSelectionByList(ids!);
        Assert.Equal(2, groups.Count);
        Assert.Equal("L2", groups[0].ListId);
        Assert.Equal(new[] { "r3" }, groups[0].TaskIds);
        Assert.Equal("L1", groups[1].ListId);
        Assert.Equal(new[] { "r1", "r2" }, groups[1].TaskIds);
    }

    [Fact]
    public async Task ReviewMode_ReconcileTick_KeepsUnticks_AndPicksUpNewReviewTasks()
    {
        await SeedReviewAcrossListsAsync();
        var vm = BuildVm();
        vm.ConfigureForReview();
        await vm.LoadAsync();

        vm.Tasks.Single(t => t.Id == "r1").IsSelected = false;

        await using (var db = NewContext())
        {
            db.Tasks.Add(new TaskEntity
            {
                Number = TestTaskNumbers.Next(),
                Id = "r5", ListId = "L1", Title = "arrived later",
                Status = TaskStatus.WaitingForReview, CreatedAt = DateTime.UtcNow, SortOrder = 99,
            });
            await db.SaveChangesAsync();
        }

        await vm.ReconcileTickAsync();

        Assert.False(vm.Tasks.Single(t => t.Id == "r1").IsSelected);
        Assert.True(vm.Tasks.Single(t => t.Id == "r5").IsSelected);
        Assert.Contains(vm.Tasks, t => t.Id == "header:L1" && t.IsListHeader);
    }

    private sealed class CountingDbFactory : IDbContextFactory<ClaudeDoDbContext>
    {
        private readonly Func<ClaudeDoDbContext> _create;
        public int CreateCalls { get; private set; }
        public CountingDbFactory(Func<ClaudeDoDbContext> create) => _create = create;
        public ClaudeDoDbContext CreateDbContext()
        {
            CreateCalls++;
            return _create();
        }
    }
}
