using System.Reflection;
using System.Security.Claims;
using Bunit;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NSubstitute;
using ScadaLink.CentralUI.Auth;
using ScadaLink.Commons.Entities.Sites;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Communication;
using ScadaLink.Communication.Grpc;
using DebugViewPage = ScadaLink.CentralUI.Components.Pages.Deployment.DebugView;

namespace ScadaLink.CentralUI.Tests.Deployment;

/// <summary>
/// Regression tests for CentralUI-009. The <c>DebugView</c> stream callbacks
/// (<c>onEvent</c>/<c>onTerminated</c>) run on an Akka/gRPC thread and capture
/// <c>this</c> and <c>_toast</c>. If the user navigates away, an in-flight
/// callback could still call <c>_toast.ShowError(...)</c> /
/// <c>InvokeAsync(StateHasChanged)</c> on a disposed component. The fix adds a
/// <c>_disposed</c> flag checked at the top of every callback, set in
/// <c>Dispose()</c> before the stream is stopped.
/// <para>
/// The Akka-thread timing race itself is not deterministically reproducible in
/// a unit test (<see cref="DebugStreamService"/> is a non-virtual concrete
/// class with no seam to inject and later fire the callbacks). These tests pin
/// the observable parts of the fix: the component exposes a disposal guard, and
/// disposal is clean and idempotent.
/// </para>
/// </summary>
public class DebugViewDisposalTests : BunitContext
{
    private void RegisterServices()
    {
        // DebugView touches localStorage on render; let bUnit answer loosely.
        JSInterop.Mode = JSRuntimeMode.Loose;

        var repo = Substitute.For<ITemplateEngineRepository>();
        var siteRepo = Substitute.For<ISiteRepository>();
        siteRepo.GetAllSitesAsync().Returns(new List<Site>());
        Services.AddSingleton(repo);
        Services.AddSingleton(siteRepo);

        var comms = new CommunicationService(
            Options.Create(new CommunicationOptions()),
            NullLogger<CommunicationService>.Instance);
        Services.AddSingleton(comms);

        var grpcFactory = new SiteStreamGrpcClientFactory(NullLoggerFactory.Instance);
        var debugStream = new DebugStreamService(
            comms, Services.BuildServiceProvider(), grpcFactory,
            NullLogger<DebugStreamService>.Instance);
        Services.AddSingleton(debugStream);

        var identity = new ClaimsIdentity(
            new[] { new Claim(ClaimTypes.Name, "deployer") }, "TestCookie");
        var authState = new AuthenticationState(new ClaimsPrincipal(identity));
        var stubAuth = new StubAuthStateProvider(authState);
        Services.AddSingleton<AuthenticationStateProvider>(stubAuth);
        Services.AddScoped(_ => new SiteScopeService(stubAuth));
    }

    private sealed class StubAuthStateProvider : AuthenticationStateProvider
    {
        private readonly AuthenticationState _state;
        public StubAuthStateProvider(AuthenticationState state) => _state = state;
        public override Task<AuthenticationState> GetAuthenticationStateAsync()
            => Task.FromResult(_state);
    }

    [Fact]
    public void DebugView_HasDisposalGuardField()
    {
        // The fix introduces a `_disposed` flag that every stream callback
        // checks before touching component state.
        var field = typeof(DebugViewPage).GetField(
            "_disposed", BindingFlags.Instance | BindingFlags.NonPublic);

        Assert.NotNull(field);
        Assert.Equal(typeof(bool), field!.FieldType);
    }

    [Fact]
    public void DebugView_Dispose_SetsDisposedFlag_AndIsIdempotent()
    {
        RegisterServices();
        var cut = Render<DebugViewPage>();

        var field = typeof(DebugViewPage).GetField(
            "_disposed", BindingFlags.Instance | BindingFlags.NonPublic)!;
        Assert.False((bool)field.GetValue(cut.Instance)!);

        cut.Instance.Dispose();
        Assert.True((bool)field.GetValue(cut.Instance)!,
            "Dispose() must set the guard so in-flight callbacks no-op.");

        // Disposing again must not throw (idempotent).
        var ex = Record.Exception(() => cut.Instance.Dispose());
        Assert.Null(ex);
    }
}
