using Bunit;
using ScadaLink.CentralUI.Components.Shared;

namespace ScadaLink.CentralUI.Tests.Shared;

/// <summary>
/// Regression tests for CentralUI-010. <c>ToastNotification.AddToast</c>
/// scheduled <c>Task.Delay(dismissMs).ContinueWith(...)</c> with the result
/// discarded; the continuation called <c>InvokeAsync(StateHasChanged)</c>. When
/// the host page is disposed before the delay elapses, the continuation ran
/// against a disposed component and threw <c>ObjectDisposedException</c> on a
/// thread-pool thread with no catch (an unobserved task exception). The fix
/// holds a <c>CancellationTokenSource</c> cancelled in <c>Dispose()</c>.
/// </summary>
public class ToastNotificationTests : BunitContext
{
    [Fact]
    public async Task ShowToast_AfterDisposal_IsNoOp_AndSchedulesNothing()
    {
        // Regression: the pre-fix AddToast always added the toast and scheduled
        // a Task.Delay continuation, even after Dispose() — the continuation
        // then ran InvokeAsync(StateHasChanged) against the disposed component.
        // The fix short-circuits AddToast once the disposal token is cancelled.
        var cut = Render<ToastNotification>();

        await cut.InvokeAsync(() => cut.Instance.Dispose());
        await cut.InvokeAsync(() => cut.Instance.ShowError("after dispose", autoDismissMs: 20));

        Assert.Equal(0, cut.Instance.ToastCount);
    }

    [Fact]
    public async Task AutoDismiss_AfterDisposal_DoesNotThrowUnobservedException()
    {
        var unobserved = new List<Exception>();
        void Handler(object? s, UnobservedTaskExceptionEventArgs e)
        {
            unobserved.Add(e.Exception);
            e.SetObserved();
        }
        TaskScheduler.UnobservedTaskException += Handler;
        try
        {
            var cut = Render<ToastNotification>();
            // Auto-dismiss after a very short delay so the continuation is
            // guaranteed to fire well after we dispose the component.
            await cut.InvokeAsync(() => cut.Instance.ShowSuccess("hello", autoDismissMs: 20));

            // Dispose the component while the auto-dismiss is still pending.
            await cut.InvokeAsync(() => cut.Instance.Dispose());

            // Give the (now-cancelled) auto-dismiss well past its delay.
            await Task.Delay(250);
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        }
        finally
        {
            TaskScheduler.UnobservedTaskException -= Handler;
        }

        Assert.Empty(unobserved);
    }

    [Fact]
    public async Task AutoDismiss_BeforeDisposal_StillRemovesToast()
    {
        var cut = Render<ToastNotification>();
        await cut.InvokeAsync(() => cut.Instance.ShowInfo("transient", autoDismissMs: 20));

        // The toast is visible immediately.
        Assert.Contains("transient", cut.Markup);

        // After the dismiss delay it is removed (auto-dismiss still works).
        cut.WaitForAssertion(
            () => Assert.DoesNotContain("transient", cut.Markup),
            timeout: TimeSpan.FromSeconds(2));
    }
}
