// Copyright 2012-2026 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.
//
// Adapted from server/server_test.go in the NATS server Go source.
// Session 09–10: standalone unit tests for NatsServer helpers.

using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Net.Security;
using System.Threading;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Shouldly;
using Xunit;
using ZB.MOM.NatsNet.Server.Auth;
using ZB.MOM.NatsNet.Server.Internal;

namespace ZB.MOM.NatsNet.Server.Tests;

/// <summary>
/// Standalone unit tests for <see cref="NatsServer"/> helpers.
/// Tests that require a running server (listener, TLS, cluster) are marked n/a
/// and will be ported in sessions 10–23.
/// </summary>
public sealed class ServerTests
{
    [Fact]
    public void Init_CalledMultipleTimes_RemainsIdempotent()
    {
        var before = ServerConstants.GitCommit;

        ServerConstants.Init();
        ServerConstants.Init();

        ServerConstants.GitCommit.ShouldBe(before);
    }

    // =========================================================================
    // TestSemanticVersion — Test ID 2866
    // Validates that ServerConstants.Version matches semver format.
    // Mirrors Go TestSemanticVersion in server/server_test.go.
    // =========================================================================

    [Fact]
    public void Version_IsValidSemVer()
    {
        // SemVer regex: major.minor.patch with optional pre-release / build meta.
        var semVerRe = new Regex(@"^\d+\.\d+\.\d+(-\S+)?(\+\S+)?$", RegexOptions.Compiled);
        semVerRe.IsMatch(ServerConstants.Version).ShouldBeTrue(
            $"Version ({ServerConstants.Version}) is not a valid SemVer string");
    }

    // =========================================================================
    // TestProcessCommandLineArgs — Test ID 2882
    // Tests the ProcessCommandLineArgs helper.
    // The Go version uses flag.FlagSet; our C# port takes string[].
    // Mirrors Go TestProcessCommandLineArgs in server/server_test.go.
    // =========================================================================

    [Fact]
    public void ProcessCommandLineArgs_NoArgs_ReturnsFalse()
    {
        var (showVersion, showHelp, err) = NatsServer.ProcessCommandLineArgs([]);
        err.ShouldBeNull();
        showVersion.ShouldBeFalse();
        showHelp.ShouldBeFalse();
    }

    [Theory]
    [InlineData("version", true,  false)]
    [InlineData("VERSION", true,  false)]
    [InlineData("help",    false, true)]
    [InlineData("HELP",    false, true)]
    public void ProcessCommandLineArgs_KnownSubcommand_ReturnsCorrectFlags(
        string arg, bool wantVersion, bool wantHelp)
    {
        var (showVersion, showHelp, err) = NatsServer.ProcessCommandLineArgs([arg]);
        err.ShouldBeNull();
        showVersion.ShouldBe(wantVersion);
        showHelp.ShouldBe(wantHelp);
    }

    [Fact]
    public void ProcessCommandLineArgs_UnknownSubcommand_ReturnsError()
    {
        var (_, _, err) = NatsServer.ProcessCommandLineArgs(["foo"]);
        err.ShouldNotBeNull();
    }

    // =========================================================================
    // CompressionMode helpers — standalone tests for features 2976–2982
    // =========================================================================

    [Theory]
    [InlineData("off",    CompressionMode.Off)]
    [InlineData("false",  CompressionMode.Off)]
    [InlineData("accept", CompressionMode.Accept)]
    [InlineData("s2_fast", CompressionMode.S2Fast)]
    [InlineData("fast",   CompressionMode.S2Fast)]
    [InlineData("better", CompressionMode.S2Better)]
    [InlineData("best",   CompressionMode.S2Best)]
    public void ValidateAndNormalizeCompressionOption_KnownModes_NormalizesCorrectly(
        string input, string expected)
    {
        var co = new CompressionOpts { Mode = input };
        NatsServer.ValidateAndNormalizeCompressionOption(co, CompressionMode.S2Fast);
        co.Mode.ShouldBe(expected);
    }

    [Fact]
    public void ValidateAndNormalizeCompressionOption_OnAlias_MapsToChosenMode()
    {
        var co = new CompressionOpts { Mode = "on" };
        NatsServer.ValidateAndNormalizeCompressionOption(co, CompressionMode.S2Better);
        co.Mode.ShouldBe(CompressionMode.S2Better);
    }

    [Fact]
    public void ValidateAndNormalizeCompressionOption_S2Auto_UsesDefaults_WhenNoThresholds()
    {
        var co = new CompressionOpts { Mode = "s2_auto" };
        NatsServer.ValidateAndNormalizeCompressionOption(co, CompressionMode.S2Fast);
        co.Mode.ShouldBe(CompressionMode.S2Auto);
        co.RttThresholds.ShouldBe(NatsServer.DefaultCompressionS2AutoRttThresholds.ToList());
    }

    [Fact]
    public void ValidateAndNormalizeCompressionOption_UnsupportedMode_Throws()
    {
        var co = new CompressionOpts { Mode = "bogus" };
        Should.Throw<InvalidOperationException>(
            () => NatsServer.ValidateAndNormalizeCompressionOption(co, CompressionMode.S2Fast));
    }

    [Theory]
    [InlineData(5,   CompressionMode.S2Uncompressed)]  // <= 10 ms threshold
    [InlineData(25,  CompressionMode.S2Fast)]           // 10 < rtt <= 50 ms
    [InlineData(75,  CompressionMode.S2Better)]         // 50 < rtt <= 100 ms
    [InlineData(150, CompressionMode.S2Best)]           // > 100 ms
    public void SelectS2AutoModeBasedOnRtt_DefaultThresholds_CorrectMode(int rttMs, string expected)
    {
        var result = NatsServer.SelectS2AutoModeBasedOnRtt(
            TimeSpan.FromMilliseconds(rttMs),
            NatsServer.DefaultCompressionS2AutoRttThresholds);
        result.ShouldBe(expected);
    }

    [Theory]
    [InlineData(CompressionMode.Off,  CompressionMode.S2Fast, CompressionMode.Off)]
    [InlineData(CompressionMode.Accept, CompressionMode.Accept, CompressionMode.Off)]
    [InlineData(CompressionMode.S2Fast, CompressionMode.Accept, CompressionMode.S2Fast)]
    [InlineData(CompressionMode.Accept, CompressionMode.S2Fast, CompressionMode.S2Fast)]
    [InlineData(CompressionMode.Accept, CompressionMode.S2Auto, CompressionMode.S2Fast)]
    public void SelectCompressionMode_TableDriven(string local, string remote, string expected)
    {
        NatsServer.SelectCompressionMode(local, remote).ShouldBe(expected);
    }

    [Fact]
    public void SelectCompressionMode_RemoteNotSupported_ReturnsNotSupported()
    {
        NatsServer.SelectCompressionMode(CompressionMode.S2Fast, CompressionMode.NotSupported)
            .ShouldBe(CompressionMode.NotSupported);
    }

    [Fact]
    public void CompressOptsEqual_SameMode_ReturnsTrue()
    {
        var c1 = new CompressionOpts { Mode = CompressionMode.S2Fast };
        var c2 = new CompressionOpts { Mode = CompressionMode.S2Fast };
        NatsServer.CompressOptsEqual(c1, c2).ShouldBeTrue();
    }

    [Fact]
    public void CompressOptsEqual_DifferentModes_ReturnsFalse()
    {
        var c1 = new CompressionOpts { Mode = CompressionMode.S2Fast };
        var c2 = new CompressionOpts { Mode = CompressionMode.S2Best };
        NatsServer.CompressOptsEqual(c1, c2).ShouldBeFalse();
    }

    // =========================================================================
    // Validation helpers
    // =========================================================================

    [Fact]
    public void ValidateCluster_ClusterNameWithSpaces_ReturnsError()
    {
        var opts = new ServerOptions();
        opts.Cluster.Name = "bad name";
        var err = NatsServer.ValidateCluster(opts);
        err.ShouldNotBeNull();
        err.ShouldBeSameAs(ServerErrors.ErrClusterNameHasSpaces);
    }

    [Fact]
    public void ValidatePinnedCerts_ValidSha256_ReturnsNull()
    {
        var pinned = new PinnedCertSet(
            [new string('a', 64)]); // 64 hex chars
        var err = NatsServer.ValidatePinnedCerts(pinned);
        err.ShouldBeNull();
    }

    [Fact]
    public void ValidatePinnedCerts_InvalidSha256_ReturnsError()
    {
        var pinned = new PinnedCertSet(["not_a_sha256"]);
        var err = NatsServer.ValidatePinnedCerts(pinned);
        err.ShouldNotBeNull();
    }

    [Fact]
    public void MatchesPinnedCert_NullPinnedSet_ReturnsTrue()
    {
        var client = new ClientConnection(ClientKind.Client, nc: new MemoryStream());
        client.MatchesPinnedCert(null).ShouldBeTrue();
    }

    [Fact]
    public void MatchesPinnedCert_NoTlsCertificate_ReturnsFalse()
    {
        var client = new ClientConnection(ClientKind.Client, nc: new MemoryStream());
        var pinned = new PinnedCertSet([new string('a', 64)]);
        client.MatchesPinnedCert(pinned).ShouldBeFalse();
    }

    // =========================================================================
    // GetServerProto
    // =========================================================================

    [Fact]
    public void GetServerProto_DefaultOpts_ReturnsMsgTraceProto()
    {
        var opts = new ServerOptions();
        // SetBaselineOptions so OverrideProto gets default 0.
        opts.SetBaselineOptions();
        var (s, err) = NatsServer.NewServer(opts);
        err.ShouldBeNull();
        s.ShouldNotBeNull();
        s!.GetServerProto().ShouldBe(ServerProtocol.MsgTraceProto);
    }

    // =========================================================================
    // Account helpers
    // =========================================================================

    [Fact]
    public void ComputeRoutePoolIdx_PoolSizeOne_AlwaysReturnsZero()
    {
        NatsServer.ComputeRoutePoolIdx(1, "any-account").ShouldBe(0);
        NatsServer.ComputeRoutePoolIdx(0, "any-account").ShouldBe(0);
    }

    [Fact]
    public void ComputeRoutePoolIdx_PoolSizeN_ReturnsIndexInRange()
    {
        const int poolSize = 5;
        var idx = NatsServer.ComputeRoutePoolIdx(poolSize, "my-account");
        idx.ShouldBeInRange(0, poolSize - 1);
    }

    [Fact]
    public void NeedsCompression_Empty_ReturnsFalse()
        => NatsServer.NeedsCompression(string.Empty).ShouldBeFalse();

    [Fact]
    public void NeedsCompression_Off_ReturnsFalse()
        => NatsServer.NeedsCompression(CompressionMode.Off).ShouldBeFalse();

    [Fact]
    public void NeedsCompression_S2Fast_ReturnsTrue()
        => NatsServer.NeedsCompression(CompressionMode.S2Fast).ShouldBeTrue();

    [Theory]
    [InlineData(CompressionMode.S2Uncompressed, "writer_concurrency=1", "writer_uncompressed")]
    [InlineData(CompressionMode.S2Best, "writer_concurrency=1", "writer_best_compression")]
    [InlineData(CompressionMode.S2Better, "writer_concurrency=1", "writer_better_compression")]
    public void S2WriterOptions_KnownModes_ReturnExpectedOptions(
        string mode,
        string expectedFirst,
        string expectedSecond)
    {
        var options = NatsServer.S2WriterOptions(mode);

        options.ShouldNotBeNull();
        options!.ShouldBe([expectedFirst, expectedSecond]);
    }

    [Fact]
    public void S2WriterOptions_UnsupportedMode_ReturnsNull()
    {
        NatsServer.S2WriterOptions(CompressionMode.S2Fast).ShouldBeNull();
    }
}

// =============================================================================
// Session 10: Listeners, INFO JSON, TLS helpers, GetRandomIP
// =============================================================================

/// <summary>
/// Tests for session 10 features: GenerateInfoJson, TlsVersion helpers,
/// CopyInfo, and GetRandomIP.
/// </summary>
public sealed class ServerListenersTests
{
    [Fact]
    public void TlsTimeout_IncompleteHandshake_ClosesConnection()
    {
        var c = new ClientConnection(ClientKind.Client, nc: new MemoryStream());
        using var tls = new SslStream(new MemoryStream(), leaveInnerStreamOpen: false);

        c.IsClosed().ShouldBeFalse();

        NatsServer.TlsTimeout(c, tls);

        c.IsClosed().ShouldBeTrue();
    }

    [Fact]
    public void StartGoRoutine_WithLabels_InvokesSetGoRoutineLabels()
    {
        var (s, err) = NatsServer.NewServer(new ServerOptions());
        err.ShouldBeNull();
        s.ShouldNotBeNull();
        s!.Start();

        var signal = new ManualResetEventSlim(false);
        IReadOnlyList<KeyValuePair<string, string>>? observed = null;

        NatsServer.SetGoRoutineLabelsHookForTest = labels =>
        {
            observed = labels;
            signal.Set();
        };

        try
        {
            var started = s.StartGoRoutine(
                () => { },
                new Dictionary<string, string> { ["component"] = "server", ["loop"] = "tls" });

            started.ShouldBeTrue();
            signal.Wait(TimeSpan.FromSeconds(2)).ShouldBeTrue();

            observed.ShouldNotBeNull();
            observed!.ShouldContain(kv => kv.Key == "component" && kv.Value == "server");
            observed.ShouldContain(kv => kv.Key == "loop" && kv.Value == "tls");
        }
        finally
        {
            NatsServer.SetGoRoutineLabelsHookForTest = null;
            s.Shutdown();
            s.WaitForShutdown();
        }
    }

    // =========================================================================
    // GenerateInfoJson (feature 3069) — Test ID 2906
    // Mirrors Go TestServerJsonMarshalNestedStructsPanic (guards against
    // marshaller panics with nested/nullable structs).
    // =========================================================================

    [Fact]
    public void GenerateInfoJson_MinimalInfo_ProducesInfoFrame()
    {
        var info = new ServerInfo { Id = "TEST", Version = "1.0.0", Host = "0.0.0.0", Port = 4222 };
        var bytes = NatsServer.GenerateInfoJson(info);
        var text  = Encoding.UTF8.GetString(bytes);
        text.ShouldStartWith("INFO {");
        text.ShouldEndWith("}\r\n");
    }

    [Fact]
    public void GenerateInfoJson_WithConnectUrls_IncludesUrls()
    {
        var info = new ServerInfo
        {
            Id = "TEST",
            Version = "1.0.0",
            ClientConnectUrls = ["nats://127.0.0.1:4222", "nats://127.0.0.1:4223"],
        };
        var text = Encoding.UTF8.GetString(NatsServer.GenerateInfoJson(info));
        text.ShouldContain("connect_urls");
        text.ShouldContain("4222");
    }

    [Fact]
    public void GenerateInfoJson_WithSubjectPermissions_DoesNotThrow()
    {
        // Mirrors Go TestServerJsonMarshalNestedStructsPanic — guards against
        // JSON marshaller failures with nested nullable structs.
        var info = new ServerInfo
        {
            Id = "TEST",
            Version = "1.0.0",
            Import = new SubjectPermission { Allow = ["foo.>"], Deny = ["bar"] },
            Export = new SubjectPermission { Allow = ["pub.>"] },
        };
        var bytes = NatsServer.GenerateInfoJson(info);
        bytes.ShouldNotBeEmpty();
        // Strip the "INFO " prefix (5 bytes) and the trailing "\r\n" (2 bytes) to get pure JSON.
        var json = Encoding.UTF8.GetString(bytes, 5, bytes.Length - 7);
        var doc  = JsonDocument.Parse(json);
        doc.RootElement.GetProperty("import").ValueKind.ShouldBe(JsonValueKind.Object);
    }

    // =========================================================================
    // TlsVersion helpers (features 3079–3080)
    // =========================================================================

    [Theory]
    [InlineData(0x0301u, "1.0")]
    [InlineData(0x0302u, "1.1")]
    [InlineData(0x0303u, "1.2")]
    [InlineData(0x0304u, "1.3")]
    public void TlsVersion_KnownCodes_ReturnsVersionString(uint ver, string expected)
        => NatsServer.TlsVersion((ushort)ver).ShouldBe(expected);

    [Fact]
    public void TlsVersion_UnknownCode_ReturnsUnknownLabel()
        => NatsServer.TlsVersion(0xFFFF).ShouldStartWith("Unknown");

    [Theory]
    [InlineData("1.0", (ushort)0x0301)]
    [InlineData("1.1", (ushort)0x0302)]
    [InlineData("1.2", (ushort)0x0303)]
    [InlineData("1.3", (ushort)0x0304)]
    public void TlsVersionFromString_KnownStrings_ReturnsCode(string input, ushort expected)
    {
        var (ver, err) = NatsServer.TlsVersionFromString(input);
        err.ShouldBeNull();
        ver.ShouldBe(expected);
    }

    [Fact]
    public void TlsVersionFromString_UnknownString_ReturnsError()
    {
        var (_, err) = NatsServer.TlsVersionFromString("9.9");
        err.ShouldNotBeNull();
    }

    // =========================================================================
    // CopyInfo (feature 3069)
    // =========================================================================

    [Fact]
    public void CopyInfo_DeepCopiesSlices()
    {
        var (s, err) = NatsServer.NewServer(new ServerOptions());
        err.ShouldBeNull();
        s.ShouldNotBeNull();
        s!._info.ClientConnectUrls = ["nats://127.0.0.1:4222"];

        var copy = s.CopyInfo();
        copy.ClientConnectUrls.ShouldNotBeNull();
        copy.ClientConnectUrls!.ShouldBe(["nats://127.0.0.1:4222"]);
        // Mutating original slice shouldn't affect the copy.
        s._info.ClientConnectUrls = ["nats://10.0.0.1:4222"];
        copy.ClientConnectUrls[0].ShouldBe("nats://127.0.0.1:4222");
    }

    // =========================================================================
    // GetRandomIP (feature 3141) — Test ID 2895
    // Mirrors Go TestGetRandomIP.
    // =========================================================================

    private static NatsServer MakeServer()
    {
        var (s, err) = NatsServer.NewServer(new ServerOptions());
        err.ShouldBeNull();
        return s!;
    }

    [Fact]
    public async Task GetRandomIP_NoPort_ReturnsFormatError()
    {
        var s        = MakeServer();
        var resolver = Substitute.For<INetResolver>();
        var (_, err) = await s.GetRandomIP(resolver, "noport");
        err.ShouldNotBeNull();
        err!.Message.ShouldContain("port");
    }

    [Fact]
    public async Task GetRandomIP_ResolverThrows_PropagatesError()
    {
        var s        = MakeServer();
        var resolver = Substitute.For<INetResolver>();
        resolver.LookupHostAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
                .ThrowsAsync(new InvalidOperationException("on purpose"));
        var (_, err) = await s.GetRandomIP(resolver, "localhost:4222");
        err.ShouldNotBeNull();
        err!.Message.ShouldContain("on purpose");
    }

    [Fact]
    public async Task GetRandomIP_EmptyIps_ReturnsFallbackUrl()
    {
        var s        = MakeServer();
        var resolver = Substitute.For<INetResolver>();
        resolver.LookupHostAsync("localhost", Arg.Any<CancellationToken>())
                .Returns(Task.FromResult(Array.Empty<string>()));
        var (addr, err) = await s.GetRandomIP(resolver, "localhost:4222");
        err.ShouldBeNull();
        addr.ShouldBe("localhost:4222");
    }

    [Fact]
    public async Task GetRandomIP_SingleIp_ReturnsMappedAddress()
    {
        var s        = MakeServer();
        var resolver = Substitute.For<INetResolver>();
        resolver.LookupHostAsync("localhost", Arg.Any<CancellationToken>())
                .Returns(Task.FromResult(new[] { "1.2.3.4" }));
        var (addr, err) = await s.GetRandomIP(resolver, "localhost:4222");
        err.ShouldBeNull();
        addr.ShouldBe("1.2.3.4:4222");
    }

    [Fact]
    public async Task GetRandomIP_MultipleIps_AllSelectedWithinRange()
    {
        var s        = MakeServer();
        var resolver = Substitute.For<INetResolver>();
        resolver.LookupHostAsync("localhost", Arg.Any<CancellationToken>())
                .Returns(Task.FromResult(new[] { "1.2.3.4", "2.2.3.4", "3.2.3.4" }));

        var dist = new int[3];
        for (int i = 0; i < 100; i++)
        {
            var (addr, err) = await s.GetRandomIP(resolver, "localhost:4222");
            err.ShouldBeNull();
            dist[int.Parse(addr[..1]) - 1]++;
        }

        // Each IP should appear at least once and no single IP should dominate.
        foreach (var d in dist)
            d.ShouldBeGreaterThan(0);
    }

    [Fact]
    public async Task GetRandomIP_ExcludedIp_NeverReturned()
    {
        var s        = MakeServer();
        var resolver = Substitute.For<INetResolver>();
        resolver.LookupHostAsync("localhost", Arg.Any<CancellationToken>())
                .Returns(Task.FromResult(new[] { "1.2.3.4", "2.2.3.4", "3.2.3.4" }));

        var excluded = new HashSet<string> { "1.2.3.4:4222" };
        for (int i = 0; i < 100; i++)
        {
            var (addr, err) = await s.GetRandomIP(resolver, "localhost:4222", excluded);
            err.ShouldBeNull();
            addr.ShouldNotBe("1.2.3.4:4222");
        }
    }
}
