using System.Security.Claims;
using Bunit;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Forms;
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections;
using ZB.MOM.WW.ScadaBridge.Security;

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

/// <summary>
/// Covers the M7-B8 (T17) "Verify endpoint" affordance added to
/// <see cref="OpcUaEndpointEditor"/>: a button that calls the injected
/// <see cref="IEndpointVerificationService"/> with the current endpoint config and
/// renders the typed <see cref="VerifyEndpointResult"/> — green on success, a red
/// failure line otherwise, and a read-only certificate panel when the failure is
/// <see cref="VerifyFailureKind.UntrustedCertificate"/> with a captured cert.
///
/// <para>
/// Also covers the M7-B10 (T17) "Trust certificate" affordance on the untrusted-cert
/// panel: an Administrator-gated button that calls
/// <see cref="ICertManagementService.TrustAsync"/> and re-runs Verify on success.
/// </para>
/// </summary>
public class OpcUaEndpointVerifyTests : BunitContext
{
    private readonly IEndpointVerificationService _verify =
        Substitute.For<IEndpointVerificationService>();

    private readonly ICertManagementService _certs =
        Substitute.For<ICertManagementService>();

    public OpcUaEndpointVerifyTests()
    {
        Services.AddSingleton(_verify);
        Services.AddSingleton(_certs);
    }

    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 bool _authConfigured;

    private void UseRoles(params string[] roles)
    {
        Services.AddSingleton<AuthenticationStateProvider>(
            new TestAuthStateProvider(BuildPrincipal(roles)));
        Services.AddAuthorizationCore();
        AuthorizationPolicies.AddScadaBridgeAuthorization(Services);
        Services.AddSingleton<IAuthorizationService, DefaultAuthorizationService>();
        _authConfigured = true;
    }

    // The editor's untrusted-cert panel hosts an AuthorizeView (RequireAdmin) around
    // the Trust button, so the editor must render inside a CascadingAuthenticationState
    // with an auth provider available. Tests that don't call UseRoles get a default
    // Designer principal (can verify + see the cert panel, but NOT the Trust button).
    private IRenderedComponent<OpcUaEndpointEditor> RenderEditor()
    {
        if (!_authConfigured)
        {
            UseRoles(Roles.Designer);
        }

        var host = Render<CascadingAuthenticationState>(parameters => parameters
            .Add(p => p.ChildContent, (Microsoft.AspNetCore.Components.RenderFragment)(builder =>
            {
                builder.OpenComponent<OpcUaEndpointEditor>(0);
                builder.AddAttribute(1, nameof(OpcUaEndpointEditor.Config),
                    new OpcUaEndpointConfig { EndpointUrl = "opc.tcp://host:4840" });
                builder.AddAttribute(2, nameof(OpcUaEndpointEditor.SiteIdentifier), "plant-a");
                builder.AddAttribute(3, nameof(OpcUaEndpointEditor.ConnectionName), "PLC-OPC");
                builder.AddAttribute(4, nameof(OpcUaEndpointEditor.Protocol), "OpcUa");
                builder.CloseComponent();
            })));

        return host.FindComponent<OpcUaEndpointEditor>();
    }

    [Fact]
    public void Verify_Success_ShowsSuccessMessage()
    {
        _verify.VerifyAsync(
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
                Arg.Any<OpcUaEndpointConfig>(), Arg.Any<CancellationToken>())
            .Returns(new VerifyEndpointResult(true, null, null, null));

        var cut = RenderEditor();
        cut.Find("[data-test=verify-endpoint-btn]").Click();

        Assert.NotEmpty(cut.FindAll("[data-test=verify-success]"));
        Assert.Empty(cut.FindAll("[data-test=verify-failure]"));
        Assert.Empty(cut.FindAll("[data-test=verify-cert-panel]"));
    }

    [Fact]
    public void Verify_UntrustedCertificate_ShowsCertPanelWithThumbprint()
    {
        var cert = new ServerCertInfo(
            Thumbprint: "ABCDEF0123456789",
            Subject: "CN=opc-server",
            Issuer: "CN=opc-server",
            NotBeforeUtc: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
            NotAfterUtc: new DateTime(2027, 1, 1, 0, 0, 0, DateTimeKind.Utc),
            DerBase64: "ZGVy");
        _verify.VerifyAsync(
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
                Arg.Any<OpcUaEndpointConfig>(), Arg.Any<CancellationToken>())
            .Returns(new VerifyEndpointResult(
                false, VerifyFailureKind.UntrustedCertificate, "Untrusted certificate.", cert));

        var cut = RenderEditor();
        cut.Find("[data-test=verify-endpoint-btn]").Click();

        var panel = cut.Find("[data-test=verify-cert-panel]");
        Assert.Contains("ABCDEF0123456789", panel.TextContent);
        Assert.Contains("CN=opc-server", panel.TextContent);
        // The failure line is still shown alongside the cert panel.
        Assert.NotEmpty(cut.FindAll("[data-test=verify-failure]"));
    }

    [Fact]
    public void Verify_GenericFailure_ShowsFailureMessage()
    {
        _verify.VerifyAsync(
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
                Arg.Any<OpcUaEndpointConfig>(), Arg.Any<CancellationToken>())
            .Returns(new VerifyEndpointResult(
                false, VerifyFailureKind.Unreachable, "Connection refused.", null));

        var cut = RenderEditor();
        cut.Find("[data-test=verify-endpoint-btn]").Click();

        var failure = cut.Find("[data-test=verify-failure]");
        Assert.Contains("Connection refused.", failure.TextContent);
        Assert.Empty(cut.FindAll("[data-test=verify-success]"));
        Assert.Empty(cut.FindAll("[data-test=verify-cert-panel]"));
    }

    private static ServerCertInfo SampleCert() => new(
        Thumbprint: "ABCDEF0123456789",
        Subject: "CN=opc-server",
        Issuer: "CN=opc-server",
        NotBeforeUtc: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
        NotAfterUtc: new DateTime(2027, 1, 1, 0, 0, 0, DateTimeKind.Utc),
        DerBase64: "ZGVy");

    private void ArrangeUntrustedVerify(ServerCertInfo cert) =>
        _verify.VerifyAsync(
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
                Arg.Any<OpcUaEndpointConfig>(), Arg.Any<CancellationToken>())
            .Returns(new VerifyEndpointResult(
                false, VerifyFailureKind.UntrustedCertificate, "Untrusted certificate.", cert));

    [Fact]
    public void TrustButton_Admin_SeesButton_AndClick_CallsTrustAsync()
    {
        UseRoles(Roles.Administrator);
        var cert = SampleCert();
        ArrangeUntrustedVerify(cert);
        _certs.TrustAsync(
                Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
                Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(new CertTrustResult(true, null, null));

        var cut = RenderEditor();
        cut.Find("[data-test=verify-endpoint-btn]").Click();

        // Admin sees the Trust button on the untrusted-cert panel.
        var trustBtn = cut.Find("[data-test=trust-cert-btn]");
        trustBtn.Click();

        _certs.Received(1).TrustAsync(
            "plant-a", "PLC-OPC", cert.DerBase64, cert.Thumbprint, Arg.Any<CancellationToken>());
    }

    [Fact]
    public void TrustButton_NonAdmin_DesignerOnly_DoesNotSeeButton()
    {
        UseRoles(Roles.Designer);
        ArrangeUntrustedVerify(SampleCert());

        var cut = RenderEditor();
        cut.Find("[data-test=verify-endpoint-btn]").Click();

        // The cert panel is shown, but a Designer (non-Admin) does not get the Trust button.
        Assert.NotEmpty(cut.FindAll("[data-test=verify-cert-panel]"));
        Assert.Empty(cut.FindAll("[data-test=trust-cert-btn]"));
    }
}
