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 the Deployment Status push coalescing (arch-review WP2.4). The
/// notifier fires per status write, so a multi-instance deploy produced a stampede of
/// reloads per circuit. The reload is leading-edge debounced: the first push after an
/// idle gap is still immediate, and a burst behind it collapses into one trailing reload.
///
/// <para>
/// Server-side paging (residual R3) shrank what one reload costs but not how many
/// arrive, so the coalescing still has to hold. The assertions now count
/// <c>QueryDeploymentListPageAsync</c> calls instead of the deleted
/// <c>GetAllDeploymentRecordsAsync</c>.
/// </para>
/// </summary>
public class DeploymentsReloadDebounceTests : 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);
    }

    private void AssertReloaded(IRenderedComponent<DeploymentsPage> cut) =>
        cut.WaitForAssertion(() =>
            _deployRepo.Received().QueryDeploymentListPageAsync(
                Arg.Any<DeploymentListFilter>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>()));

    [Fact]
    public void BurstOfStatusWrites_CollapsesIntoFarFewerReloads()
    {
        RegisterServices();
        var cut = Render<DeploymentsPage>();
        AssertReloaded(cut);
        _deployRepo.ClearReceivedCalls();

        // A 40-instance site deploy: every status write raises the notifier.
        for (var i = 0; i < 40; i++)
        {
            _notifier.NotifyStatusChanged(
                new DeploymentStatusChange($"dep-{i}", 1, DeploymentStatus.InProgress));
        }

        // Leading edge fires at once; the rest ride one trailing reload.
        AssertReloaded(cut);

        // Let the trailing window close before counting.
        Thread.Sleep(900);

        var reloads = _deployRepo.ReceivedCalls()
            .Count(c => c.GetMethodInfo().Name == nameof(IDeploymentManagerRepository.QueryDeploymentListPageAsync));
        Assert.InRange(reloads, 1, 4);
    }

    [Fact]
    public void FirstPushAfterIdle_ReloadsImmediately()
    {
        RegisterServices();
        var cut = Render<DeploymentsPage>();
        AssertReloaded(cut);
        _deployRepo.ClearReceivedCalls();

        // Idle since the initial load — the leading edge must not wait out the window.
        Thread.Sleep(600);
        _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>()),
            TimeSpan.FromMilliseconds(400));
    }

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

        // Two pushes: the first takes the leading edge, the second arms the trailing timer.
        _notifier.NotifyStatusChanged(new DeploymentStatusChange("dep-1", 1, DeploymentStatus.InProgress));
        _notifier.NotifyStatusChanged(new DeploymentStatusChange("dep-2", 1, DeploymentStatus.InProgress));

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

        // The armed timer must be disposed with the component, not fire against it.
        Thread.Sleep(900);
        _deployRepo.DidNotReceive().QueryDeploymentListPageAsync(
            Arg.Any<DeploymentListFilter>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>());
    }
}
