using GmRelay.Bot.Features.Confirmation.SendConfirmation;
using GmRelay.Bot.Features.Reminders.SendJoinLink;
using GmRelay.Bot.Features.Reminders.SendOneHourReminder;
using GmRelay.Bot.Infrastructure.Scheduling;
using Microsoft.Extensions.Logging.Abstractions;

namespace GmRelay.Bot.Tests.Infrastructure.Scheduling;

public sealed class SessionSchedulerServiceTests
{
    private readonly FakeSystemClock _clock = new();
    private readonly FakeSessionTriggerStore _store = new();
    private readonly FakeSendConfirmationHandler _confirmationHandler = new();
    private readonly FakeSendOneHourReminderHandler _oneHourHandler = new();
    private readonly FakeSendJoinLinkHandler _joinLinkHandler = new();

    private SessionSchedulerService CreateSut()
    {
        return new SessionSchedulerService(
            _store,
            _confirmationHandler,
            _oneHourHandler,
            _joinLinkHandler,
            _clock,
            NullLogger<SessionSchedulerService>.Instance);
    }

    [Fact]
    public async Task TickAsync_WhenSessionNeedsConfirmation_CallsConfirmationHandler()
    {
        var sessionId = Guid.NewGuid();
        var now = new DateTimeOffset(2026, 5, 15, 10, 0, 0, TimeSpan.Zero);
        _clock.UtcNow = now;
        _store.SessionsNeedingConfirmation = [sessionId];

        var sut = CreateSut();
        await sut.TickAsync(CancellationToken.None);

        Assert.Single(_confirmationHandler.Calls);
        Assert.Equal(sessionId, _confirmationHandler.Calls[0]);
    }

    [Fact]
    public async Task TickAsync_WhenSessionNeedsOneHourReminder_CallsOneHourHandler()
    {
        var sessionId = Guid.NewGuid();
        var now = new DateTimeOffset(2026, 5, 15, 10, 0, 0, TimeSpan.Zero);
        _clock.UtcNow = now;
        _store.SessionsNeedingOneHourReminder = [sessionId];

        var sut = CreateSut();
        await sut.TickAsync(CancellationToken.None);

        Assert.Single(_oneHourHandler.Calls);
        Assert.Equal(sessionId, _oneHourHandler.Calls[0]);
    }

    [Fact]
    public async Task TickAsync_WhenSessionNeedsJoinLink_CallsJoinLinkHandler()
    {
        var sessionId = Guid.NewGuid();
        var now = new DateTimeOffset(2026, 5, 15, 10, 0, 0, TimeSpan.Zero);
        _clock.UtcNow = now;
        _store.SessionsNeedingJoinLink = [sessionId];

        var sut = CreateSut();
        await sut.TickAsync(CancellationToken.None);

        Assert.Single(_joinLinkHandler.Calls);
        Assert.Equal(sessionId, _joinLinkHandler.Calls[0]);
    }

    [Fact]
    public async Task TickAsync_WhenMultipleTriggers_AllHandlersCalled()
    {
        var confirmId = Guid.NewGuid();
        var remindId = Guid.NewGuid();
        var linkId = Guid.NewGuid();
        var now = new DateTimeOffset(2026, 5, 15, 10, 0, 0, TimeSpan.Zero);
        _clock.UtcNow = now;
        _store.SessionsNeedingConfirmation = [confirmId];
        _store.SessionsNeedingOneHourReminder = [remindId];
        _store.SessionsNeedingJoinLink = [linkId];

        var sut = CreateSut();
        await sut.TickAsync(CancellationToken.None);

        Assert.Single(_confirmationHandler.Calls);
        Assert.Single(_oneHourHandler.Calls);
        Assert.Single(_joinLinkHandler.Calls);
    }

    [Fact]
    public async Task TickAsync_WhenNoSessions_NothingCalled()
    {
        var sut = CreateSut();
        await sut.TickAsync(CancellationToken.None);

        Assert.Empty(_confirmationHandler.Calls);
        Assert.Empty(_oneHourHandler.Calls);
        Assert.Empty(_joinLinkHandler.Calls);
    }

    [Fact]
    public async Task TickAsync_WhenConfirmationAlreadySent_DoesNotCallAgain()
    {
        var sessionId = Guid.NewGuid();
        var now = new DateTimeOffset(2026, 5, 15, 10, 0, 0, TimeSpan.Zero);
        _clock.UtcNow = now;
        _store.SessionsNeedingConfirmation = [sessionId];

        var sut = CreateSut();
        await sut.TickAsync(CancellationToken.None);
        Assert.Single(_confirmationHandler.Calls);

        // Simulate idempotency: store no longer returns the session
        _store.SessionsNeedingConfirmation = [];
        await sut.TickAsync(CancellationToken.None);

        Assert.Single(_confirmationHandler.Calls);
    }

    [Fact]
    public async Task TickAsync_WhenHandlerThrows_LogsErrorAndContinues()
    {
        var goodId = Guid.NewGuid();
        var badId = Guid.NewGuid();
        var now = new DateTimeOffset(2026, 5, 15, 10, 0, 0, TimeSpan.Zero);
        _clock.UtcNow = now;
        _store.SessionsNeedingConfirmation = [badId, goodId];
        _confirmationHandler.ThrowFor.Add(badId);

        var sut = CreateSut();
        await sut.TickAsync(CancellationToken.None);

        Assert.Equal(2, _confirmationHandler.Calls.Count);
        Assert.Equal(badId, _confirmationHandler.Calls[0]);
        Assert.Equal(goodId, _confirmationHandler.Calls[1]);
    }

    [Fact]
    public async Task TickAsync_WhenStoreThrows_LogsErrorAndDoesNotCrash()
    {
        _store.ThrowOnConfirmationQuery = true;
        var sut = CreateSut();

        var ex = await Record.ExceptionAsync(() => sut.TickAsync(CancellationToken.None));
        Assert.Null(ex);
    }

    private sealed class FakeSendConfirmationHandler : ISendConfirmationHandler
    {
        public List<Guid> Calls { get; } = [];
        public HashSet<Guid> ThrowFor { get; } = [];

        public Task HandleAsync(Guid sessionId, CancellationToken ct)
        {
            Calls.Add(sessionId);
            if (ThrowFor.Contains(sessionId))
                throw new InvalidOperationException($"Boom for {sessionId}");
            return Task.CompletedTask;
        }
    }

    private sealed class FakeSendOneHourReminderHandler : ISendOneHourReminderHandler
    {
        public List<Guid> Calls { get; } = [];

        public Task HandleAsync(Guid sessionId, CancellationToken ct)
        {
            Calls.Add(sessionId);
            return Task.CompletedTask;
        }
    }

    private sealed class FakeSendJoinLinkHandler : ISendJoinLinkHandler
    {
        public List<Guid> Calls { get; } = [];

        public Task HandleAsync(Guid sessionId, CancellationToken ct)
        {
            Calls.Add(sessionId);
            return Task.CompletedTask;
        }
    }

    private sealed class FakeSessionTriggerStore : ISessionTriggerStore
    {
        public List<Guid> SessionsNeedingConfirmation { get; set; } = [];
        public List<Guid> SessionsNeedingOneHourReminder { get; set; } = [];
        public List<Guid> SessionsNeedingJoinLink { get; set; } = [];
        public bool ThrowOnConfirmationQuery { get; set; }

        public Task<IReadOnlyList<Guid>> GetSessionsNeedingConfirmationAsync(
            DateTimeOffset now, CancellationToken ct)
        {
            if (ThrowOnConfirmationQuery)
                throw new InvalidOperationException("Store boom");
            return Task.FromResult<IReadOnlyList<Guid>>(SessionsNeedingConfirmation);
        }

        public Task<IReadOnlyList<Guid>> GetSessionsNeedingOneHourReminderAsync(
            DateTimeOffset now, CancellationToken ct)
        {
            return Task.FromResult<IReadOnlyList<Guid>>(SessionsNeedingOneHourReminder);
        }

        public Task<IReadOnlyList<Guid>> GetSessionsNeedingJoinLinkAsync(
            DateTimeOffset now, CancellationToken ct)
        {
            return Task.FromResult<IReadOnlyList<Guid>>(SessionsNeedingJoinLink);
        }
    }
}
