using Microsoft.Extensions.Logging.Abstractions;
using Opc.Ua;
using Opc.Ua.Server;
using Shouldly;
using Xunit;

namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;

/// <summary>
/// F13b — verifies <see cref="OpcUaApplicationHost"/> publishes one
/// <see cref="ServerSecurityPolicy"/> per <see cref="OpcUaSecurityProfile"/> and emits both
/// Anonymous and UserName <see cref="UserTokenPolicy"/> entries. The pure-builder tests run
/// cross-platform without touching disk; the boot-verify test reuses the F13a PKI pattern.
/// </summary>
public sealed class OpcUaApplicationHostSecurityTests : IDisposable
{
    private static CancellationToken Ct => TestContext.Current.CancellationToken;

    private readonly string _pkiRoot = Path.Combine(
        Path.GetTempPath(),
        $"otopcua-pki-{Guid.NewGuid():N}");

    /// <summary>
    /// Verifies that BuildSecurityPolicies emits all three baseline security profiles.
    /// </summary>
    [Fact]
    public void BuildSecurityPolicies_default_set_emits_all_three_baseline_profiles()
    {
        var policies = OpcUaApplicationHost.BuildSecurityPolicies(new[]
        {
            OpcUaSecurityProfile.None,
            OpcUaSecurityProfile.Basic256Sha256Sign,
            OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt,
        }).ToList();

        policies.Count.ShouldBe(3);
        policies[0].SecurityMode.ShouldBe(MessageSecurityMode.None);
        policies[0].SecurityPolicyUri.ShouldBe(SecurityPolicies.None);
        policies[1].SecurityMode.ShouldBe(MessageSecurityMode.Sign);
        policies[1].SecurityPolicyUri.ShouldBe(SecurityPolicies.Basic256Sha256);
        policies[2].SecurityMode.ShouldBe(MessageSecurityMode.SignAndEncrypt);
        policies[2].SecurityPolicyUri.ShouldBe(SecurityPolicies.Basic256Sha256);
    }

    /// <summary>
    /// Verifies that BuildSecurityPolicies deduplicates repeated profiles.
    /// </summary>
    [Fact]
    public void BuildSecurityPolicies_dedupes_repeated_profiles()
    {
        var policies = OpcUaApplicationHost.BuildSecurityPolicies(new[]
        {
            OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt,
            OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt,
            OpcUaSecurityProfile.None,
        }).ToList();

        policies.Count.ShouldBe(2);
        policies[0].SecurityMode.ShouldBe(MessageSecurityMode.SignAndEncrypt);
        policies[1].SecurityMode.ShouldBe(MessageSecurityMode.None);
    }

    /// <summary>
    /// Verifies that BuildSecurityPolicies falls back to None when given empty input.
    /// </summary>
    [Fact]
    public void BuildSecurityPolicies_empty_input_falls_back_to_none()
    {
        var policies = OpcUaApplicationHost.BuildSecurityPolicies(Array.Empty<OpcUaSecurityProfile>()).ToList();

        policies.Count.ShouldBe(1);
        policies[0].SecurityMode.ShouldBe(MessageSecurityMode.None);
        policies[0].SecurityPolicyUri.ShouldBe(SecurityPolicies.None);
    }

    /// <summary>
    /// Verifies that BuildUserTokenPolicies emits anonymous and username policies.
    /// </summary>
    [Fact]
    public void BuildUserTokenPolicies_emits_anonymous_and_username()
    {
        var tokens = OpcUaApplicationHost.BuildUserTokenPolicies().ToList();

        tokens.Count.ShouldBe(2);
        tokens.ShouldContain(t => t.TokenType == UserTokenType.Anonymous && t.PolicyId == "anonymous");
        var userName = tokens.Single(t => t.TokenType == UserTokenType.UserName);
        userName.PolicyId.ShouldBe("username_basic256sha256");
        userName.SecurityPolicyUri.ShouldBe(SecurityPolicies.Basic256Sha256);
    }

    /// <summary>
    /// Verifies that StartAsync populates ServerConfiguration with all enabled security profiles.
    /// </summary>
    [Fact]
    public async Task StartAsync_populates_ServerConfiguration_with_all_enabled_profiles()
    {
        await using var host = new OpcUaApplicationHost(
            new OpcUaApplicationHostOptions
            {
                ApplicationName = "OtOpcUa.SecAll",
                ApplicationUri = $"urn:OtOpcUa.SecAll:{Guid.NewGuid():N}",
                OpcUaPort = AllocateFreePort(),
                PublicHostname = "localhost",
                PkiStoreRoot = _pkiRoot,
                EnabledSecurityProfiles =
                {
                    OpcUaSecurityProfile.None,
                    OpcUaSecurityProfile.Basic256Sha256Sign,
                    OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt,
                },
                AutoAcceptUntrustedClientCertificates = true,
            },
            NullLogger<OpcUaApplicationHost>.Instance);

        await host.StartAsync(new StandardServer(), Ct);

        var config = host.ApplicationInstance!.ApplicationConfiguration;
        config.ServerConfiguration.SecurityPolicies.Count.ShouldBe(3);
        config.ServerConfiguration.UserTokenPolicies.Count.ShouldBe(2);
        config.SecurityConfiguration.AutoAcceptUntrustedCertificates.ShouldBeTrue();

        var modes = config.ServerConfiguration.SecurityPolicies
            .Select(p => p.SecurityMode)
            .OrderBy(m => (int)m)
            .ToArray();
        modes.ShouldBe(new[] { MessageSecurityMode.None, MessageSecurityMode.Sign, MessageSecurityMode.SignAndEncrypt });
    }

    /// <summary>
    /// Verifies that StartAsync with only SignAndEncrypt omits the None endpoint.
    /// </summary>
    [Fact]
    public async Task StartAsync_with_only_signandencrypt_omits_None_endpoint()
    {
        await using var host = new OpcUaApplicationHost(
            new OpcUaApplicationHostOptions
            {
                ApplicationName = "OtOpcUa.SecHardened",
                ApplicationUri = $"urn:OtOpcUa.SecHardened:{Guid.NewGuid():N}",
                OpcUaPort = AllocateFreePort(),
                PublicHostname = "localhost",
                PkiStoreRoot = _pkiRoot,
                EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt },
                AutoAcceptUntrustedClientCertificates = false,
            },
            NullLogger<OpcUaApplicationHost>.Instance);

        await host.StartAsync(new StandardServer(), Ct);

        var policies = host.ApplicationInstance!.ApplicationConfiguration.ServerConfiguration.SecurityPolicies;
        policies.Count.ShouldBe(1);
        policies[0].SecurityMode.ShouldBe(MessageSecurityMode.SignAndEncrypt);
        policies[0].SecurityPolicyUri.ShouldBe(SecurityPolicies.Basic256Sha256);
        host.ApplicationInstance.ApplicationConfiguration.SecurityConfiguration
            .AutoAcceptUntrustedCertificates.ShouldBeFalse();
    }

    private static int AllocateFreePort()
    {
        using var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0);
        listener.Start();
        var port = ((System.Net.IPEndPoint)listener.LocalEndpoint).Port;
        listener.Stop();
        return port;
    }

    /// <summary>
    /// Cleans up temporary PKI files.
    /// </summary>
    public void Dispose()
    {
        if (Directory.Exists(_pkiRoot))
        {
            try { Directory.Delete(_pkiRoot, recursive: true); }
            catch { /* best-effort cleanup */ }
        }
    }
}
