using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;

namespace MBN_STOCK_WEBVIEW.Core.Tests;

public sealed class LegacyRefreshSchedulerTests
{
    [Fact]
    public async Task LongPlayCallback_DoesNotConsumeTheInitialDelay()
    {
        var callbackCompletion = NewCompletion<bool>();
        var delays = new ControlledDelay();
        var scheduler = CreateScheduler(delays);

        var opportunity = scheduler.WaitForNextRefreshAsync(
            cancellationToken => callbackCompletion.Task.WaitAsync(cancellationToken),
            null,
            CancellationToken.None);

        await Task.Yield();
        Assert.Equal(0, delays.CallCount);

        callbackCompletion.SetResult(true);
        Assert.Equal(TimeSpan.FromSeconds(2), await delays.WaitForCallAsync(0));
        Assert.False(opportunity.IsCompleted);

        delays.Complete(0);
        Assert.True(await opportunity);
    }

    [Fact]
    public async Task OneShotScheduling_CoalescesMissedTicksAndPreventsBackToBackPlays()
    {
        var delays = new ControlledDelay();
        var scheduler = CreateScheduler(delays);
        var playCount = 0;

        var firstOpportunity = scheduler.WaitForNextRefreshAsync(
            _ => Task.FromResult(true),
            null,
            CancellationToken.None);
        Assert.Equal(TimeSpan.FromSeconds(2), await delays.WaitForCallAsync(0));
        delays.Complete(0);
        Assert.True(await firstOpportunity);
        Assert.True(await scheduler.ExecuteRefreshAsync(
            _ => Task.FromResult(++playCount == 1),
            CancellationToken.None));
        scheduler.MarkRefreshSucceeded();

        var firstPlayCompletion = NewCompletion<bool>();
        var secondOpportunity = scheduler.WaitForNextRefreshAsync(
            cancellationToken => firstPlayCompletion.Task.WaitAsync(cancellationToken),
            null,
            CancellationToken.None);

        await Task.Yield();
        Assert.Equal(1, delays.CallCount);
        Assert.Equal(1, playCount);

        firstPlayCompletion.SetResult(true);
        Assert.Equal(TimeSpan.FromSeconds(3), await delays.WaitForCallAsync(1));
        Assert.Equal(1, playCount);
        Assert.Equal(2, delays.CallCount);

        delays.Complete(1);
        Assert.True(await secondOpportunity);
        Assert.True(await scheduler.ExecuteRefreshAsync(
            _ => Task.FromResult(++playCount == 2),
            CancellationToken.None));
        Assert.Equal(2, playCount);
        Assert.Equal(2, delays.CallCount);
    }

    [Fact]
    public async Task ScheduledDelay_LeavesAQuiescentWindowForOperatorNext()
    {
        var delays = new ControlledDelay();
        var scheduler = CreateScheduler(delays);
        using var commandGate = new SemaphoreSlim(1, 1);
        using var cancellation = new CancellationTokenSource();
        var refreshCalled = false;

        var opportunity = RunOneRefreshAsync(
            scheduler,
            _ => Task.FromResult(true),
            async cancellationToken =>
            {
                await commandGate.WaitAsync(cancellationToken);
                try
                {
                    refreshCalled = true;
                    return true;
                }
                finally
                {
                    commandGate.Release();
                }
            },
            cancellation.Token);
        await delays.WaitForCallAsync(0);

        Assert.False(scheduler.IsRefreshInFlight);
        Assert.True(await commandGate.WaitAsync(0));
        commandGate.Release();

        cancellation.Cancel();
        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => opportunity);
        Assert.False(refreshCalled);
    }

    [Fact]
    public async Task StopCancellation_DropsTheScheduledRefresh()
    {
        var delays = new ControlledDelay();
        var scheduler = CreateScheduler(delays);
        using var cancellation = new CancellationTokenSource();
        var refreshCalled = false;

        var opportunity = RunOneRefreshAsync(
            scheduler,
            _ => Task.FromResult(true),
            _ =>
            {
                refreshCalled = true;
                return Task.FromResult(true);
            },
            cancellation.Token);
        await delays.WaitForCallAsync(0);

        cancellation.Cancel();
        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => opportunity);

        Assert.False(refreshCalled);
        Assert.False(scheduler.IsRefreshInFlight);
        Assert.Equal(1, delays.CallCount);
    }

    [Fact]
    public async Task ExecuteRefresh_RejectsASecondInFlightRefresh()
    {
        var scheduler = CreateScheduler(new ControlledDelay());
        var releaseRefresh = NewCompletion<bool>();
        var refreshEntered = NewCompletion<bool>();

        var firstRefresh = scheduler.ExecuteRefreshAsync(
            async cancellationToken =>
            {
                refreshEntered.SetResult(true);
                return await releaseRefresh.Task.WaitAsync(cancellationToken);
            },
            CancellationToken.None);
        await refreshEntered.Task;

        Assert.True(scheduler.IsRefreshInFlight);
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            scheduler.ExecuteRefreshAsync(_ => Task.FromResult(true), CancellationToken.None));

        releaseRefresh.SetResult(true);
        Assert.True(await firstRefresh);
        Assert.False(scheduler.IsRefreshInFlight);
    }

    [Fact]
    public async Task CallbackTimeout_DoesNotScheduleOrDispatchARefresh()
    {
        var delays = new ControlledDelay();
        var scheduler = CreateScheduler(delays);
        var refreshCalled = false;

        var ready = await RunOneRefreshAsync(
            scheduler,
            _ => Task.FromResult(false),
            _ =>
            {
                refreshCalled = true;
                return Task.FromResult(true);
            },
            CancellationToken.None);

        Assert.False(ready);
        Assert.False(refreshCalled);
        Assert.Equal(0, delays.CallCount);
    }

    [Fact]
    public async Task MaximumOne_StopsBeforeASecondDelayOrRefreshCanBeScheduled()
    {
        var delays = new ControlledDelay();
        var scheduler = CreateScheduler(delays, maximumRefreshCount: 1);
        var untrackedCalls = 0;
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            scheduler.ExecuteRefreshAsync(
                _ =>
                {
                    untrackedCalls++;
                    return Task.FromResult(true);
                },
                CancellationToken.None));
        Assert.Equal(0, untrackedCalls);
        Assert.Throws<InvalidOperationException>(scheduler.MarkRefreshSucceeded);

        var firstOpportunity = scheduler.WaitForNextRefreshAsync(
            _ => Task.FromResult(true),
            null,
            CancellationToken.None);
        Assert.Equal(TimeSpan.FromSeconds(2), await delays.WaitForCallAsync(0));
        delays.Complete(0);
        Assert.True(await firstOpportunity);

        Assert.True(await scheduler.ExecuteRefreshAndTrackSuccessAsync(
            _ => Task.FromResult(true),
            static result => result,
            CancellationToken.None));

        Assert.Equal(1, scheduler.CompletedRefreshCount);
        Assert.Equal(1, scheduler.MaximumRefreshCount);
        Assert.True(scheduler.HasReachedMaximum);
        Assert.False(scheduler.IsMaximumCallbackDrained);
        var finalCallback = NewCompletion<bool>();
        var drain = scheduler.DrainMaximumCallbackAsync(
            cancellationToken => finalCallback.Task.WaitAsync(cancellationToken),
            CancellationToken.None);
        await Task.Yield();
        Assert.False(drain.IsCompleted);
        Assert.False(scheduler.IsMaximumCallbackDrained);
        finalCallback.SetResult(true);
        Assert.True(await drain);
        Assert.True(scheduler.IsMaximumCallbackDrained);
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            scheduler.WaitForNextRefreshAsync(
                _ => Task.FromResult(true),
                null,
                CancellationToken.None));
        var unexpectedRefreshCalls = 0;
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            scheduler.ExecuteRefreshAsync(
                _ =>
                {
                    unexpectedRefreshCalls++;
                    return Task.FromResult(true);
                },
                CancellationToken.None));
        Assert.Equal(0, unexpectedRefreshCalls);
        Assert.Equal(1, delays.CallCount);
        Assert.Throws<InvalidOperationException>(scheduler.MarkRefreshSucceeded);
    }

    [Fact]
    public void NullMaximum_PreservesUnlimitedLegacyScheduling()
    {
        var scheduler = CreateScheduler(new ControlledDelay());

        for (var index = 0; index < 10_000; index++)
        {
            scheduler.MarkRefreshSucceeded();
        }

        Assert.Null(scheduler.MaximumRefreshCount);
        Assert.Equal(10_000, scheduler.CompletedRefreshCount);
        Assert.False(scheduler.HasReachedMaximum);
    }

    [Fact]
    public async Task ZeroMaximum_DisablesAutomaticRefreshBeforeCallbacksOrTheFirstDelay()
    {
        var delays = new ControlledDelay();
        var scheduler = CreateScheduler(delays, maximumRefreshCount: 0);
        var callbackCalls = 0;
        var refreshCalls = 0;

        Assert.Equal(0, scheduler.CompletedRefreshCount);
        Assert.Equal(0, scheduler.MaximumRefreshCount);
        Assert.True(scheduler.HasReachedMaximum);
        Assert.True(scheduler.IsMaximumCallbackDrained);

        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            scheduler.WaitForNextRefreshAsync(
                _ =>
                {
                    callbackCalls++;
                    return Task.FromResult(true);
                },
                null,
                CancellationToken.None));
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            scheduler.ExecuteRefreshAndTrackSuccessAsync(
                _ =>
                {
                    refreshCalls++;
                    return Task.FromResult(true);
                },
                static result => result,
                CancellationToken.None));
        Assert.True(await scheduler.DrainMaximumCallbackAsync(
            _ =>
            {
                callbackCalls++;
                return Task.FromResult(true);
            },
            CancellationToken.None));

        Assert.Equal(0, callbackCalls);
        Assert.Equal(0, refreshCalls);
        Assert.Equal(0, delays.CallCount);
        Assert.Throws<InvalidOperationException>(scheduler.MarkRefreshSucceeded);
    }

    [Fact]
    public async Task MaximumCallbackTimeout_DoesNotConfirmTheLimit()
    {
        var scheduler = CreateScheduler(new ControlledDelay(), maximumRefreshCount: 1);
        Assert.True(await scheduler.ExecuteRefreshAndTrackSuccessAsync(
            _ => Task.FromResult(true),
            static result => result,
            CancellationToken.None));

        Assert.False(await scheduler.DrainMaximumCallbackAsync(
            _ => Task.FromResult(false),
            CancellationToken.None));
        Assert.True(scheduler.HasReachedMaximum);
        Assert.False(scheduler.IsMaximumCallbackDrained);
    }

    [Fact]
    public async Task FailedTrackedRefresh_DoesNotConsumeTheMaximum()
    {
        var scheduler = CreateScheduler(new ControlledDelay(), maximumRefreshCount: 1);

        Assert.False(await scheduler.ExecuteRefreshAndTrackSuccessAsync(
            _ => Task.FromResult(false),
            static result => result,
            CancellationToken.None));

        Assert.Equal(0, scheduler.CompletedRefreshCount);
        Assert.False(scheduler.HasReachedMaximum);
        Assert.False(scheduler.IsMaximumCallbackDrained);
    }

    [Fact]
    public void NegativeMaximum_IsRejected()
    {
        Assert.Throws<ArgumentOutOfRangeException>(() =>
            new LegacyRefreshScheduler(
                TimeSpan.FromSeconds(2),
                TimeSpan.FromSeconds(3),
                maximumRefreshCount: -1));
    }

    private static LegacyRefreshScheduler CreateScheduler(
        ControlledDelay delays,
        int? maximumRefreshCount = null) =>
        new(
            TimeSpan.FromSeconds(2),
            TimeSpan.FromSeconds(3),
            delays.DelayAsync,
            maximumRefreshCount);

    private static async Task<bool> RunOneRefreshAsync(
        LegacyRefreshScheduler scheduler,
        Func<CancellationToken, Task<bool>> waitForPlayCompletionAsync,
        Func<CancellationToken, Task<bool>> refreshAsync,
        CancellationToken cancellationToken)
    {
        if (!await scheduler.WaitForNextRefreshAsync(
            waitForPlayCompletionAsync,
            null,
            cancellationToken))
        {
            return false;
        }

        return await scheduler.ExecuteRefreshAsync(refreshAsync, cancellationToken);
    }

    private static TaskCompletionSource<T> NewCompletion<T>() =>
        new(TaskCreationOptions.RunContinuationsAsynchronously);

    private sealed class ControlledDelay
    {
        private readonly object _gate = new();
        private readonly List<DelayRequest> _requests = [];
        private TaskCompletionSource<bool> _requestAdded = NewCompletion<bool>();

        public int CallCount
        {
            get
            {
                lock (_gate)
                {
                    return _requests.Count;
                }
            }
        }

        public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken)
        {
            DelayRequest request;
            TaskCompletionSource<bool> requestAdded;
            lock (_gate)
            {
                request = new DelayRequest(delay);
                _requests.Add(request);
                requestAdded = _requestAdded;
                _requestAdded = NewCompletion<bool>();
            }

            requestAdded.TrySetResult(true);
            return request.Release.Task.WaitAsync(cancellationToken);
        }

        public async Task<TimeSpan> WaitForCallAsync(int index)
        {
            while (true)
            {
                Task requestAdded;
                lock (_gate)
                {
                    if (_requests.Count > index)
                    {
                        return _requests[index].Delay;
                    }

                    requestAdded = _requestAdded.Task;
                }

                await requestAdded.WaitAsync(TimeSpan.FromSeconds(5));
            }
        }

        public void Complete(int index)
        {
            TaskCompletionSource<bool> release;
            lock (_gate)
            {
                release = _requests[index].Release;
            }

            release.SetResult(true);
        }

        private sealed record DelayRequest(
            TimeSpan Delay,
            TaskCompletionSource<bool> Release)
        {
            public DelayRequest(TimeSpan delay)
                : this(delay, NewCompletion<bool>())
            {
            }
        }
    }
}
