using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using Microsoft.EntityFrameworkCore;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Data.Tests;

public sealed class TaskNumberAllocatorTests : IDisposable
{
    private readonly string _dbPath;
    private readonly DbContextOptions<ClaudeDoDbContext> _options;
    private readonly ClaudeDoDbContext _ctx;

    public TaskNumberAllocatorTests()
    {
        _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_tasknum_{Guid.NewGuid():N}.db");
        _options = new DbContextOptionsBuilder<ClaudeDoDbContext>()
            .UseSqlite($"Data Source={_dbPath}")
            .Options;
        _ctx = new ClaudeDoDbContext(_options);
        _ctx.Database.EnsureCreated();
    }

    public void Dispose()
    {
        _ctx.Dispose();
        foreach (var suffix in new[] { "", "-wal", "-shm" })
            try { File.Delete(_dbPath + suffix); } catch { }
    }

    private async Task<string> SeedListAsync(string id = "l1")
    {
        _ctx.Lists.Add(new ListEntity { Id = id, Name = "Test List", CreatedAt = DateTime.UtcNow });
        await _ctx.SaveChangesAsync();
        return id;
    }

    private static TaskEntity NewTask(string listId, string title = "T")
        => new()
        {
            Id = Guid.NewGuid().ToString(),
            ListId = listId,
            Title = title,
            Status = TaskStatus.Idle,
            CreatedAt = DateTime.UtcNow,
        };

    [Fact]
    public async Task AddAsync_assigns_increasing_unique_numbers()
    {
        await SeedListAsync();
        var repo = new TaskRepository(_ctx);

        var first = NewTask("l1");
        await repo.AddAsync(first);
        var second = NewTask("l1");
        await repo.AddAsync(second);

        Assert.True(first.Number > 0);
        Assert.True(second.Number > first.Number);
    }

    [Fact]
    public async Task CreateChildAsync_also_allocates_a_number()
    {
        await SeedListAsync();
        var parent = NewTask("l1");
        await new TaskRepository(_ctx).AddAsync(parent);

        var child = await new TaskRepository(_ctx).CreateChildAsync(parent.Id, "Child", null, null);

        Assert.True(child.Number > 0);
        Assert.NotEqual(parent.Number, child.Number);
    }

    [Fact]
    public async Task Deleting_the_highest_numbered_task_does_not_recycle_its_number()
    {
        await SeedListAsync();
        var repo = new TaskRepository(_ctx);

        var t1 = NewTask("l1");
        await repo.AddAsync(t1);
        var t2 = NewTask("l1");
        await repo.AddAsync(t2);
        Assert.True(t2.Number > t1.Number);

        await repo.DeleteAsync(t2.Id);

        var t3 = NewTask("l1");
        await repo.AddAsync(t3);

        Assert.True(t3.Number > t2.Number);
        Assert.NotEqual(t2.Number, t3.Number);
    }

    [Fact]
    public async Task Repeated_creation_never_assigns_the_same_number_twice()
    {
        var listId = await SeedListAsync();

        var numbers = new List<int>();
        for (var i = 0; i < 20; i++)
        {
            await using var ctx = new ClaudeDoDbContext(_options);
            var task = NewTask(listId, $"T{i}");
            await new TaskRepository(ctx).AddAsync(task);
            numbers.Add(task.Number);
        }

        Assert.Equal(numbers.Count, numbers.Distinct().Count());
    }

    [Fact]
    public async Task Concurrent_creation_never_assigns_the_same_number_twice()
    {
        var listId = await SeedListAsync();

        var tasks = Enumerable.Range(0, 10).Select(async i =>
        {
            await using var ctx = new ClaudeDoDbContext(_options);
            var task = NewTask(listId, $"C{i}");
            await new TaskRepository(ctx).AddAsync(task);
            return task.Number;
        });

        var numbers = await Task.WhenAll(tasks);

        Assert.Equal(numbers.Length, numbers.Distinct().Count());
        Assert.All(numbers, n => Assert.True(n > 0));
    }

    [Fact]
    public async Task AddAsync_recreates_a_missing_app_settings_row_instead_of_crashing()
    {
        await SeedListAsync();
        await _ctx.Database.ExecuteSqlRawAsync("DELETE FROM app_settings");

        var task = NewTask("l1");
        await new TaskRepository(_ctx).AddAsync(task);

        Assert.True(task.Number > 0);
    }

    [Fact]
    public async Task AddAsync_noncollision_DbUpdateException_surfaces_immediately_without_burning_numbers()
    {
        await SeedListAsync();
        var badTask = NewTask("no-such-list");

        await Assert.ThrowsAsync<DbUpdateException>(() => new TaskRepository(_ctx).AddAsync(badTask));

        // The failed attempt above should have burned exactly one number (attempt #1, no
        // collision-driven retries) -- confirm the next successful insert isn't several numbers
        // further along.
        var goodTask = NewTask("l1");
        await new TaskRepository(_ctx).AddAsync(goodTask);

        Assert.Equal(2, goodTask.Number);
    }

    [Fact]
    public async Task GetByNumberAsync_finds_the_task_and_returns_null_for_unknown_numbers()
    {
        await SeedListAsync();
        var task = NewTask("l1");
        await new TaskRepository(_ctx).AddAsync(task);

        var found = await new TaskRepository(_ctx).GetByNumberAsync(task.Number);
        var missing = await new TaskRepository(_ctx).GetByNumberAsync(task.Number + 999);

        Assert.NotNull(found);
        Assert.Equal(task.Id, found!.Id);
        Assert.Null(missing);
    }
}
