using System.Security.Claims;
using Akka.Actor;
using Bunit;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Notification;
using ZB.MOM.WW.ScadaBridge.Communication;
using ZB.MOM.WW.ScadaBridge.Security;
using NotificationReportPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Notifications.NotificationReport;

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

/// <summary>
/// bUnit rendering tests for the Notification Report page.
///
/// Testability note: <see cref="CommunicationService"/> is a concrete class with
/// non-virtual methods, so NSubstitute cannot intercept it. The report calls all
/// route through an injected <see cref="IActorRef"/> (the notification-outbox
/// proxy), so the tests wire a real, lightweight <see cref="ActorSystem"/> with a
/// scripted <see cref="ReceiveActor"/> that replies with fixed responses — the
/// same seam <c>SetNotificationOutbox</c> exists for.
/// </summary>
public class NotificationReportPageTests : BunitContext
{
    private readonly ActorSystem _system = ActorSystem.Create("notif-report-tests");
    private readonly CommunicationService _comms;

    // Mutable scripted reply — individual tests can override before rendering.
    private NotificationOutboxQueryResponse _queryReply =
        new("q", true, null, new List<NotificationSummary>
        {
            new("notif-aaaaaaaa-1111", "Email", "Ops On-Call", "Pump fault at Plant-A",
                "Parked", RetryCount: 3, LastError: "SMTP timeout", SourceSiteId: "plant-a",
                SourceInstanceId: "Pump-001", CreatedAt: DateTimeOffset.UtcNow.AddMinutes(-30),
                DeliveredAt: null, IsStuck: true),
            new("notif-bbbbbbbb-2222", "Email", "Maintenance", "Daily summary",
                "Delivered", RetryCount: 0, LastError: null, SourceSiteId: "plant-b",
                SourceInstanceId: null, CreatedAt: DateTimeOffset.UtcNow.AddHours(-2),
                DeliveredAt: DateTimeOffset.UtcNow.AddHours(-2), IsStuck: false),
        }, TotalCount: 2);

    // Records the most recent retry/discard requests the actor received.
    private readonly List<RetryNotificationRequest> _retryRequests = new();
    private readonly List<DiscardNotificationRequest> _discardRequests = new();

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

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

        Services.AddSingleton(_comms);
        Services.AddSingleton<IDialogService>(new AlwaysConfirmDialogService());

        var siteRepo = Substitute.For<ISiteRepository>();
        siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<Site>>(new List<Site>
            {
                new("Plant A", "plant-a") { Id = 1 },
                new("Plant B", "plant-b") { Id = 2 },
            }));
        Services.AddSingleton(siteRepo);

        var claims = new[]
        {
            new Claim(JwtTokenService.UsernameClaimType, "tester"),
            new Claim(JwtTokenService.RoleClaimType, "Deployer"),
        };
        var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
        Services.AddSingleton<AuthenticationStateProvider>(new TestAuthStateProvider(user));
        Services.AddAuthorizationCore();
        // CentralUI-028: the page now injects SiteScopeService — the test user
        // has no SiteId claims, so this resolves to system-wide and the
        // pre-existing test expectations hold.
        Services.AddScoped<ZB.MOM.WW.ScadaBridge.CentralUI.Auth.SiteScopeService>();
    }

    [Fact]
    public void Page_RequiresDeploymentPolicy()
    {
        var attr = typeof(NotificationReportPage)
            .GetCustomAttributes(typeof(AuthorizeAttribute), true)
            .Cast<AuthorizeAttribute>()
            .FirstOrDefault();

        Assert.NotNull(attr);
        Assert.Equal(AuthorizationPolicies.RequireDeployment, attr!.Policy);
    }

    [Fact]
    public void Renders_NotificationRows()
    {
        var cut = Render<NotificationReportPage>();

        cut.WaitForAssertion(() =>
        {
            Assert.Contains("Pump fault at Plant-A", cut.Markup);
            Assert.Contains("Daily summary", cut.Markup);
            Assert.Contains("Ops On-Call", cut.Markup);
        });
    }

    [Fact]
    public void StuckRow_IsBadged()
    {
        var cut = Render<NotificationReportPage>();

        cut.WaitForAssertion(() =>
        {
            var stuckRow = cut.FindAll("tbody tr")
                .First(r => r.TextContent.Contains("Pump fault at Plant-A"));

            // The stuck row carries a visible "Stuck" badge.
            Assert.Contains("badge", stuckRow.InnerHtml);
            Assert.Contains("Stuck", stuckRow.TextContent);
        });
    }

    [Fact]
    public void ClickRetry_OnParkedRow_CallsRetryNotification()
    {
        var cut = Render<NotificationReportPage>();

        cut.WaitForState(() => cut.Markup.Contains("Pump fault at Plant-A"));

        var parkedRow = cut.FindAll("tbody tr")
            .First(r => r.TextContent.Contains("Pump fault at Plant-A"));
        var retryButton = parkedRow.QuerySelectorAll("button")
            .First(b => b.TextContent.Contains("Retry"));

        retryButton.Click();

        cut.WaitForAssertion(() =>
        {
            Assert.Single(_retryRequests);
            Assert.Equal("notif-aaaaaaaa-1111", _retryRequests[0].NotificationId);
        });
    }

    [Fact]
    public void ClickDiscard_OnParkedRow_CallsDiscardNotification()
    {
        var cut = Render<NotificationReportPage>();

        cut.WaitForState(() => cut.Markup.Contains("Pump fault at Plant-A"));

        var parkedRow = cut.FindAll("tbody tr")
            .First(r => r.TextContent.Contains("Pump fault at Plant-A"));
        var discardButton = parkedRow.QuerySelectorAll("button")
            .First(b => b.TextContent.Contains("Discard"));

        discardButton.Click();

        cut.WaitForAssertion(() =>
        {
            Assert.Single(_discardRequests);
            Assert.Equal("notif-aaaaaaaa-1111", _discardRequests[0].NotificationId);
        });
    }

    [Fact]
    public void QueryFailure_ShowsErrorMessage()
    {
        _queryReply = new NotificationOutboxQueryResponse(
            "q", false, "outbox query backend unavailable",
            new List<NotificationSummary>(), TotalCount: 0);

        var cut = Render<NotificationReportPage>();

        cut.WaitForAssertion(() =>
            Assert.Contains("outbox query backend unavailable", cut.Markup));
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Bundle D drill-in (#23 M7-T10) — every notification row carries a
    // "View audit history" link to /audit/log?correlationId={NotificationId}.
    // ─────────────────────────────────────────────────────────────────────────

    [Fact]
    public void NotificationRow_ViewAuditHistory_Link_HasCorrectHref()
    {
        var cut = Render<NotificationReportPage>();

        cut.WaitForAssertion(() =>
        {
            // Both rows (Parked + Delivered) must surface the link — the drill-in
            // is row-scope, not status-scope. We pin the parked row's href to the
            // canonical correlationId-deep-link shape.
            var parkedRow = cut.FindAll("tbody tr")
                .First(r => r.TextContent.Contains("Pump fault at Plant-A"));
            var link = parkedRow.QuerySelector("a[data-test^=\"audit-link-\"]");
            Assert.NotNull(link);
            Assert.Equal(
                "/audit/log?correlationId=notif-aaaaaaaa-1111",
                link!.GetAttribute("href"));
            Assert.Contains("View audit history", link.TextContent);

            var deliveredRow = cut.FindAll("tbody tr")
                .First(r => r.TextContent.Contains("Daily summary"));
            var deliveredLink = deliveredRow.QuerySelector("a[data-test^=\"audit-link-\"]");
            Assert.NotNull(deliveredLink);
            Assert.Equal(
                "/audit/log?correlationId=notif-bbbbbbbb-2222",
                deliveredLink!.GetAttribute("href"));
        });
    }

    [Fact]
    public void Click_NavigatesTo_AuditLog_WithCorrelationId()
    {
        // The drill-in is a plain <a href> — browser-native navigation, not a
        // Blazor onclick handler — so this test verifies the rendered anchor's
        // attributes are exactly what a browser would follow: href, role, and
        // human-visible text. (Triggering bUnit's .Click() on a bare anchor
        // raises MissingEventHandlerException because there is no onclick
        // handler to invoke; the navigation contract lives in the <a> markup.)
        var cut = Render<NotificationReportPage>();

        cut.WaitForState(() => cut.Markup.Contains("Pump fault at Plant-A"));

        var parkedRow = cut.FindAll("tbody tr")
            .First(r => r.TextContent.Contains("Pump fault at Plant-A"));
        var link = parkedRow.QuerySelector("a[data-test^=\"audit-link-\"]")!;

        Assert.Equal("a", link.TagName, ignoreCase: true);
        Assert.Equal("/audit/log?correlationId=notif-aaaaaaaa-1111", link.GetAttribute("href"));
        Assert.Contains("View audit history", link.TextContent);
    }

    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 each outbox message
    /// type with the test's currently-scripted response.
    /// </summary>
    private sealed class ScriptedOutboxActor : ReceiveActor
    {
        public ScriptedOutboxActor(NotificationReportPageTests test)
        {
            Receive<NotificationOutboxQueryRequest>(_ => Sender.Tell(test._queryReply));
            Receive<RetryNotificationRequest>(r =>
            {
                test._retryRequests.Add(r);
                Sender.Tell(new RetryNotificationResponse(r.CorrelationId, true, null));
            });
            Receive<DiscardNotificationRequest>(r =>
            {
                test._discardRequests.Add(r);
                Sender.Tell(new DiscardNotificationResponse(r.CorrelationId, true, null));
            });
        }
    }

    /// <summary>A dialog service that auto-confirms, so action paths run end-to-end.</summary>
    private sealed class AlwaysConfirmDialogService : IDialogService
    {
        public Task<bool> ConfirmAsync(string title, string message, bool danger = false)
            => Task.FromResult(true);

        public Task<string?> PromptAsync(
            string title, string label, string initialValue = "", string? placeholder = null)
            => Task.FromResult<string?>(null);

        public Task<TResult?> ShowAsync<TResult>(
            string title,
            Microsoft.AspNetCore.Components.RenderFragment<DialogContext<TResult>> body,
            string? size = null)
            => Task.FromResult<TResult?>(default);
    }
}
