using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Islands;
using Microsoft.EntityFrameworkCore;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Ui.Tests.ViewModels;

// Polish-Audit 2026-08-20 #1: typing in task A, then binding to task B inside the 400 ms debounce
// window used to write A's text into B's row, because SaveDescriptionAsync/SaveTitleAsync read
// `Task`/`EditableDescription` after the delay instead of capturing them at the call site.
public class DetailsIslandDescriptionSaveRaceTests : IDisposable
{
    private readonly string _dbPath;

    public DetailsIslandDescriptionSaveRaceTests()
    {
        _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_desc_race_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 sealed class FakeWorker : StubWorkerClient
    {
        public override bool IsConnected => true;
    }

    private async Task SeedTaskAsync(string id, string title, string? description)
    {
        await using var db = NewContext();
        if (!await db.Lists.AnyAsync(l => l.Id == "L1"))
            db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
        db.Tasks.Add(new TaskEntity
        {
            Number = TestTaskNumbers.Next(),
            Id = id, ListId = "L1", Title = title, Description = description,
            Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow,
        });
        await db.SaveChangesAsync();
    }

    private DetailsIslandViewModel BuildVm() =>
        new(new TestDbFactory(NewContext), new FakeWorker(), new NullServiceProvider(),
            new ClaudeDo.Ui.Services.MergeCoordinator());

    private async Task<TaskEntity> ReadTaskAsync(string id)
    {
        await using var db = NewContext();
        return await db.Tasks.FirstAsync(t => t.Id == id);
    }

    [Fact]
    public async Task TypingInA_ThenTaskSwappedUnderneath_StillWritesOntoA_NeverB()
    {
        // Reproduces the exact split the audit called out: `Bind()` assigns `Task` synchronously,
        // while `EditableDescription` is only reset once `BindAsync`'s DB round trip completes —
        // so for a window, `Task` already points at B while `EditableDescription` still holds A's
        // freshly typed text. Setting `Task` directly (bypassing `Bind()`'s own DB round trip and
        // its now-added save cancellation) isolates that exact window deterministically, instead of
        // racing the real 400 ms debounce against however long a real bind happens to take.
        await SeedTaskAsync("A", "Task A", "original A");
        await SeedTaskAsync("B", "Task B", "original B");

        var vm = BuildVm();
        vm.Bind(new TaskRowViewModel { Id = "A", Title = "Task A" });
        await Task.Delay(50);

        vm.EditableDescription = "typed into A";
        // Simulate the mid-debounce moment where Task already points at B but EditableDescription
        // has not been reset yet — without going through Bind() (which would cancel the pending
        // save and dispose this exact race).
        vm.Task = new TaskRowViewModel { Id = "B", Title = "Task B" };

        await Task.Delay(600); // past the debounce window

        var a = await ReadTaskAsync("A");
        var b = await ReadTaskAsync("B");
        Assert.Equal("typed into A", a.Description);
        Assert.Equal("original B", b.Description);
    }

    [Fact]
    public async Task Bind_CancelsPendingTitleAndDescriptionSaves_ForThePreviousTask()
    {
        await SeedTaskAsync("A", "Task A", "original A");
        await SeedTaskAsync("B", "Task B", "original B");

        var vm = BuildVm();
        vm.Bind(new TaskRowViewModel { Id = "A", Title = "Task A" });
        await Task.Delay(50);

        vm.EditableTitle = "typed title A";
        vm.EditableDescription = "typed desc A";
        vm.Bind(new TaskRowViewModel { Id = "B", Title = "Task B" });

        await Task.Delay(600); // past the debounce window

        var a = await ReadTaskAsync("A");
        Assert.Equal("Task A", a.Title);
        Assert.Equal("original A", a.Description);
    }
}
