using System.Security.Claims;
using ZB.MOM.WW.ScadaBridge.Security;
using Akka.Actor;
using Bunit;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
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.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Notification;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Communication;
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
using HealthPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Monitoring.Health;

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

/// <summary>
/// bUnit rendering tests for the Health Monitoring dashboard (Task 24).
///
/// Scope: the Notification Outbox KPI tile row added to the Health dashboard.
/// <see cref="ICentralHealthAggregator"/> is an interface (mockable), but
/// <see cref="CommunicationService"/> is a concrete class whose outbox calls
/// route through an injected notification-outbox <see cref="IActorRef"/>; the
/// tests reuse the scripted-actor seam established by the Notification Report
/// page tests (see <c>NotificationReportPageTests</c>).
/// </summary>
public class HealthPageTests : BunitContext
{
    private readonly ActorSystem _system = ActorSystem.Create("health-page-tests");
    private readonly CommunicationService _comms;

    // Mutable scripted reply — individual tests can override before rendering.
    private NotificationKpiResponse _kpiReply =
        new("k", true, null, QueueDepth: 12, StuckCount: 4, ParkedCount: 3,
            DeliveredLastInterval: 88, OldestPendingAge: TimeSpan.FromMinutes(6));

    // Site Call Audit (#22) Task 7 — mutable scripted Site Call KPI reply. Tests
    // that target the Site Call tiles override this before rendering.
    private SiteCallKpiResponse _siteCallKpiReply =
        new("k", true, null, BufferedCount: 9, ParkedCount: 2, FailedLastInterval: 1,
            DeliveredLastInterval: 40, OldestPendingAge: TimeSpan.FromMinutes(3),
            StuckCount: 5);

    public HealthPageTests()
    {
        _comms = new CommunicationService(
            Options.Create(new CommunicationOptions()),
            NullLogger<CommunicationService>.Instance);

        var outbox = _system.ActorOf(Props.Create(() => new ScriptedOutboxActor(this)));
        _comms.SetNotificationOutbox(outbox);

        var siteCallAudit = _system.ActorOf(Props.Create(() => new ScriptedSiteCallAuditActor(this)));
        _comms.SetSiteCallAudit(siteCallAudit);
        Services.AddSingleton(_comms);

        var aggregator = Substitute.For<ICentralHealthAggregator>();
        aggregator.GetAllSiteStates()
            .Returns(new Dictionary<string, SiteHealthState>());
        Services.AddSingleton(aggregator);

        var siteRepo = Substitute.For<ISiteRepository>();
        siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<Site>>(new List<Site>()));
        Services.AddSingleton(siteRepo);

        // Audit Log (#23) M7 Bundle E — the Health page now also fetches the
        // Audit KPI snapshot. Stub it with an empty point-in-time reading so
        // the existing assertions (Notification Outbox tiles, Online/Offline
        // counts) keep passing; tests that target the Audit tiles set their
        // own substitute.
        var auditService = Substitute.For<IAuditLogQueryService>();
        auditService.GetKpiSnapshotAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult(new AuditLogKpiSnapshot(0, 0, 0, DateTime.UtcNow)));
        Services.AddSingleton(auditService);

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

    [Fact]
    public void Renders_OutboxKpiTiles_WithValues()
    {
        var cut = Render<HealthPage>();

        // KPI data arrives via an async actor Ask after first render.
        cut.WaitForAssertion(() =>
        {
            Assert.Contains("Notification Outbox", cut.Markup);
            Assert.Contains("Queue Depth", cut.Markup);
            Assert.Contains("Stuck", cut.Markup);
            Assert.Contains("Parked", cut.Markup);
            // KPI numeric values surface in the tiles.
            Assert.Contains(">12<", cut.Markup);  // QueueDepth
            Assert.Contains(">4<", cut.Markup);   // StuckCount
            Assert.Contains(">3<", cut.Markup);   // ParkedCount
        });
    }

    [Fact]
    public void RendersLinkToTheNotificationKpisPage()
    {
        var cut = Render<HealthPage>();
        var link = cut.Find("a[href='/notifications/kpis']");
        Assert.Contains("View details", link.TextContent);
    }

    [Fact]
    public void Renders_AuditKpiTiles_WithValues()
    {
        // Override the default empty snapshot — this test wants concrete values
        // to land in the three Audit tiles.
        var auditService = Substitute.For<IAuditLogQueryService>();
        auditService.GetKpiSnapshotAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult(new AuditLogKpiSnapshot(
                TotalEventsLastHour: 250,
                ErrorEventsLastHour: 5,
                BacklogTotal: 17,
                AsOfUtc: DateTime.UtcNow)));
        Services.AddSingleton(auditService);

        var cut = Render<HealthPage>();

        cut.WaitForAssertion(() =>
        {
            // The three audit tiles render at the documented data-test selectors.
            Assert.Contains("data-test=\"audit-kpi-volume\"", cut.Markup);
            Assert.Contains("data-test=\"audit-kpi-error-rate\"", cut.Markup);
            Assert.Contains("data-test=\"audit-kpi-backlog\"", cut.Markup);
            // Volume shows the formatted thousand-separator value.
            Assert.Contains("250", cut.Markup);
            // Backlog renders 17.
            Assert.Contains("17", cut.Markup);
        });
    }

    [Fact]
    public void Renders_SiteCallKpiTiles_WithValues()
    {
        var cut = Render<HealthPage>();

        // KPI data arrives via an async actor Ask after first render.
        cut.WaitForAssertion(() =>
        {
            Assert.Contains("Site Calls", cut.Markup);
            // The three Site Call tiles render at the documented data-test selectors.
            Assert.Contains("data-test=\"site-call-kpi-buffered\"", cut.Markup);
            Assert.Contains("data-test=\"site-call-kpi-stuck\"", cut.Markup);
            Assert.Contains("data-test=\"site-call-kpi-parked\"", cut.Markup);
            // KPI numeric values surface in the tiles.
            Assert.Contains(">9<", cut.Markup);  // BufferedCount
            Assert.Contains(">5<", cut.Markup);  // StuckCount
            Assert.Contains(">2<", cut.Markup);  // ParkedCount
        });
    }

    [Fact]
    public void RendersLinkToTheSiteCallsReportPage()
    {
        var cut = Render<HealthPage>();
        var link = cut.Find("a[href='/site-calls/report']");
        Assert.Contains("View details", link.TextContent);
    }

    [Fact]
    public void SiteCallKpiFailure_ShowsGracefulFallback()
    {
        _siteCallKpiReply = new SiteCallKpiResponse(
            "k", false, "site call repository unavailable", 0, 0, 0, 0, null, 0);

        var cut = Render<HealthPage>();

        cut.WaitForAssertion(() =>
        {
            // Failure must not crash the page; tiles fall back to a dash and the
            // inline error message surfaces.
            Assert.Contains("Site Calls", cut.Markup);
            Assert.Contains("Site Call KPIs unavailable", cut.Markup);
            Assert.Contains("site call repository unavailable", cut.Markup);
            Assert.Contains(">—<", cut.Markup);
        });
    }

    [Fact]
    public void OutboxKpiFailure_ShowsGracefulFallback()
    {
        _kpiReply = new NotificationKpiResponse(
            "k", false, "outbox repository unavailable", 0, 0, 0, 0, null);

        var cut = Render<HealthPage>();

        cut.WaitForAssertion(() =>
        {
            // Failure must not crash the page; tiles fall back to a dash.
            Assert.Contains("Notification Outbox", cut.Markup);
            Assert.Contains("Queue Depth", cut.Markup);
            Assert.Contains(">—<", cut.Markup);
        });
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _system.Terminate().Wait(TimeSpan.FromSeconds(5));
        }
        base.Dispose(disposing);
    }

    /// <summary>
    /// Stand-in for the notification-outbox actor. Replies to the KPI request
    /// with the test's currently-scripted response.
    /// </summary>
    private sealed class ScriptedOutboxActor : ReceiveActor
    {
        public ScriptedOutboxActor(HealthPageTests test)
        {
            Receive<NotificationKpiRequest>(_ => Sender.Tell(test._kpiReply));
        }
    }

    /// <summary>
    /// Stand-in for the Site Call Audit actor. Replies to the KPI request with
    /// the test's currently-scripted response.
    /// </summary>
    private sealed class ScriptedSiteCallAuditActor : ReceiveActor
    {
        public ScriptedSiteCallAuditActor(HealthPageTests test)
        {
            Receive<SiteCallKpiRequest>(_ => Sender.Tell(test._siteCallKpiReply));
        }
    }
}
