using System.Security.Claims;
using Bunit;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Communication;
using ZB.MOM.WW.ScadaBridge.Security;
using AlarmSummaryPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Monitoring.AlarmSummary;

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

/// <summary>
/// Covers the Alarm Summary flat table's two rendering paths (arch-review WP2.4). A short
/// list renders every row with a plain foreach; past the threshold the same row template is
/// handed to <c>Virtualize</c> so a site with thousands of alarms stops materialising the
/// whole table into the circuit's render tree on every delta. Row markup — including the
/// <c>data-test</c> hook the operator tests key off — is identical either way.
/// </summary>
public class AlarmSummaryVirtualizeTests : BunitContext
{
    private readonly IAlarmSummaryService _summary = Substitute.For<IAlarmSummaryService>();
    private readonly ISiteRepository _siteRepo = Substitute.For<ISiteRepository>();

    private void Arrange(int rowCount)
    {
        JSInterop.Mode = JSRuntimeMode.Loose;

        var rows = Enumerable.Range(0, rowCount)
            .Select(i => new AlarmSummaryRow(
                $"inst-{i:D4}",
                new AlarmStateChanged($"inst-{i:D4}", $"alarm-{i:D4}", AlarmState.Active, i, DateTimeOffset.UtcNow)))
            .ToList();

        _summary.GetSiteAlarmsAsync(Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns(Task.FromResult(new AlarmSummaryResult(rows, Array.Empty<string>())));
        _summary.ComputeRollup(Arg.Any<IReadOnlyList<AlarmSummaryRow>>())
            .Returns(new AlarmRollup(rowCount, rowCount, 0, new Dictionary<AlarmKind, int>()));
        Services.AddSingleton(_summary);

        _siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<Site>>(new List<Site>
            {
                new("Site 1", "site1") { Id = 1 },
            }));
        Services.AddSingleton(_siteRepo);
        Services.AddSingleton<ISiteAlarmLiveCache>(new InertLiveCache());

        var claims = new[]
        {
            new Claim(JwtTokenService.UsernameClaimType, "tester"),
            new Claim(JwtTokenService.RoleClaimType, "Administrator"),
        };
        Services.AddSingleton<AuthenticationStateProvider>(
            new TestAuthStateProvider(new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"))));
        Services.AddAuthorizationCore();
    }

    private IRenderedComponent<AlarmSummaryPage> RenderWithSiteSelected()
    {
        var cut = Render<AlarmSummaryPage>();
        cut.Find("[data-test='alarm-summary-site']").Change("1");
        return cut;
    }

    [Fact]
    public void ShortList_RendersEveryRowInline()
    {
        Arrange(rowCount: 20);

        var cut = RenderWithSiteSelected();

        cut.WaitForAssertion(() =>
            Assert.Equal(20, cut.FindAll("tr[data-test='alarm-summary-row']").Count));
        // The row-count line is unchanged by either path.
        Assert.Contains("Showing 20 of 20", cut.Markup);
    }

    [Fact]
    public void LongList_VirtualizesWithoutRenderingEveryRow()
    {
        Arrange(rowCount: 2000);

        var cut = RenderWithSiteSelected();

        cut.WaitForAssertion(() => Assert.Contains("Showing 2000 of 2000", cut.Markup));

        var rendered = cut.FindAll("tr[data-test='alarm-summary-row']").Count;
        Assert.InRange(rendered, 1, 1999);
        // Rows still carry the same shape — instance name, alarm name, severity.
        Assert.Contains("alarm-", cut.Markup);
    }

    /// <summary>Never goes live, so the page keeps its poll snapshot for these tests.</summary>
    private sealed class InertLiveCache : ISiteAlarmLiveCache
    {
        public IDisposable Subscribe(int siteId, Action onChanged) => new NoOp();

        public IReadOnlyList<AlarmStateChanged> GetCurrentAlarms(int siteId) =>
            Array.Empty<AlarmStateChanged>();

        public bool IsLive(int siteId) => false;

        public IReadOnlyList<string> GetNotReportingInstances(int siteId) => Array.Empty<string>();

        private sealed class NoOp : IDisposable
        {
            public void Dispose() { }
        }
    }
}
