using System.Collections.Concurrent;
using MBN_STOCK_WEBVIEW.Playout.Runtime;

namespace MBN_STOCK_WEBVIEW.Playout.Tests;

public sealed class StaDispatcherTests
{
    [Fact]
    public async Task InvokeAsync_RunsCallbacksOnOneStaThreadInFifoOrder()
    {
        await using var dispatcher = new StaDispatcher(capacity: 4);
        using var firstStarted = new ManualResetEventSlim();
        using var releaseFirst = new ManualResetEventSlim();
        var calls = new ConcurrentQueue<(int Order, int ThreadId, ApartmentState Apartment)>();

        var first = dispatcher.InvokeAsync(
            () =>
            {
                calls.Enqueue((
                    1,
                    Environment.CurrentManagedThreadId,
                    Thread.CurrentThread.GetApartmentState()));
                firstStarted.Set();
                Assert.True(releaseFirst.Wait(TimeSpan.FromSeconds(5)));
                return 1;
            },
            TimeSpan.FromSeconds(10),
            CancellationToken.None);
        Assert.True(firstStarted.Wait(TimeSpan.FromSeconds(5)));

        var second = dispatcher.InvokeAsync(
            () =>
            {
                calls.Enqueue((
                    2,
                    Environment.CurrentManagedThreadId,
                    Thread.CurrentThread.GetApartmentState()));
                return 2;
            },
            TimeSpan.FromSeconds(10),
            CancellationToken.None);
        var third = dispatcher.InvokeAsync(
            () =>
            {
                calls.Enqueue((
                    3,
                    Environment.CurrentManagedThreadId,
                    Thread.CurrentThread.GetApartmentState()));
                return 3;
            },
            TimeSpan.FromSeconds(10),
            CancellationToken.None);

        releaseFirst.Set();
        var results = await Task.WhenAll(first, second, third);
        Assert.Equal(new[] { 1, 2, 3 }, results);

        var recorded = calls.ToArray();
        Assert.Equal([1, 2, 3], recorded.Select(call => call.Order));
        Assert.Single(recorded.Select(call => call.ThreadId).Distinct());
        Assert.All(recorded, call => Assert.Equal(ApartmentState.STA, call.Apartment));
        Assert.Equal(recorded[0].ThreadId, dispatcher.ManagedThreadId);
    }

    [Fact]
    public async Task InvokeAsync_CancelledWhileQueued_DoesNotRunCallback()
    {
        await using var dispatcher = new StaDispatcher(capacity: 2);
        using var firstStarted = new ManualResetEventSlim();
        using var releaseFirst = new ManualResetEventSlim();
        var cancelledCallbackRan = false;

        var first = dispatcher.InvokeAsync(
            () =>
            {
                firstStarted.Set();
                Assert.True(releaseFirst.Wait(TimeSpan.FromSeconds(5)));
                return 1;
            },
            TimeSpan.FromSeconds(10),
            CancellationToken.None);
        Assert.True(firstStarted.Wait(TimeSpan.FromSeconds(5)));

        using var cancellation = new CancellationTokenSource();
        var cancelled = dispatcher.InvokeAsync(
            () =>
            {
                cancelledCallbackRan = true;
                return 2;
            },
            TimeSpan.FromSeconds(10),
            cancellation.Token);
        cancellation.Cancel();
        releaseFirst.Set();

        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => cancelled);
        Assert.Equal(1, await first);
        Assert.False(cancelledCallbackRan);
    }

    [Fact]
    public async Task InvokeAsync_WhenBoundedQueueIsFull_CanCancelWithoutRunning()
    {
        await using var dispatcher = new StaDispatcher(capacity: 1);
        using var firstStarted = new ManualResetEventSlim();
        using var releaseFirst = new ManualResetEventSlim();
        var thirdCallbackRan = false;

        var first = dispatcher.InvokeAsync(
            () =>
            {
                firstStarted.Set();
                Assert.True(releaseFirst.Wait(TimeSpan.FromSeconds(5)));
                return 1;
            },
            TimeSpan.FromSeconds(10),
            CancellationToken.None);
        Assert.True(firstStarted.Wait(TimeSpan.FromSeconds(5)));

        var second = dispatcher.InvokeAsync(
            () => 2,
            TimeSpan.FromSeconds(10),
            CancellationToken.None);
        using var cancellation = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
        var third = dispatcher.InvokeAsync(
            () =>
            {
                thirdCallbackRan = true;
                return 3;
            },
            TimeSpan.FromSeconds(10),
            cancellation.Token);

        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => third);
        Assert.False(thirdCallbackRan);
        releaseFirst.Set();
        var results = await Task.WhenAll(first, second);
        Assert.Equal(new[] { 1, 2 }, results);
    }

    [Fact]
    public async Task InvokeAsync_TimeoutQuarantinesDispatcherAndRejectsLaterWork()
    {
        await using var dispatcher = new StaDispatcher(capacity: 2);
        using var callbackStarted = new ManualResetEventSlim();
        using var releaseCallback = new ManualResetEventSlim();

        var timedOut = dispatcher.InvokeAsync(
            () =>
            {
                callbackStarted.Set();
                Assert.True(releaseCallback.Wait(TimeSpan.FromSeconds(5)));
                return 1;
            },
            TimeSpan.FromMilliseconds(100),
            CancellationToken.None);
        Assert.True(callbackStarted.Wait(TimeSpan.FromSeconds(5)));

        await Assert.ThrowsAsync<StaOperationTimedOutException>(() => timedOut);
        Assert.True(dispatcher.IsQuarantined);
        await Assert.ThrowsAsync<StaDispatcherQuarantinedException>(
            () => dispatcher.InvokeAsync(
                () => 2,
                TimeSpan.FromSeconds(1),
                CancellationToken.None));

        releaseCallback.Set();
    }
}
