// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Globalization;

namespace Microsoft.AspNetCore.Components.Rendering;

public class RendererSynchronizationContextTest
{
    // Nothing should exceed the timeout in a successful run of the the tests, this is just here to catch
    // failures.
    public TimeSpan Timeout = Debugger.IsAttached ? System.Threading.Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10);

    [Fact]
    public void Post_RunsAsynchronously_WhenNotBusy()
    {
        // Arrange
        var context = new RendererSynchronizationContext();
        var thread = Thread.CurrentThread;
        Thread capturedThread = null;

        var e = new ManualResetEventSlim();

        // Act
        context.Post((_) =>
        {
            capturedThread = Thread.CurrentThread;

            e.Set();
        }, null);

        // Assert
        Assert.True(e.Wait(Timeout), "timeout");
        Assert.NotSame(thread, capturedThread);
    }

    [Fact]
    public void Post_RunsAsynchronously_WhenNotBusy_Exception()
    {
        // Arrange
        var context = new RendererSynchronizationContext();

        Exception exception = null;
        context.UnhandledException += (sender, e) =>
        {
            exception = (InvalidTimeZoneException)e.ExceptionObject;
        };

        // Act
        context.Post((_) =>
        {
            throw new InvalidTimeZoneException();
        }, null);

        // Assert
        //
        // Use another item to 'push through' the throwing one
        context.Send((_) => { }, null);
        Assert.NotNull(exception);
    }

    [Fact]
    public async Task Post_CanRunAsynchronously_WhenBusy()
    {
        // Arrange
        var context = new RendererSynchronizationContext();
        var thread = Thread.CurrentThread;
        Thread capturedThread = null;

        var e1 = new ManualResetEventSlim();
        var e2 = new ManualResetEventSlim();
        var e3 = new ManualResetEventSlim();

        var task = Task.Run(() =>
        {
            context.Send((_) =>
            {
                e1.Set();
                Assert.True(e2.Wait(Timeout), "timeout");
            }, null);
        });

        Assert.True(e1.Wait(Timeout), "timeout");

        // Act
        context.Post((_) =>
        {
            capturedThread = Thread.CurrentThread;

            e3.Set();
        }, null);

        // Assert
        Assert.False(e2.IsSet);
        e2.Set(); // Unblock the first item
        await task;

        Assert.True(e3.Wait(Timeout), "timeout");
        Assert.NotSame(thread, capturedThread);
    }

    [Fact]
    public async Task Post_CanRunAsynchronously_CaptureExecutionContext()
    {
        // Arrange
        var context = new RendererSynchronizationContext();

        // CultureInfo uses the execution context.
        CultureInfo.CurrentCulture = new CultureInfo("en-GB");
        CultureInfo capturedCulture = null;

        SynchronizationContext capturedContext = null;

        var e1 = new ManualResetEventSlim();
        var e2 = new ManualResetEventSlim();
        var e3 = new ManualResetEventSlim();

        var task = Task.Run(() =>
        {
            context.Send((_) =>
            {
                e1.Set();
                Assert.True(e2.Wait(Timeout), "timeout");
            }, null);
        });

        Assert.True(e1.Wait(Timeout), "timeout");

        // Act
        SynchronizationContext original = SynchronizationContext.Current;

        try
        {
            SynchronizationContext.SetSynchronizationContext(context);
            context.Post((_) =>
            {
                capturedCulture = CultureInfo.CurrentCulture;
                capturedContext = SynchronizationContext.Current;
                e3.Set();
            }, null);
        }
        finally
        {
            SynchronizationContext.SetSynchronizationContext(original);
        }

        // Assert
        Assert.False(e2.IsSet);
        e2.Set(); // Unblock the first item
        await task;

        Assert.True(e3.Wait(Timeout), "timeout");
        Assert.Same(CultureInfo.CurrentCulture, capturedCulture);
        Assert.Same(context, capturedContext);
    }

    [Fact]
    public async Task Post_CanRunAsynchronously_WhenBusy_Exception()
    {
        // Arrange
        var context = new RendererSynchronizationContext();

        Exception exception = null;
        context.UnhandledException += (sender, e) =>
        {
            exception = (InvalidTimeZoneException)e.ExceptionObject;
        };

        var e1 = new ManualResetEventSlim();
        var e2 = new ManualResetEventSlim();

        var task = Task.Run(() =>
        {
            context.Send((_) =>
            {
                e1.Set();
                Assert.True(e2.Wait(Timeout), "timeout");
            }, null);
        });

        Assert.True(e1.Wait(Timeout), "timeout");

        // Act
        context.Post((_) =>
        {
            throw new InvalidTimeZoneException();
        }, null);

        // Assert
        Assert.False(e2.IsSet);
        e2.Set(); // Unblock the first item
        await task;

        // Use another item to 'push through' the throwing one
        context.Send((_) => { }, null);
        Assert.NotNull(exception);
    }

    [Fact]
    public async Task Post_BackgroundWorkItem_CanProcessMoreItemsInline()
    {
        // Arrange
        var context = new RendererSynchronizationContext();
        Thread capturedThread = null;

        var e1 = new ManualResetEventSlim();
        var e2 = new ManualResetEventSlim();
        var e3 = new ManualResetEventSlim();
        var e4 = new ManualResetEventSlim();
        var e5 = new ManualResetEventSlim();
        var e6 = new ManualResetEventSlim();

        // Force task2 to execute in the background
        var task1 = Task.Run(() => context.Send((_) =>
        {
            e1.Set();
            Assert.True(e2.Wait(Timeout), "timeout");
        }, null));

        Assert.True(e1.Wait(Timeout), "timeout");

        var task2 = Task.Run(() =>
        {
            context.Send((_) =>
            {
                e3.Set();
                Assert.True(e4.Wait(Timeout), "timeout");
                capturedThread = Thread.CurrentThread;
            }, null);
        });

        e2.Set();
        await task1;

        Assert.True(e3.Wait(Timeout), "timeout");

        // Act
        //
        // Now task2 is 'running' in the sync context. Schedule more work items - they will be
        // run immediately after the second item
        context.Post((_) =>
        {
            e5.Set();
            Assert.Same(Thread.CurrentThread, capturedThread);
        }, null);
        context.Post((_) =>
        {
            e6.Set();
            Assert.Same(Thread.CurrentThread, capturedThread);
        }, null);

        // Assert
        e4.Set();
        await task2;

        Assert.True(e5.Wait(Timeout), "timeout");
        Assert.True(e6.Wait(Timeout), "timeout");
    }

    [Fact]
    public void Post_CapturesContext()
    {
        // Arrange
        var context = new RendererSynchronizationContext();

        var e1 = new ManualResetEventSlim();

        // CultureInfo uses the execution context.
        CultureInfo.CurrentCulture = new CultureInfo("en-GB");
        CultureInfo capturedCulture = null;

        SynchronizationContext capturedContext = null;

        // Act
        context.Post(async (_) =>
        {
            await Task.Yield();

            capturedCulture = CultureInfo.CurrentCulture;
            capturedContext = SynchronizationContext.Current;
            e1.Set();
        }, null);

        // Assert
        Assert.True(e1.Wait(Timeout), "timeout");
        Assert.Same(CultureInfo.CurrentCulture, capturedCulture);
        Assert.Same(context, capturedContext);
    }

    [Fact]
    public void Send_CanRunSynchronously()
    {
        // Arrange
        var context = new RendererSynchronizationContext();
        var thread = Thread.CurrentThread;
        Thread capturedThread = null;

        // Act
        context.Send((_) =>
        {
            capturedThread = Thread.CurrentThread;
        }, null);

        // Assert
        Assert.Same(thread, capturedThread);
    }

    [Fact]
    public void Send_CanRunSynchronously_Exception()
    {
        // Arrange
        var context = new RendererSynchronizationContext();

        // Act & Assert
        Assert.Throws<InvalidTimeZoneException>(() => context.Send((_) =>
        {
            throw new InvalidTimeZoneException();
        }, null));
    }

    [Fact]
    public async Task Send_BlocksWhenOtherWorkRunning()
    {
        // Arrange
        var context = new RendererSynchronizationContext();

        var e1 = new ManualResetEventSlim();
        var e2 = new ManualResetEventSlim();
        var e3 = new ManualResetEventSlim();
        var e4 = new ManualResetEventSlim();

        // Force task2 to execute in the background
        var task1 = Task.Run(() =>
        {
            context.Send((_) =>
            {
                e1.Set();
                Assert.True(e2.Wait(Timeout), "timeout");
            }, null);
        });

        Assert.True(e1.Wait(Timeout), "timeout");

        // Act
        //
        // Dispatch this on the background thread because otherwise it would block.
        var task2 = Task.Run(() =>
        {
            e3.Set();
            context.Send((_) =>
            {
                e4.Set();
            }, null);
        });

        // Assert
        Assert.True(e3.Wait(Timeout), "timeout");
        Assert.True(e3.IsSet);

        // Unblock the first item
        e2.Set();
        await task1;

        await task2;
        Assert.True(e4.IsSet);
    }

    [Fact]
    public void Send_CapturesContext()
    {
        // Arrange
        var context = new RendererSynchronizationContext();

        var e1 = new ManualResetEventSlim();

        // CultureInfo uses the execution context.
        CultureInfo.CurrentCulture = new CultureInfo("en-GB");
        CultureInfo capturedCulture = null;

        SynchronizationContext capturedContext = null;

        // Act
        context.Send(async (_) =>
        {
            await Task.Yield();

            capturedCulture = CultureInfo.CurrentCulture;
            capturedContext = SynchronizationContext.Current;

            e1.Set();
        }, null);

        // Assert
        Assert.True(e1.Wait(Timeout), "timeout");
        Assert.Same(CultureInfo.CurrentCulture, capturedCulture);
        Assert.Same(context, capturedContext);
    }

    [Fact]
    public async Task InvokeAsync_Action_CanRunSynchronously_WhenNotBusy()
    {
        // Arrange
        var context = new RendererSynchronizationContext();
        var thread = Thread.CurrentThread;
        Thread capturedThread = null;

        // Act
        var task = context.InvokeAsync(() =>
        {
            capturedThread = Thread.CurrentThread;
        });

        // Assert
        await task;
        Assert.Same(thread, capturedThread);
    }

    [Fact]
    public async Task InvokeAsync_Action_CanRunAsynchronously_WhenBusy()
    {
        // Arrange
        var context = new RendererSynchronizationContext();
        var thread = Thread.CurrentThread;
        Thread capturedThread = null;

        var e1 = new ManualResetEventSlim();
        var e2 = new ManualResetEventSlim();
        var e3 = new ManualResetEventSlim();

        var task1 = Task.Run(() =>
        {
            context.Send((_) =>
            {
                e1.Set();
                Assert.True(e2.Wait(Timeout), "timeout");
            }, null);
        });

        Assert.True(e1.Wait(Timeout), "timeout");

        var task2 = context.InvokeAsync(() =>
        {
            capturedThread = Thread.CurrentThread;

            e3.Set();
        });

        // Assert
        Assert.False(e2.IsSet);
        e2.Set(); // Unblock the first item
        await task1;

        Assert.True(e3.Wait(Timeout), "timeout");
        await task2;
        Assert.NotSame(thread, capturedThread);
    }

    [Fact]
    public async Task InvokeAsync_Action_CanRethrowExceptions()
    {
        // Arrange
        var context = new RendererSynchronizationContext();

        // Act
        var task = context.InvokeAsync((Action)(() =>
        {
            throw new InvalidTimeZoneException();
        }));

        // Assert
        await Assert.ThrowsAsync<InvalidTimeZoneException>(async () => await task);
    }

    [Fact]
    public async Task InvokeAsync_Action_CanReportCancellation()
    {
        // Arrange
        var context = new RendererSynchronizationContext();

        // Act
        var task = context.InvokeAsync((Action)(() =>
        {
            throw new OperationCanceledException();
        }));

        // Assert
        Assert.Equal(TaskStatus.Canceled, task.Status);
        await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await task);
    }

    [Fact]
    public async Task InvokeAsync_FuncT_CanRunSynchronously_WhenNotBusy()
    {
        // Arrange
        var context = new RendererSynchronizationContext();
        var thread = Thread.CurrentThread;

        // Act
        var task = context.InvokeAsync(() =>
        {
            return Thread.CurrentThread;
        });

        // Assert
        Assert.Same(thread, await task);
    }

    [Fact]
    public async Task InvokeAsync_FuncT_CanRunAsynchronously_WhenBusy()
    {
        // Arrange
        var context = new RendererSynchronizationContext();
        var thread = Thread.CurrentThread;

        var e1 = new ManualResetEventSlim();
        var e2 = new ManualResetEventSlim();
        var e3 = new ManualResetEventSlim();

        var task1 = Task.Run(() =>
        {
            context.Send((_) =>
            {
                e1.Set();
                Assert.True(e2.Wait(Timeout), "timeout");
            }, null);
        });

        Assert.True(e1.Wait(Timeout), "timeout");

        var task2 = context.InvokeAsync(() =>
        {
            e3.Set();

            return Thread.CurrentThread;
        });

        // Assert
        Assert.False(e2.IsSet);
        e2.Set(); // Unblock the first item
        await task1;

        Assert.True(e3.Wait(Timeout), "timeout");
        Assert.NotSame(thread, await task2);
    }

    [Fact]
    public async Task InvokeAsync_FuncT_CanRethrowExceptions()
    {
        // Arrange
        var context = new RendererSynchronizationContext();

        // Act
        var task = context.InvokeAsync<string>((Func<string>)(() =>
        {
            throw new InvalidTimeZoneException();
        }));

        // Assert
        await Assert.ThrowsAsync<InvalidTimeZoneException>(async () => await task);
    }

    [Fact]
    public async Task InvokeAsync_FuncT_CanReportCancellation()
    {
        // Arrange
        var context = new RendererSynchronizationContext();

        // Act
        var task = context.InvokeAsync<string>((Func<string>)(() =>
        {
            throw new OperationCanceledException();
        }));

        // Assert
        Assert.Equal(TaskStatus.Canceled, task.Status);
        await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await task);
    }

    [Fact]
    public async Task InvokeAsync_FuncTask_CanRunSynchronously_WhenNotBusy()
    {
        // Arrange
        var context = new RendererSynchronizationContext();
        var thread = Thread.CurrentThread;
        Thread capturedThread = null;

        // Act
        var task = context.InvokeAsync(() =>
        {
            capturedThread = Thread.CurrentThread;
            return Task.CompletedTask;
        });

        // Assert
        await task;
        Assert.Same(thread, capturedThread);
    }

    [Fact]
    public async Task InvokeAsync_FuncTask_CanRunAsynchronously_WhenBusy()
    {
        // Arrange
        var context = new RendererSynchronizationContext();
        var thread = Thread.CurrentThread;
        Thread capturedThread = null;

        var e1 = new ManualResetEventSlim();
        var e2 = new ManualResetEventSlim();
        var e3 = new ManualResetEventSlim();

        var task1 = Task.Run(() =>
        {
            context.Send((_) =>
            {
                e1.Set();
                Assert.True(e2.Wait(Timeout), "timeout");
            }, null);
        });

        Assert.True(e1.Wait(Timeout), "timeout");

        var task2 = context.InvokeAsync(() =>
        {
            capturedThread = Thread.CurrentThread;

            e3.Set();
            return Task.CompletedTask;
        });

        // Assert
        Assert.False(e2.IsSet);
        e2.Set(); // Unblock the first item
        await task1;

        Assert.True(e3.Wait(Timeout), "timeout");
        await task2;
        Assert.NotSame(thread, capturedThread);
    }

    [Fact]
    public async Task InvokeAsync_FuncTask_CanRethrowExceptions()
    {
        // Arrange
        var context = new RendererSynchronizationContext();

        // Act
        var task = context.InvokeAsync(() =>
        {
            throw new InvalidTimeZoneException();
        });

        // Assert
        await Assert.ThrowsAsync<InvalidTimeZoneException>(async () => await task);
    }

    [Fact]
    public async Task InvokeAsync_FuncTask_CanReportCancellation()
    {
        // Arrange
        var context = new RendererSynchronizationContext();

        // Act
        var task = context.InvokeAsync(() =>
        {
            throw new OperationCanceledException();
        });

        // Assert
        Assert.Equal(TaskStatus.Canceled, task.Status);
        await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await task);
    }

    [Fact]
    public async Task InvokeAsync_FuncTaskT_CanRunSynchronously_WhenNotBusy()
    {
        // Arrange
        var context = new RendererSynchronizationContext();
        var thread = Thread.CurrentThread;

        // Act
        var task = context.InvokeAsync(() =>
        {
            return Task.FromResult(Thread.CurrentThread);
        });

        // Assert
        Assert.Same(thread, await task);
    }

    [Fact]
    public async Task InvokeAsync_FuncTaskT_CanRunAsynchronously_WhenBusy()
    {
        // Arrange
        var context = new RendererSynchronizationContext();
        var thread = Thread.CurrentThread;

        var e1 = new ManualResetEventSlim();
        var e2 = new ManualResetEventSlim();
        var e3 = new ManualResetEventSlim();

        var task1 = Task.Run(() =>
        {
            context.Send((_) =>
            {
                e1.Set();
                Assert.True(e2.Wait(Timeout), "timeout");
            }, null);
        });

        Assert.True(e1.Wait(Timeout), "timeout");

        var task2 = context.InvokeAsync(() =>
        {
            e3.Set();

            return Task.FromResult(Thread.CurrentThread);
        });

        // Assert
        Assert.False(e2.IsSet);
        e2.Set(); // Unblock the first item
        await task1;

        Assert.True(e3.Wait(Timeout), "timeout");
        Assert.NotSame(thread, await task2);
    }

    [Fact]
    public async Task InvokeAsync_FuncTaskT_CanRethrowExceptions()
    {
        // Arrange
        var context = new RendererSynchronizationContext();

        // Act
        var task = context.InvokeAsync<string>((Func<Task<string>>)(() =>
        {
            throw new InvalidTimeZoneException();
        }));

        // Assert
        await Assert.ThrowsAsync<InvalidTimeZoneException>(async () => await task);
    }

    [Fact]
    public async Task InvokeAsync_FuncTaskT_CanReportCancellation()
    {
        // Arrange
        var context = new RendererSynchronizationContext();

        // Act
        var task = context.InvokeAsync<string>((Func<Task<string>>)(() =>
        {
            throw new OperationCanceledException();
        }));

        // Assert
        Assert.Equal(TaskStatus.Canceled, task.Status);
        await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await task);
    }

    [Fact]
    public async Task InvokeAsync_SyncWorkInAsyncTaskIsCompletedFirst()
    {
        // Simplified version of ServerComponentRenderingTest.CanDispatchAsyncWorkToSyncContext
        var expected = "First Second Third Fourth Fifth";
        var context = new RendererSynchronizationContext();
        string actual;

        // Act
        await Task.Yield();
        actual = "First";

        var invokeTask = context.InvokeAsync(async () =>
        {
            // When the sync context is idle, queued work items start synchronously
            actual += " Second";
            await Task.Delay(250);
            actual += " Fourth";
        });

        actual += " Third";
        await invokeTask;
        actual += " Fifth";

        // Assert
        Assert.Equal(expected, actual);
    }
}
