using System.Reflection;
using System.Security.Claims;
using Bunit;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Deployment;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.DeploymentManager;
using DeploymentsPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment.Deployments;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;

/// <summary>
/// Regression tests for CentralUI-006. Component-CentralUI "Real-Time Updates"
/// states deployment status transitions push to the UI immediately via SignalR
/// with no polling. The page previously ran a 10-second <c>Timer</c> that
/// reloaded every deployment record + instance map per tick. The fix removes
/// the timer and subscribes to <see cref="IDeploymentStatusNotifier"/>, which
/// <c>DeploymentService</c> raises on every deployment-record status write;
/// Blazor Server then pushes the re-render over its SignalR circuit.
///
/// <para>
/// The "did it reload?" assertions moved from <c>GetAllDeploymentRecordsAsync</c>
/// to <c>QueryDeploymentListPageAsync</c> when the page's read path went
/// server-paged (residual R3) — the push mechanism under test is unchanged, only
/// the query it drives.
/// </para>
/// </summary>
public class DeploymentsPushUpdateTests : BunitContext
{
    private IDeploymentManagerRepository _deployRepo = null!;
    private DeploymentStatusNotifier _notifier = null!;

    private void RegisterServices()
    {
        _deployRepo = Substitute.For<IDeploymentManagerRepository>();
        _notifier = new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance);

        _deployRepo.QueryDeploymentListPageAsync(
                Arg.Any<DeploymentListFilter>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns(DeploymentListPage.Empty);
        _deployRepo.GetDeploymentStatusCountsAsync(
                Arg.Any<DeploymentListFilter>(), Arg.Any<CancellationToken>())
            .Returns(DeploymentStatusCounts.Empty);

        Services.AddSingleton(_deployRepo);
        Services.AddSingleton<IDeploymentStatusNotifier>(_notifier);

        var identity = new ClaimsIdentity(
            new[] { new Claim(ClaimTypes.Name, "deployer") }, "TestCookie");
        var stubAuth = new StubAuthStateProvider(
            new AuthenticationState(new ClaimsPrincipal(identity)));
        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 Deployments_DoesNotPoll_HasNoRefreshTimer()
    {
        // The 10-second polling Timer must be gone — push replaces polling.
        var timerField = typeof(DeploymentsPage).GetField(
            "_refreshTimer", BindingFlags.Instance | BindingFlags.NonPublic);

        Assert.Null(timerField);
    }

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

        // Initial load: the paged query is issued once.
        _deployRepo.ClearReceivedCalls();

        // A deployment status write in DeploymentManager raises the notifier;
        // the page must reload in response (no polling timer involved).
        _notifier.NotifyStatusChanged(
            new DeploymentStatusChange("dep-1", 1, DeploymentStatus.Success));

        cut.WaitForAssertion(() =>
            _deployRepo.Received().QueryDeploymentListPageAsync(
                Arg.Any<DeploymentListFilter>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>()));
    }

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

        cut.Instance.Dispose();
        _deployRepo.ClearReceivedCalls();

        // After disposal, a status change must NOT touch the disposed component.
        _notifier.NotifyStatusChanged(
            new DeploymentStatusChange("dep-2", 1, DeploymentStatus.Failed));

        _deployRepo.DidNotReceive().QueryDeploymentListPageAsync(
            Arg.Any<DeploymentListFilter>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>());
    }

    /// <summary>
    /// Regression test for CentralUI-022. The notifier is a process singleton:
    /// it can read its subscriber list and begin invoking
    /// <c>OnDeploymentStatusChanged</c> on the DeploymentManager thread an
    /// instant before the component is disposed. The handler must no-op against
    /// a disposed component rather than letting <c>InvokeAsync</c> throw an
    /// unobserved <see cref="ObjectDisposedException"/>.
    /// </summary>
    [Fact]
    public void Deployments_HasDisposalGuardField()
    {
        var field = typeof(DeploymentsPage).GetField(
            "_disposed", BindingFlags.Instance | BindingFlags.NonPublic);

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

    [Fact]
    public void Deployments_StatusChangeAfterDispose_DoesNotThrowOrReload()
    {
        RegisterServices();
        var cut = Render<DeploymentsPage>();
        var component = cut.Instance;

        component.Dispose();
        _deployRepo.ClearReceivedCalls();

        // Simulate the race: the notifier captured the handler before the
        // Dispose() unsubscribe and invokes it directly against the now-disposed
        // component. Pre-fix this dispatched InvokeAsync against a dead circuit
        // and threw ObjectDisposedException on a fire-and-forget task.
        var handler = typeof(DeploymentsPage).GetMethod(
            "OnDeploymentStatusChanged", BindingFlags.Instance | BindingFlags.NonPublic)!;

        var ex = Record.Exception(() => handler.Invoke(component,
            new object[] { new DeploymentStatusChange("dep-9", 1, DeploymentStatus.Success) }));

        Assert.Null(ex);
        // The guard short-circuits before any reload is attempted.
        _deployRepo.DidNotReceive().QueryDeploymentListPageAsync(
            Arg.Any<DeploymentListFilter>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>());
    }
}
