using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Prime;
using ClaudeDo.Worker.Tests.Infrastructure;
using Microsoft.Extensions.Logging.Abstractions;

namespace ClaudeDo.Worker.Tests.Prime;

public class PrimeSchedulerTests : IDisposable
{
    private readonly DbFixture _db = new();
    public void Dispose() => _db.Dispose();

    private sealed class FakeClock : IPrimeClock
    {
        public DateTimeOffset Now { get; set; }
    }

    private sealed class FakeRunner : IPrimeRunner
    {
        public List<Guid> FiredIds { get; } = new();
        public Task<PrimeRunOutcome> FireAsync(PrimeScheduleDto s, CancellationToken ct)
        {
            FiredIds.Add(s.Id);
            return Task.FromResult(new PrimeRunOutcome(true, "ok"));
        }
    }

    private sealed class FakeBroadcaster : IPrimeBroadcaster
    {
        public List<(Guid id, bool ok, string msg)> Calls { get; } = new();
        public Task PrimeFiredAsync(Guid id, bool ok, string msg, DateTimeOffset firedAt)
        {
            Calls.Add((id, ok, msg));
            return Task.CompletedTask;
        }
        public Task PrepStartedAsync() => Task.CompletedTask;
        public Task PrepLineAsync(string line) => Task.CompletedTask;
        public Task PrepFinishedAsync(bool success) => Task.CompletedTask;
    }

    [Fact]
    public async Task Fires_Schedule_When_Within_CatchUp_On_Startup()
    {
        var id = Guid.NewGuid();
        using (var ctx = _db.CreateContext())
            await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity
            {
                Id = id,
                Days = PrimeDays.All,
                TimeOfDay = new TimeSpan(7, 0, 0),
                Enabled = true,
                CreatedAt = DateTimeOffset.UtcNow,
            });

        var clock = new FakeClock { Now = new DateTimeOffset(2026, 5, 5, 7, 10, 0, TimeSpan.FromHours(2)) };
        var runner = new FakeRunner();
        var broadcaster = new FakeBroadcaster();
        var signal = new PrimeScheduleSignal();
        var scheduler = new PrimeScheduler(
            _db.CreateFactory(), runner, clock, signal, broadcaster,
            PrimeSchedulerOptions.Default, NullLogger<PrimeScheduler>.Instance);

        using var cts = new CancellationTokenSource();
        var run = scheduler.StartAsync(cts.Token);

        await WaitFor(() => runner.FiredIds.Count >= 1, TimeSpan.FromSeconds(3));

        cts.Cancel();
        await scheduler.StopAsync(CancellationToken.None);

        Assert.Single(runner.FiredIds);
        Assert.Equal(id, runner.FiredIds[0]);
        Assert.Single(broadcaster.Calls);
        Assert.True(broadcaster.Calls[0].ok);

        using var read = _db.CreateContext();
        var row = await new PrimeScheduleRepository(read).GetAsync(id);
        Assert.NotNull(row!.LastRunAt);
    }

    [Fact]
    public async Task Does_Not_Fire_Past_CatchUp_Window()
    {
        var id = Guid.NewGuid();
        using (var ctx = _db.CreateContext())
            await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity
            {
                Id = id,
                Days = PrimeDays.All,
                TimeOfDay = new TimeSpan(7, 0, 0),
                Enabled = true,
                CreatedAt = DateTimeOffset.UtcNow,
            });

        var clock = new FakeClock { Now = new DateTimeOffset(2026, 5, 5, 9, 0, 0, TimeSpan.FromHours(2)) };
        var runner = new FakeRunner();
        var signal = new PrimeScheduleSignal();
        var scheduler = new PrimeScheduler(
            _db.CreateFactory(), runner, clock, signal, new FakeBroadcaster(),
            PrimeSchedulerOptions.Default, NullLogger<PrimeScheduler>.Instance);

        using var cts = new CancellationTokenSource();
        await scheduler.StartAsync(cts.Token);
        await Task.Delay(200);
        cts.Cancel();
        await scheduler.StopAsync(CancellationToken.None);

        Assert.Empty(runner.FiredIds);
    }

    [Fact]
    public async Task Signal_Recomputes_Mid_Wait()
    {
        var clock = new FakeClock { Now = new DateTimeOffset(2026, 5, 5, 7, 10, 0, TimeSpan.FromHours(2)) };
        var runner = new FakeRunner();
        var signal = new PrimeScheduleSignal();
        var scheduler = new PrimeScheduler(
            _db.CreateFactory(), runner, clock, signal, new FakeBroadcaster(),
            PrimeSchedulerOptions.Default, NullLogger<PrimeScheduler>.Instance);

        using var cts = new CancellationTokenSource();
        await scheduler.StartAsync(cts.Token);

        var id = Guid.NewGuid();
        using (var ctx = _db.CreateContext())
            await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity
            {
                Id = id,
                Days = PrimeDays.All,
                TimeOfDay = new TimeSpan(7, 0, 0),
                Enabled = true,
                CreatedAt = DateTimeOffset.UtcNow,
            });
        signal.Signal();

        await WaitFor(() => runner.FiredIds.Count >= 1, TimeSpan.FromSeconds(3));
        cts.Cancel();
        await scheduler.StopAsync(CancellationToken.None);

        Assert.Single(runner.FiredIds);
    }

    private static async Task WaitFor(Func<bool> cond, TimeSpan timeout)
    {
        var deadline = DateTime.UtcNow + timeout;
        while (!cond() && DateTime.UtcNow < deadline)
            await Task.Delay(20);
    }
}
