using System.Security.Claims;
using Bunit;
using Bunit.TestDoubles;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
using ZB.MOM.WW.ScadaBridge.Security;
using AuditLogPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Audit.AuditLogPage;

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

/// <summary>
/// Tests for the M6 (K15) Trends panel added to the Audit Log page — one
/// <c>KpiTrendChart</c> per AuditLog global metric, over a 24h/7d window. The
/// panel is best-effort: a failing <see cref="IKpiHistoryQueryService"/> must
/// degrade the affected charts without breaking the audit query UI (the filter
/// bar + results grid that share the page).
///
/// <para>
/// bUnit hosts the page directly, so the test registers every service the page
/// and its child components inject: <see cref="AuthenticationStateProvider"/> +
/// the real authorization policies (the page carries
/// <c>[Authorize(OperationalAudit)]</c> and an in-page <c>AuthorizeView</c> for
/// the Export button), <see cref="ISiteRepository"/> (AuditFilterBar),
/// <see cref="IAuditLogQueryService"/> (AuditResultsGrid), and the new
/// <see cref="IKpiHistoryQueryService"/> (the Trends panel). The page is wrapped
/// in a <see cref="CascadingAuthenticationState"/> the router would supply in
/// production.
/// </para>
/// </summary>
public class AuditLogPageTrendTests : BunitContext
{
    private static readonly DateTime Base = new(2026, 6, 15, 10, 0, 0, DateTimeKind.Utc);

    public AuditLogPageTrendTests()
    {
        // AuditResultsGrid's OnAfterRenderAsync wires its column resize/reorder UX
        // through audit-grid.js. Loose mode lets those unconfigured JS calls no-op
        // so these trend tests need not configure browser interop.
        JSInterop.Mode = JSRuntimeMode.Loose;
    }

    private static ClaimsPrincipal BuildPrincipal(params string[] roles)
    {
        var claims = new List<Claim> { new(JwtTokenService.UsernameClaimType, "tester") };
        claims.AddRange(roles.Select(r => new Claim(JwtTokenService.RoleClaimType, r)));
        return new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
    }

    private static IReadOnlyList<KpiSeriesPoint> ThreePoints() => new[]
    {
        new KpiSeriesPoint(Base, 1.0),
        new KpiSeriesPoint(Base.AddMinutes(20), 4.0),
        new KpiSeriesPoint(Base.AddMinutes(40), 2.0),
    };

    private IRenderedComponent<AuditLogPage> RenderAuditLogPage(IKpiHistoryQueryService kpiHistory)
    {
        var user = BuildPrincipal("Administrator");
        Services.AddSingleton<AuthenticationStateProvider>(new TestAuthStateProvider(user));
        Services.AddAuthorizationCore();
        AuthorizationPolicies.AddScadaBridgeAuthorization(Services);
        Services.AddSingleton<IAuthorizationService, DefaultAuthorizationService>();

        // The page hosts AuditFilterBar + AuditResultsGrid (Bundle B) and the
        // K15 Trends panel — register a stand-in for each injected dependency.
        Services.AddSingleton(Substitute.For<ISiteRepository>());
        Services.AddSingleton(Substitute.For<IAuditLogQueryService>());
        Services.AddSingleton(kpiHistory);

        var host = Render<CascadingAuthenticationState>(parameters => parameters
            .Add(p => p.ChildContent, (RenderFragment)(builder =>
            {
                builder.OpenComponent<AuditLogPage>(0);
                builder.CloseComponent();
            })));

        return host.FindComponent<AuditLogPage>();
    }

    [Fact]
    public void TrendsPanel_WithSeries_RendersChartsWithPolyline()
    {
        var kpi = Substitute.For<IKpiHistoryQueryService>();
        kpi.GetSeriesAsync(
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string?>(),
                Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
            .Returns(Task.FromResult(ThreePoints()));

        var cut = RenderAuditLogPage(kpi);

        cut.WaitForAssertion(() =>
        {
            // The panel container is present...
            Assert.Contains("data-test=\"audit-trends\"", cut.Markup);
            // ...and at least one chart rendered an actual polyline series.
            Assert.Contains("kpi-trend-", cut.Markup);
            Assert.Contains("<polyline", cut.Markup);
            // The audit query UI still rendered alongside the trends panel.
            Assert.Contains("Audit Log", cut.Markup);
        });

        // All three AuditLog global metrics were queried (one chart each).
        kpi.Received().GetSeriesAsync(
            KpiSources.AuditLog, "totalEventsLastHour", KpiScopes.Global, null,
            Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<CancellationToken>());
        kpi.Received().GetSeriesAsync(
            KpiSources.AuditLog, "errorEventsLastHour", KpiScopes.Global, null,
            Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<CancellationToken>());
        kpi.Received().GetSeriesAsync(
            KpiSources.AuditLog, "backlogTotal", KpiScopes.Global, null,
            Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public void TrendsPanel_LongRangeWindows_RenderAndRefetchWithMatchingSpan()
    {
        // The 30d (720h) / 90d (2160h) buttons added for the hourly-rollup path
        // (plan #22 T7) must render alongside 24h/7d and, when clicked, re-fetch
        // every series over the corresponding range — the query service then
        // transparently routes the long windows to hourly rollups.
        var ranges = new List<(DateTime From, DateTime To)>();
        var kpi = Substitute.For<IKpiHistoryQueryService>();
        kpi.GetSeriesAsync(
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string?>(),
                Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
            .Returns(ci =>
            {
                ranges.Add((ci.ArgAt<DateTime>(4), ci.ArgAt<DateTime>(5)));
                return Task.FromResult(ThreePoints());
            });

        var cut = RenderAuditLogPage(kpi);

        // Both new window buttons render in the toggle group.
        cut.WaitForAssertion(() =>
        {
            Assert.Contains(cut.FindAll("button"), b => b.TextContent.Trim() == "30d");
            Assert.Contains(cut.FindAll("button"), b => b.TextContent.Trim() == "90d");
        });

        // Click 30d — every series re-fetches over a ~720h span.
        cut.FindAll("button").First(b => b.TextContent.Trim() == "30d").Click();
        cut.WaitForAssertion(() =>
            Assert.Contains(ranges, r => Math.Abs((r.To - r.From).TotalHours - 720) < 0.5));

        // Click 90d — every series re-fetches over a ~2160h span.
        cut.FindAll("button").First(b => b.TextContent.Trim() == "90d").Click();
        cut.WaitForAssertion(() =>
            Assert.Contains(ranges, r => Math.Abs((r.To - r.From).TotalHours - 2160) < 0.5));
    }

    [Fact]
    public void TrendsPanel_WhenQueryServiceThrows_PageStillRenders()
    {
        // Best-effort contract: a failing KPI-history query degrades the charts to
        // the unavailable placeholder but must NOT throw out of the page render.
        var kpi = Substitute.For<IKpiHistoryQueryService>();
        kpi.GetSeriesAsync(
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string?>(),
                Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
            .Returns<Task<IReadOnlyList<KpiSeriesPoint>>>(_ => throw new InvalidOperationException("kpi down"));

        var cut = RenderAuditLogPage(kpi);

        cut.WaitForAssertion(() =>
        {
            // The page rendered — heading + trends panel container both present —
            // even though every series fetch threw.
            Assert.Contains("Audit Log", cut.Markup);
            Assert.Contains("data-test=\"audit-trends\"", cut.Markup);
        });
    }
}
