using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Opc.Ua;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;

namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests;

/// <summary>
/// T17: Unit tests for <see cref="RealOpcUaClient.MapVerifyOutcome"/> — the pure
/// (exception + captured cert) → <see cref="VerifyEndpointResult"/> mapping that backs the
/// OPC UA verify-endpoint probe. These exercise every <see cref="VerifyFailureKind"/>
/// branch WITHOUT a live OPC UA server, so the classification logic is covered
/// deterministically.
/// </summary>
public class VerifyEndpointResultMappingTests
{
    /// <summary>
    /// Builds a throwaway self-signed certificate so the untrusted-cert branch can be
    /// exercised with a real <see cref="X509Certificate2"/> (and real DER bytes) without a
    /// live server. Not persisted; only its public fields are asserted.
    /// </summary>
    private static X509Certificate2 BuildSelfSignedCert(
        string subjectName = "CN=ScadaBridge-Verify-Test")
    {
        using var rsa = RSA.Create(2048);
        var request = new CertificateRequest(
            subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        var notBefore = DateTimeOffset.UtcNow.AddMinutes(-5);
        var notAfter = DateTimeOffset.UtcNow.AddDays(1);
        return request.CreateSelfSigned(notBefore, notAfter);
    }

    [Fact]
    public void CapturedCert_WithValidationException_MapsToUntrustedCertificate()
    {
        using var cert = BuildSelfSignedCert();
        // A certificate-validation rejection surfaces as a ServiceResultException with
        // BadCertificateUntrusted; the captured cert is what makes the kind authoritative.
        var ex = new ServiceResultException(
            StatusCodes.BadCertificateUntrusted, "Certificate is not trusted.");

        var result = RealOpcUaClient.MapVerifyOutcome(ex, cert);

        Assert.False(result.Success);
        Assert.Equal(VerifyFailureKind.UntrustedCertificate, result.FailureKind);
        Assert.NotNull(result.Cert);
        Assert.Equal(cert.Thumbprint, result.Cert!.Thumbprint);
        Assert.Equal(cert.Subject, result.Cert.Subject);
        Assert.Equal(cert.Issuer, result.Cert.Issuer);
        // The captured DER must round-trip to the original raw bytes verbatim.
        Assert.Equal(Convert.ToBase64String(cert.RawData), result.Cert.DerBase64);
        Assert.Equal(DateTimeKind.Utc, result.Cert.NotBeforeUtc.Kind);
        Assert.Equal(DateTimeKind.Utc, result.Cert.NotAfterUtc.Kind);
        Assert.NotNull(result.Error);
    }

    [Fact]
    public void CapturedCert_WithoutException_MapsToUntrustedCertificate()
    {
        // The validator may capture+reject the cert and the SDK then fail with a different
        // (or no) exception. A captured cert ALWAYS dominates the classification.
        using var cert = BuildSelfSignedCert();

        var result = RealOpcUaClient.MapVerifyOutcome(null, cert);

        Assert.False(result.Success);
        Assert.Equal(VerifyFailureKind.UntrustedCertificate, result.FailureKind);
        Assert.NotNull(result.Cert);
        Assert.Equal(cert.Thumbprint, result.Cert!.Thumbprint);
    }

    [Fact]
    public void TimeoutException_MapsToTimeout()
    {
        var result = RealOpcUaClient.MapVerifyOutcome(new TimeoutException("took too long"), null);

        Assert.False(result.Success);
        Assert.Equal(VerifyFailureKind.Timeout, result.FailureKind);
        Assert.Null(result.Cert);
    }

    [Fact]
    public void OperationCanceledException_MapsToTimeout()
    {
        var result = RealOpcUaClient.MapVerifyOutcome(new OperationCanceledException(), null);

        Assert.False(result.Success);
        Assert.Equal(VerifyFailureKind.Timeout, result.FailureKind);
        Assert.Null(result.Cert);
    }

    [Fact]
    public void ServiceResultException_BadRequestTimeout_MapsToTimeout()
    {
        var ex = new ServiceResultException(StatusCodes.BadRequestTimeout, "request timed out");

        var result = RealOpcUaClient.MapVerifyOutcome(ex, null);

        Assert.False(result.Success);
        Assert.Equal(VerifyFailureKind.Timeout, result.FailureKind);
    }

    [Theory]
    [InlineData(StatusCodes.BadUserAccessDenied)]
    [InlineData(StatusCodes.BadIdentityTokenRejected)]
    [InlineData(StatusCodes.BadIdentityTokenInvalid)]
    public void ServiceResultException_AuthCodes_MapToAuthFailed(uint statusCode)
    {
        var ex = new ServiceResultException(statusCode, "access denied");

        var result = RealOpcUaClient.MapVerifyOutcome(ex, null);

        Assert.False(result.Success);
        Assert.Equal(VerifyFailureKind.AuthFailed, result.FailureKind);
        Assert.Null(result.Cert);
    }

    [Theory]
    [InlineData(StatusCodes.BadConnectionRejected)]
    [InlineData(StatusCodes.BadNotConnected)]
    public void ServiceResultException_ConnectionCodes_MapToUnreachable(uint statusCode)
    {
        var ex = new ServiceResultException(statusCode, "no route");

        var result = RealOpcUaClient.MapVerifyOutcome(ex, null);

        Assert.False(result.Success);
        Assert.Equal(VerifyFailureKind.Unreachable, result.FailureKind);
    }

    [Fact]
    public void SocketException_MapsToUnreachable()
    {
        var ex = new System.Net.Sockets.SocketException(
            (int)System.Net.Sockets.SocketError.ConnectionRefused);

        var result = RealOpcUaClient.MapVerifyOutcome(ex, null);

        Assert.False(result.Success);
        Assert.Equal(VerifyFailureKind.Unreachable, result.FailureKind);
    }

    [Fact]
    public void SocketExceptionWrappedInServiceResult_MapsToUnreachable()
    {
        // The SDK surfaces a refused/DNS connect as a generic ServiceResultException whose
        // inner exception is the socket failure; the inner socket cause must dominate.
        var inner = new System.Net.Sockets.SocketException(
            (int)System.Net.Sockets.SocketError.HostNotFound);
        var ex = new ServiceResultException(StatusCodes.BadUnexpectedError, "connect failed", inner);

        var result = RealOpcUaClient.MapVerifyOutcome(ex, null);

        Assert.False(result.Success);
        Assert.Equal(VerifyFailureKind.Unreachable, result.FailureKind);
    }

    [Fact]
    public void GenericException_MapsToServerError()
    {
        var result = RealOpcUaClient.MapVerifyOutcome(new InvalidOperationException("boom"), null);

        Assert.False(result.Success);
        Assert.Equal(VerifyFailureKind.ServerError, result.FailureKind);
        Assert.Null(result.Cert);
        Assert.NotNull(result.Error);
    }

    [Fact]
    public void NullException_NullCert_MapsToSuccess()
    {
        var result = RealOpcUaClient.MapVerifyOutcome(null, null);

        Assert.True(result.Success);
        Assert.Null(result.FailureKind);
        Assert.Null(result.Error);
        Assert.Null(result.Cert);
    }
}
