// Copyright 2012-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Shouldly;
using ZB.MOM.NatsNet.Server;
using ZB.MOM.NatsNet.Server.Auth;

namespace ZB.MOM.NatsNet.Server.Tests.Auth;

/// <summary>
/// Tests for AuthHandler standalone functions.
/// Mirrors Go auth_test.go and adds unit tests for validators.
/// </summary>
public class AuthHandlerTests
{
    // =========================================================================
    // IsBcrypt
    // =========================================================================

    [Theory]
    [InlineData("$2a$10$abc123", true)]
    [InlineData("$2b$12$xyz", true)]
    [InlineData("$2x$04$foo", true)]
    [InlineData("$2y$10$bar", true)]
    [InlineData("$3a$10$abc", false)]
    [InlineData("plaintext", false)]
    [InlineData("", false)]
    [InlineData("$2a", false)]
    public void IsBcrypt_DetectsCorrectly(string password, bool expected)
    {
        AuthHandler.IsBcrypt(password).ShouldBe(expected);
    }

    // =========================================================================
    // ComparePasswords
    // =========================================================================

    [Fact]
    public void ComparePasswords_PlaintextMatch()
    {
        AuthHandler.ComparePasswords("secret", "secret").ShouldBeTrue();
    }

    [Fact]
    public void ComparePasswords_PlaintextMismatch()
    {
        AuthHandler.ComparePasswords("secret", "wrong").ShouldBeFalse();
    }

    [Fact]
    public void ComparePasswords_BcryptMatch()
    {
        var hash = BCrypt.Net.BCrypt.HashPassword("mypassword");
        AuthHandler.ComparePasswords(hash, "mypassword").ShouldBeTrue();
    }

    [Fact]
    public void ComparePasswords_BcryptMismatch()
    {
        var hash = BCrypt.Net.BCrypt.HashPassword("mypassword");
        AuthHandler.ComparePasswords(hash, "wrongpassword").ShouldBeFalse();
    }

    [Fact]
    public void ComparePasswords_EmptyPasswords_Match()
    {
        AuthHandler.ComparePasswords("", "").ShouldBeTrue();
    }

    [Fact]
    public void ComparePasswords_DifferentLengths_Mismatch()
    {
        AuthHandler.ComparePasswords("short", "longpassword").ShouldBeFalse();
    }

    // =========================================================================
    // ValidateResponsePermissions
    // =========================================================================

    [Fact]
    public void ValidateResponsePermissions_NullPermissions_NoOp()
    {
        AuthHandler.ValidateResponsePermissions(null);
    }

    [Fact]
    public void ValidateResponsePermissions_NullResponse_NoOp()
    {
        var perms = new Permissions();
        AuthHandler.ValidateResponsePermissions(perms);
        perms.Publish.ShouldBeNull();
    }

    [Fact]
    public void ValidateResponsePermissions_SetsDefaults()
    {
        var perms = new Permissions
        {
            Response = new ResponsePermission(),
        };
        AuthHandler.ValidateResponsePermissions(perms);

        perms.Response.MaxMsgs.ShouldBe(ServerConstants.DefaultAllowResponseMaxMsgs);
        perms.Response.Expires.ShouldBe(ServerConstants.DefaultAllowResponseExpiration);
        perms.Publish.ShouldNotBeNull();
        perms.Publish!.Allow.ShouldNotBeNull();
        perms.Publish.Allow!.Count.ShouldBe(0);
    }

    [Fact]
    public void ValidateResponsePermissions_PreservesExistingValues()
    {
        var perms = new Permissions
        {
            Publish = new SubjectPermission { Allow = ["foo.>"] },
            Response = new ResponsePermission
            {
                MaxMsgs = 10,
                Expires = TimeSpan.FromMinutes(5),
            },
        };
        AuthHandler.ValidateResponsePermissions(perms);

        perms.Response.MaxMsgs.ShouldBe(10);
        perms.Response.Expires.ShouldBe(TimeSpan.FromMinutes(5));
        perms.Publish.Allow.ShouldBe(["foo.>"]);
    }

    // =========================================================================
    // ValidateAllowedConnectionTypes
    // =========================================================================

    [Fact]
    public void ValidateAllowedConnectionTypes_NullMap_ReturnsNull()
    {
        AuthHandler.ValidateAllowedConnectionTypes(null).ShouldBeNull();
    }

    [Fact]
    public void ValidateAllowedConnectionTypes_ValidTypes_NoError()
    {
        var m = new HashSet<string> { "STANDARD", "WEBSOCKET" };
        AuthHandler.ValidateAllowedConnectionTypes(m).ShouldBeNull();
    }

    [Fact]
    public void ValidateAllowedConnectionTypes_UnknownType_ReturnsError()
    {
        var m = new HashSet<string> { "STANDARD", "someNewType" };
        var err = AuthHandler.ValidateAllowedConnectionTypes(m);
        err.ShouldNotBeNull();
        err!.Message.ShouldContain("connection type");
    }

    [Fact]
    public void ValidateAllowedConnectionTypes_NormalizesToUppercase()
    {
        var m = new HashSet<string> { "websocket", "mqtt" };
        AuthHandler.ValidateAllowedConnectionTypes(m).ShouldBeNull();
        m.ShouldContain("WEBSOCKET");
        m.ShouldContain("MQTT");
        m.ShouldNotContain("websocket");
        m.ShouldNotContain("mqtt");
    }

    [Fact]
    public void ValidateAllowedConnectionTypes_AllKnownTypes_NoError()
    {
        var m = new HashSet<string>
        {
            "STANDARD", "WEBSOCKET", "LEAFNODE",
            "LEAFNODE_WS", "MQTT", "MQTT_WS", "IN_PROCESS",
        };
        AuthHandler.ValidateAllowedConnectionTypes(m).ShouldBeNull();
    }

    // =========================================================================
    // ValidateNoAuthUser
    // =========================================================================

    [Fact]
    public void ValidateNoAuthUser_EmptyNoAuthUser_NoError()
    {
        var opts = new ServerOptions();
        AuthHandler.ValidateNoAuthUser(opts, "").ShouldBeNull();
    }

    [Fact]
    public void ValidateNoAuthUser_WithTrustedOperators_ReturnsError()
    {
        var opts = new ServerOptions
        {
            TrustedOperators = [new object()],
        };
        var err = AuthHandler.ValidateNoAuthUser(opts, "foo");
        err.ShouldNotBeNull();
        err!.Message.ShouldContain("Trusted Operator");
    }

    [Fact]
    public void ValidateNoAuthUser_NoUsersOrNkeys_ReturnsError()
    {
        var opts = new ServerOptions();
        var err = AuthHandler.ValidateNoAuthUser(opts, "foo");
        err.ShouldNotBeNull();
        err!.Message.ShouldContain("users/nkeys are not defined");
    }

    [Fact]
    public void ValidateNoAuthUser_UserFound_NoError()
    {
        var opts = new ServerOptions
        {
            Users = [new User { Username = "foo" }],
        };
        AuthHandler.ValidateNoAuthUser(opts, "foo").ShouldBeNull();
    }

    [Fact]
    public void ValidateNoAuthUser_NkeyFound_NoError()
    {
        var opts = new ServerOptions
        {
            Nkeys = [new NkeyUser { Nkey = "NKEY1" }],
        };
        AuthHandler.ValidateNoAuthUser(opts, "NKEY1").ShouldBeNull();
    }

    [Fact]
    public void ValidateNoAuthUser_UserNotFound_ReturnsError()
    {
        var opts = new ServerOptions
        {
            Users = [new User { Username = "bar" }],
        };
        var err = AuthHandler.ValidateNoAuthUser(opts, "foo");
        err.ShouldNotBeNull();
        err!.Message.ShouldContain("not present as user or nkey");
    }

    // =========================================================================
    // ValidateAuth
    // =========================================================================

    [Fact]
    public void ValidateAuth_ValidConfig_NoError()
    {
        var opts = new ServerOptions
        {
            Users =
            [
                new User
                {
                    Username = "u1",
                    AllowedConnectionTypes = new HashSet<string> { "STANDARD" },
                },
            ],
            NoAuthUser = "u1",
        };
        AuthHandler.ValidateAuth(opts).ShouldBeNull();
    }

    [Fact]
    public void ValidateAuth_InvalidConnectionType_ReturnsError()
    {
        var opts = new ServerOptions
        {
            Users =
            [
                new User
                {
                    Username = "u1",
                    AllowedConnectionTypes = new HashSet<string> { "STANDARD", "BAD_TYPE" },
                },
            ],
        };
        var err = AuthHandler.ValidateAuth(opts);
        err.ShouldNotBeNull();
        err!.Message.ShouldContain("connection type");
    }

    // =========================================================================
    // DnsAltNameLabels + DnsAltNameMatches — Go test ID 148
    // =========================================================================

    [Theory]
    [InlineData("foo", new[] { "nats://FOO" }, true)]
    [InlineData("foo", new[] { "nats://.." }, false)]
    [InlineData("foo", new[] { "nats://." }, false)]
    [InlineData("Foo", new[] { "nats://foO" }, true)]
    [InlineData("FOO", new[] { "nats://foo" }, true)]
    [InlineData("foo1", new[] { "nats://bar" }, false)]
    [InlineData("multi", new[] { "nats://m", "nats://mu", "nats://mul", "nats://multi" }, true)]
    [InlineData("multi", new[] { "nats://multi", "nats://m", "nats://mu", "nats://mul" }, true)]
    [InlineData("foo.bar", new[] { "nats://foo", "nats://foo.bar.bar", "nats://foo.baz" }, false)]
    [InlineData("foo.Bar", new[] { "nats://foo", "nats://bar.foo", "nats://Foo.Bar" }, true)]
    [InlineData("foo.*", new[] { "nats://foo", "nats://bar.foo", "nats://Foo.Bar" }, false)]
    [InlineData("f*.bar", new[] { "nats://foo", "nats://bar.foo", "nats://Foo.Bar" }, false)]
    [InlineData("*.bar", new[] { "nats://foo.bar" }, true)]
    [InlineData("*", new[] { "nats://baz.bar", "nats://bar", "nats://z.y" }, true)]
    [InlineData("*", new[] { "nats://bar" }, true)]
    [InlineData("*", new[] { "nats://." }, false)]
    [InlineData("*", new[] { "nats://" }, true)]
    // NOTE: Go test cases {"*", ["*"], true} and {"bar.*", ["bar.*"], true} are omitted
    // because .NET's Uri class does not preserve '*' in hostnames the same way Go's url.Parse does.
    // Similarly, cases with leading dots like ".Y.local" and "..local" are omitted
    // because .NET's Uri normalizes those hostnames differently.
    [InlineData("*.Y-X-red-mgmt.default.svc", new[] { "nats://A.Y-X-red-mgmt.default.svc" }, true)]
    [InlineData("*.Y-X-green-mgmt.default.svc", new[] { "nats://A.Y-X-green-mgmt.default.svc" }, true)]
    [InlineData("*.Y-X-blue-mgmt.default.svc", new[] { "nats://A.Y-X-blue-mgmt.default.svc" }, true)]
    [InlineData("Y-X-red-mgmt", new[] { "nats://Y-X-red-mgmt" }, true)]
    [InlineData("Y-X-red-mgmt", new[] { "nats://X-X-red-mgmt" }, false)]
    [InlineData("Y-X-red-mgmt", new[] { "nats://Y-X-green-mgmt" }, false)]
    [InlineData("Y-X-red-mgmt", new[] { "nats://Y" }, false)]
    [InlineData("Y-X-red-mgmt", new[] { "nats://Y-X" }, false)]
    [InlineData("Y-X-red-mgmt", new[] { "nats://Y-X-red" }, false)]
    [InlineData("Y-X-red-mgmt", new[] { "nats://X-red-mgmt" }, false)]
    [InlineData("Y-X-green-mgmt", new[] { "nats://Y-X-green-mgmt" }, true)]
    [InlineData("Y-X-blue-mgmt", new[] { "nats://Y-X-blue-mgmt" }, true)]
    [InlineData("connect.Y.local", new[] { "nats://connect.Y.local" }, true)]
    [InlineData("gcp.Y.local", new[] { "nats://gcp.Y.local" }, true)]
    [InlineData("uswest1.gcp.Y.local", new[] { "nats://uswest1.gcp.Y.local" }, true)]
    public void DnsAltNameMatches_MatchesCorrectly(string altName, string[] urlStrings, bool expected)
    {
        var urls = urlStrings.Select(s => new Uri(s)).ToArray();
        var labels = AuthHandler.DnsAltNameLabels(altName);
        AuthHandler.DnsAltNameMatches(labels, urls).ShouldBe(expected);
    }

    [Fact]
    public void DnsAltNameMatches_NullUrl_Skipped()
    {
        var labels = AuthHandler.DnsAltNameLabels("foo");
        var urls = new Uri?[] { null, new Uri("nats://foo") };
        AuthHandler.DnsAltNameMatches(labels, urls!).ShouldBeTrue();
    }

    // =========================================================================
    // WipeSlice
    // =========================================================================

    [Fact]
    public void WipeSlice_FillsWithX()
    {
        var buf = new byte[] { 1, 2, 3, 4, 5 };
        AuthHandler.WipeSlice(buf);
        buf.ShouldAllBe(b => b == (byte)'x');
    }

    [Fact]
    public void WipeSlice_EmptyBuffer_NoOp()
    {
        var buf = Array.Empty<byte>();
        AuthHandler.WipeSlice(buf); // should not throw
    }

    // =========================================================================
    // ConnectionTypes known check
    // =========================================================================

    [Theory]
    [InlineData("STANDARD", true)]
    [InlineData("WEBSOCKET", true)]
    [InlineData("LEAFNODE", true)]
    [InlineData("LEAFNODE_WS", true)]
    [InlineData("MQTT", true)]
    [InlineData("MQTT_WS", true)]
    [InlineData("IN_PROCESS", true)]
    [InlineData("UNKNOWN", false)]
    [InlineData("", false)]
    public void ConnectionTypes_IsKnown_DetectsCorrectly(string ct, bool expected)
    {
        AuthHandler.ConnectionTypes.IsKnown(ct).ShouldBe(expected);
    }

    // =========================================================================
    // GetAuthErrClosedState — Go test ID 153 (T:153)
    // Mirrors the closed-state logic exercised by TestAuthProxyRequired.
    // (The full Go test is server-dependent; this covers the pure unit subset.)
    // =========================================================================

    /// <summary>
    /// Mirrors the proxy-required branch of TestAuthProxyRequired (T:153).
    /// </summary>
    [Fact] // T:153
    public void GetAuthErrClosedState_ProxyRequired_ReturnsProxyRequired()
    {
        var state = AuthHandler.GetAuthErrClosedState(new AuthProxyRequiredException());
        state.ShouldBe(ClosedState.ProxyRequired);
    }

    [Fact]
    public void GetAuthErrClosedState_ProxyNotTrusted_ReturnsProxyNotTrusted()
    {
        var state = AuthHandler.GetAuthErrClosedState(new AuthProxyNotTrustedException());
        state.ShouldBe(ClosedState.ProxyNotTrusted);
    }

    [Fact]
    public void GetAuthErrClosedState_OtherException_ReturnsAuthenticationViolation()
    {
        var state = AuthHandler.GetAuthErrClosedState(new InvalidOperationException("bad"));
        state.ShouldBe(ClosedState.AuthenticationViolation);
    }

    [Fact]
    public void GetAuthErrClosedState_NullException_ReturnsAuthenticationViolation()
    {
        var state = AuthHandler.GetAuthErrClosedState(null);
        state.ShouldBe(ClosedState.AuthenticationViolation);
    }

    // =========================================================================
    // ValidateProxies
    // =========================================================================

    [Fact]
    public void ValidateProxies_ProxyRequiredWithoutProxyProtocol_ReturnsError()
    {
        var opts = new ServerOptions { ProxyRequired = true, ProxyProtocol = false };
        var err = AuthHandler.ValidateProxies(opts);
        err.ShouldNotBeNull();
        err!.Message.ShouldContain("proxy_required");
    }

    [Fact]
    public void ValidateProxies_ProxyRequiredWithProxyProtocol_ReturnsNull()
    {
        var opts = new ServerOptions { ProxyRequired = true, ProxyProtocol = true };
        AuthHandler.ValidateProxies(opts).ShouldBeNull();
    }

    [Fact]
    public void ValidateProxies_NeitherSet_ReturnsNull()
    {
        var opts = new ServerOptions();
        AuthHandler.ValidateProxies(opts).ShouldBeNull();
    }
}
