using Opc.Ua;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Client.Shared.Adapters;

namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.Adapters;

/// <summary>
/// Regression tests for the pure best-endpoint selection logic extracted from
/// <see cref="DefaultEndpointDiscovery"/> (Client.Shared-011). The selector picks the best
/// endpoint matching the requested security mode (preferring Basic256Sha256) and rewrites
/// the discovered endpoint URL hostname to match the operator-supplied URL so internal DNS
/// hostnames in discovery responses do not leak into the session.
/// </summary>
public class EndpointSelectorTests
{
    private static EndpointDescription Ep(MessageSecurityMode mode, string policy, string url)
    {
        return new EndpointDescription
        {
            EndpointUrl = url,
            SecurityMode = mode,
            SecurityPolicyUri = policy,
        };
    }

    /// <summary>
    /// Verifies that the selector returns the only endpoint matching the requested
    /// security mode even when other endpoints with different modes are present.
    /// </summary>
    [Fact]
    public void SelectBest_PicksMatchingSecurityMode()
    {
        var endpoints = new[]
        {
            Ep(MessageSecurityMode.None, SecurityPolicies.None, "opc.tcp://server:4840"),
            Ep(MessageSecurityMode.Sign, SecurityPolicies.Basic256Sha256, "opc.tcp://server:4840"),
            Ep(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Basic256Sha256, "opc.tcp://server:4840"),
        };

        var best = EndpointSelector.SelectBest(endpoints, "opc.tcp://server:4840", MessageSecurityMode.Sign);

        best.SecurityMode.ShouldBe(MessageSecurityMode.Sign);
        best.SecurityPolicyUri.ShouldBe(SecurityPolicies.Basic256Sha256);
    }

    /// <summary>
    /// Verifies that when multiple endpoints match the requested mode, Basic256Sha256 wins
    /// over weaker policies — even when Basic256Sha256 is not the first encountered.
    /// </summary>
    [Fact]
    public void SelectBest_PrefersBasic256Sha256WhenMultipleMatch()
    {
        var endpoints = new[]
        {
            Ep(MessageSecurityMode.Sign, SecurityPolicies.Basic128Rsa15, "opc.tcp://server:4840"),
            Ep(MessageSecurityMode.Sign, SecurityPolicies.Basic256Sha256, "opc.tcp://server:4840"),
            Ep(MessageSecurityMode.Sign, SecurityPolicies.Basic256, "opc.tcp://server:4840"),
        };

        var best = EndpointSelector.SelectBest(endpoints, "opc.tcp://server:4840", MessageSecurityMode.Sign);

        best.SecurityPolicyUri.ShouldBe(SecurityPolicies.Basic256Sha256);
    }

    /// <summary>
    /// Verifies that the selector falls back to the first matching endpoint when no
    /// Basic256Sha256 endpoint is advertised for the requested security mode.
    /// </summary>
    [Fact]
    public void SelectBest_FallsBackToFirstMatchWhenNoBasic256Sha256()
    {
        var endpoints = new[]
        {
            Ep(MessageSecurityMode.Sign, SecurityPolicies.Basic128Rsa15, "opc.tcp://server:4840"),
            Ep(MessageSecurityMode.Sign, SecurityPolicies.Basic256, "opc.tcp://server:4840"),
        };

        var best = EndpointSelector.SelectBest(endpoints, "opc.tcp://server:4840", MessageSecurityMode.Sign);

        best.SecurityPolicyUri.ShouldBe(SecurityPolicies.Basic128Rsa15);
    }

    /// <summary>
    /// Verifies that no matching endpoint produces an InvalidOperationException whose
    /// message lists the available security mode/policy combinations to aid diagnosis.
    /// </summary>
    [Fact]
    public void SelectBest_NoMatchingMode_ThrowsWithDiagnostic()
    {
        var endpoints = new[]
        {
            Ep(MessageSecurityMode.None, SecurityPolicies.None, "opc.tcp://server:4840"),
        };

        var ex = Should.Throw<InvalidOperationException>(() =>
            EndpointSelector.SelectBest(endpoints, "opc.tcp://server:4840", MessageSecurityMode.SignAndEncrypt));

        ex.Message.ShouldContain("SignAndEncrypt");
        ex.Message.ShouldContain("None"); // available endpoint listed in the message
    }

    /// <summary>
    /// Verifies that the selector rewrites the discovery-returned hostname to the
    /// operator-supplied hostname so internal DNS names in the response do not leak
    /// into the resulting session.
    /// </summary>
    [Fact]
    public void SelectBest_RewritesHostToMatchRequestedUrl()
    {
        var endpoints = new[]
        {
            Ep(MessageSecurityMode.Sign, SecurityPolicies.Basic256Sha256,
                "opc.tcp://internal-host:4840/UA/Server"),
        };

        var best = EndpointSelector.SelectBest(endpoints, "opc.tcp://external-host:4840",
            MessageSecurityMode.Sign);

        new Uri(best.EndpointUrl).Host.ShouldBe("external-host");
    }

    /// <summary>
    /// Verifies that when the discovery host already matches the requested host the
    /// endpoint URL is left untouched.
    /// </summary>
    [Fact]
    public void SelectBest_HostsMatch_LeavesUrlUnchanged()
    {
        var endpoints = new[]
        {
            Ep(MessageSecurityMode.Sign, SecurityPolicies.Basic256Sha256,
                "opc.tcp://server:4840/UA/Server"),
        };

        var best = EndpointSelector.SelectBest(endpoints, "opc.tcp://server:4840",
            MessageSecurityMode.Sign);

        best.EndpointUrl.ShouldBe("opc.tcp://server:4840/UA/Server");
    }

    /// <summary>
    /// Verifies that a null endpoints argument throws ArgumentNullException rather than
    /// producing a confusing downstream NullReferenceException.
    /// </summary>
    [Fact]
    public void SelectBest_NullEndpoints_Throws()
    {
        Should.Throw<ArgumentNullException>(() =>
            EndpointSelector.SelectBest(null!, "opc.tcp://server:4840", MessageSecurityMode.None));
    }

    /// <summary>
    /// Verifies that an empty endpointUrl produces ArgumentException so the caller gets
    /// a clear contract violation rather than a downstream UriFormatException.
    /// </summary>
    [Fact]
    public void SelectBest_EmptyEndpointUrl_Throws()
    {
        Should.Throw<ArgumentException>(() =>
            EndpointSelector.SelectBest(Array.Empty<EndpointDescription>(), "", MessageSecurityMode.None));
    }
}
