using System.Security.Claims;
using System.Text.RegularExpressions;
using Bunit;
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.Components.Shared;
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.Management;
using ZB.MOM.WW.ScadaBridge.Security;
using SecuredWritesPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Operations.SecuredWrites;

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

/// <summary>
/// bUnit rendering + interaction tests for the Secured Writes page (M7 T14b / C5).
/// The page is the UI for the two-person MxGateway write workflow: an Operator submits,
/// a different Verifier approves/rejects, and everyone sees the history. The page only
/// SUBMITS commands through <see cref="ISecuredWriteService"/> — the server enforces
/// roles, the no-self-approval guard, the device relay, and the audit trail — so these
/// tests substitute the service and assert the page wires the inputs/actions correctly.
/// The real policies are registered so the per-region <c>AuthorizeView Policy=...</c>
/// blocks are genuinely evaluated against the test principal's role claims.
/// </summary>
public class SecuredWritesTests : BunitContext
{
    private readonly ISecuredWriteService _service = Substitute.For<ISecuredWriteService>();
    private readonly ISiteRepository _siteRepo = Substitute.For<ISiteRepository>();
    private readonly IDialogService _dialog = Substitute.For<IDialogService>();
    // The submit region embeds a NodeBrowserDialog (@inject IBrowseService) for the
    // OPC UA tag picker. The dialog is only rendered when the Operator opens it, but
    // bUnit resolves the injected service at render time, so it must be registered
    // even though these tests never drive the browser.
    private readonly IBrowseService _browse = Substitute.For<IBrowseService>();

    public SecuredWritesTests()
    {
        Services.AddSingleton(_service);
        Services.AddSingleton(_siteRepo);
        Services.AddSingleton(_dialog);
        Services.AddSingleton(_browse);

        // Confirm dialogs resolve to "yes" without a DialogHost in the render scope.
        _dialog.ConfirmAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>())
            .Returns(Task.FromResult(true));

        // Empty list by default; individual tests seed the queue/history.
        _service.ListAsync(
                Arg.Any<string?>(), Arg.Any<string?>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns(Task.FromResult(new SecuredWriteListResult(Array.Empty<SecuredWriteDto>(), 0)));
    }

    /// <summary>Stubs <c>ListAsync</c> (any filter/paging args) to return <paramref name="rows"/>.</summary>
    private void SeedList(IReadOnlyList<SecuredWriteDto> rows, int? totalCount = null)
        => _service.ListAsync(
                Arg.Any<string?>(), Arg.Any<string?>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns(Task.FromResult(new SecuredWriteListResult(rows, totalCount ?? rows.Count)));

    /// <summary>
    /// Registers the real authorization stack + a principal with the given username and
    /// roles so the page's <c>AuthorizeView Policy=...</c> regions evaluate genuinely.
    /// </summary>
    private void AddAuth(string username, params string[] roles)
    {
        var claims = new List<Claim> { new(JwtTokenService.UsernameClaimType, username) };
        claims.AddRange(roles.Select(r => new Claim(JwtTokenService.RoleClaimType, r)));
        var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));

        Services.AddSingleton<AuthenticationStateProvider>(new TestAuthStateProvider(user));
        Services.AddAuthorizationCore();
        AuthorizationPolicies.AddScadaBridgeAuthorization(Services);
        // BunitContext pre-registers a placeholder IAuthorizationService that throws on
        // policy evaluation; force the real one so the regions are actually gated.
        Services.AddSingleton<IAuthorizationService, DefaultAuthorizationService>();
    }

    private void SeedSites(params Site[] sites)
    {
        _siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<Site>>(sites.ToList()));
        foreach (var site in sites)
        {
            _siteRepo.GetDataConnectionsBySiteIdAsync(site.Id, Arg.Any<CancellationToken>())
                .Returns(Task.FromResult<IReadOnlyList<DataConnection>>(Array.Empty<DataConnection>()));
        }
    }

    private void SeedConnections(int siteId, params DataConnection[] connections)
        => _siteRepo.GetDataConnectionsBySiteIdAsync(siteId, Arg.Any<CancellationToken>())
            .Returns(Task.FromResult<IReadOnlyList<DataConnection>>(connections.ToList()));

    /// <summary>
    /// Renders the page inside a <see cref="CascadingAuthenticationState"/> so its
    /// <c>AuthorizeView</c> regions have the cascading <c>Task&lt;AuthenticationState&gt;</c>
    /// they require (mirrors the NavMenu tests).
    /// </summary>
    private IRenderedComponent<SecuredWritesPage> RenderPage()
    {
        var host = Render<CascadingAuthenticationState>(parameters => parameters
            .Add(p => p.ChildContent, (RenderFragment)(builder =>
            {
                builder.OpenComponent<SecuredWritesPage>(0);
                builder.CloseComponent();
            })));
        return host.FindComponent<SecuredWritesPage>();
    }

    private static SecuredWriteDto Dto(
        long id, string status, string operatorUser,
        string? verifier = null, string? executionError = null,
        string siteId = "plant-a", string connection = "GW-1", string tag = "Pump.Setpoint",
        string valueJson = "true", string valueType = "Boolean") =>
        new(id, siteId, connection, tag, valueJson, valueType, status, operatorUser,
            OperatorComment: null, SubmittedAtUtc: DateTime.UtcNow,
            VerifierUser: verifier, VerifierComment: null,
            DecidedAtUtc: status == "Pending" ? null : DateTime.UtcNow,
            ExecutedAtUtc: status is "Executed" or "Failed" ? DateTime.UtcNow : null,
            ExecutionError: executionError);

    // ── (a) Operator: submit form present + Submit calls SubmitAsync with the values ──

    [Fact]
    public void Operator_SeesSubmitForm()
    {
        AddAuth("op-user", Roles.Operator);
        SeedSites(new Site("Plant-A", "plant-a") { Id = 1 });

        var cut = RenderPage();

        cut.WaitForAssertion(() =>
        {
            Assert.NotNull(cut.Find("[data-test='secured-write-submit-region']"));
            Assert.NotNull(cut.Find("[data-test='secured-write-submit']"));
        });
    }

    [Fact]
    public void Operator_Submit_CallsServiceWithEnteredValues()
    {
        AddAuth("op-user", Roles.Operator);
        SeedSites(new Site("Plant-A", "plant-a") { Id = 1 });
        SeedConnections(1, new DataConnection("GW-1", "MxGateway", 1) { Id = 10 });

        _service.SubmitAsync(
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<CancellationToken>())
            .Returns(Task.FromResult(SecuredWriteActionResult.Ok(Dto(1, "Pending", "op-user"))));

        var cut = RenderPage();

        cut.Find("[data-test='secured-write-site']").Change("plant-a");
        cut.Find("[data-test='secured-write-connection']").Change("GW-1");
        cut.Find("[data-test='secured-write-tagpath']").Change("Pump.Setpoint");
        cut.Find("[data-test='secured-write-value']").Change("true");
        cut.Find("[data-test='secured-write-datatype']").Change("Boolean");
        cut.Find("[data-test='secured-write-comment']").Change("routine setpoint");

        cut.Find("[data-test='secured-write-submit']").Click();

        _service.Received(1).SubmitAsync(
            "plant-a", "GW-1", "Pump.Setpoint", "true", "Boolean", "routine setpoint",
            Arg.Any<CancellationToken>());
    }

    // ── (b) Verifier: pending row renders with Approve/Reject; own-row actions disabled ──

    [Fact]
    public void Verifier_PendingRow_HasEnabledActions_ForOtherOperatorsRow()
    {
        AddAuth("verifier-user", Roles.Verifier);
        SeedSites(new Site("Plant-A", "plant-a") { Id = 1 });
        SeedList(new[] { Dto(1, "Pending", "someone-else") });

        var cut = RenderPage();

        cut.WaitForAssertion(() =>
        {
            Assert.NotNull(cut.Find("[data-test='secured-write-pending-region']"));
            var approve = cut.Find("[data-test='secured-write-approve']");
            var reject = cut.Find("[data-test='secured-write-reject']");
            Assert.False(approve.HasAttribute("disabled"));
            Assert.False(reject.HasAttribute("disabled"));
        });
    }

    [Fact]
    public void Verifier_OwnSubmission_HasApproveAndRejectDisabled()
    {
        // DisableLogin dev caveat: one identity holds both roles; the page must still
        // disable approve/reject on a row the current user submitted (server enforces
        // this too, but the UI surfaces it up front).
        AddAuth("self", Roles.Operator, Roles.Verifier);
        SeedSites(new Site("Plant-A", "plant-a") { Id = 1 });
        SeedList(new[] { Dto(1, "Pending", "self") });

        var cut = RenderPage();

        cut.WaitForAssertion(() =>
        {
            var approve = cut.Find("[data-test='secured-write-approve']");
            var reject = cut.Find("[data-test='secured-write-reject']");
            Assert.True(approve.HasAttribute("disabled"));
            Assert.True(reject.HasAttribute("disabled"));
            Assert.NotNull(cut.Find("[data-test='secured-write-own-note']"));
        });
    }

    [Fact]
    public void Verifier_Approve_CallsApproveAsync()
    {
        AddAuth("verifier-user", Roles.Verifier);
        SeedSites(new Site("Plant-A", "plant-a") { Id = 1 });
        SeedList(new[] { Dto(7, "Pending", "someone-else") });
        _service.ApproveAsync(Arg.Any<long>(), Arg.Any<string?>(), Arg.Any<CancellationToken>())
            .Returns(Task.FromResult(SecuredWriteActionResult.Ok(Dto(7, "Executed", "someone-else", "verifier-user"))));

        var cut = RenderPage();
        cut.Find("[data-test='secured-write-approve']").Click();

        _service.Received(1).ApproveAsync(7, Arg.Any<string?>(), Arg.Any<CancellationToken>());
    }

    // ── (c) History: terminal rows render ──

    [Fact]
    public void History_RendersTerminalRows()
    {
        AddAuth("viewer", Roles.Viewer);
        SeedSites(new Site("Plant-A", "plant-a") { Id = 1 });
        SeedList(new[]
        {
            Dto(1, "Executed", "op-a", "ver-a", tag: "Pump.Setpoint"),
            Dto(2, "Rejected", "op-b", "ver-b", tag: "Valve.Cmd"),
            Dto(3, "Failed", "op-c", "ver-c", executionError: "device timeout", tag: "Motor.Run"),
            Dto(4, "Pending", "op-d", tag: "ShouldNotAppear"),
        });

        var cut = RenderPage();

        cut.WaitForAssertion(() =>
        {
            Assert.NotNull(cut.Find("[data-test='secured-write-history-region']"));
            var rows = cut.FindAll("[data-test='secured-write-history-row']");
            // Three terminal rows (Executed/Rejected/Failed); the Pending row is excluded.
            Assert.Equal(3, rows.Count);
            Assert.Contains("Executed", cut.Markup);
            Assert.Contains("Rejected", cut.Markup);
            Assert.Contains("device timeout", cut.Markup);
            Assert.DoesNotContain("ShouldNotAppear", cut.Markup);
        });
    }

    // ── (d) History pager: page-size 50, driven by TotalCount ──

    [Fact]
    public void History_Pager_NextRequestsSecondPage_UsingTotalCount()
    {
        // 60 terminal rows returned but TotalCount = 120 → a second page exists, so the
        // "Next" button is present + enabled and clicking it re-queries with skip: 50.
        AddAuth("viewer", Roles.Viewer);
        SeedSites(new Site("Plant-A", "plant-a") { Id = 1 });
        var terminal = Enumerable.Range(1, 60)
            .Select(i => Dto(i, "Executed", $"op-{i}", $"ver-{i}"))
            .ToArray();
        SeedList(terminal, totalCount: 120);

        var cut = RenderPage();

        cut.WaitForAssertion(() =>
        {
            var next = cut.Find("[data-test='secured-write-history-next']");
            Assert.False(next.HasAttribute("disabled"));
        });

        cut.Find("[data-test='secured-write-history-next']").Click();

        // The history page-2 fetch offsets by one page (skip: 50).
        _service.Received().ListAsync(
            Arg.Any<string?>(), Arg.Any<string?>(), 50, Arg.Any<int>(), Arg.Any<CancellationToken>());
    }

    // ── (e) Approve dialog surfaces submission age (stale-setpoint scenario) ──

    [Fact]
    public void Approve_ConfirmDialog_ShowsSubmissionAge()
    {
        AddAuth("verifier-user", Roles.Verifier);
        SeedSites(new Site("Plant-A", "plant-a") { Id = 1 });
        var stale = Dto(9, "Pending", "someone-else") with { SubmittedAtUtc = DateTime.UtcNow.AddHours(-26) };
        SeedList(new[] { stale });
        _service.ApproveAsync(Arg.Any<long>(), Arg.Any<string?>(), Arg.Any<CancellationToken>())
            .Returns(Task.FromResult(SecuredWriteActionResult.Ok(Dto(9, "Executed", "someone-else", "verifier-user"))));

        var cut = RenderPage();
        cut.WaitForAssertion(() => Assert.NotNull(cut.Find("[data-test='secured-write-approve']")));

        cut.Find("[data-test='secured-write-approve']").Click();

        _dialog.Received(1).ConfirmAsync(
            Arg.Any<string>(),
            Arg.Is<string>(m => Regex.IsMatch(m, "Submitted .* ago")),
            Arg.Any<bool>());
    }

    [Fact]
    public void Viewer_DoesNotSeeOperatorOrVerifierRegions()
    {
        AddAuth("viewer", Roles.Viewer);
        SeedSites(new Site("Plant-A", "plant-a") { Id = 1 });

        var cut = RenderPage();

        cut.WaitForAssertion(() =>
        {
            Assert.Empty(cut.FindAll("[data-test='secured-write-submit-region']"));
            Assert.Empty(cut.FindAll("[data-test='secured-write-pending-region']"));
            // History is visible to any authenticated user.
            Assert.NotNull(cut.Find("[data-test='secured-write-history-region']"));
        });
    }
}
