// 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 System.Net;
using System.Text.Json;
using Shouldly;
using ZB.MOM.NatsNet.Server;
using ZB.MOM.NatsNet.Server.Internal;

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

/// <summary>
/// Tests for <see cref="ServerUtilities"/>.
/// Mirrors server/util_test.go: TestParseSize (ID 3061), TestParseSInt64 (ID 3062),
/// TestParseHostPort (ID 3063), TestURLsAreEqual (ID 3064), TestComma (ID 3065),
/// TestURLRedaction (ID 3066), TestVersionAtLeast (ID 3067).
/// Benchmarks (IDs 3068–3073) are n/a.
/// </summary>
public sealed class ServerUtilitiesTests
{
    [Fact]
    public void ParseSize_ShouldParseValidAndRejectInvalid()
    {
        // Mirror: TestParseSize
        ServerUtilities.ParseSize(ReadOnlySpan<byte>.Empty).ShouldBe(-1, "nil/empty should return -1");

        var n = "12345678"u8;
        ServerUtilities.ParseSize(n).ShouldBe(12345678);

        var bad = "12345invalid678"u8;
        ServerUtilities.ParseSize(bad).ShouldBe(-1, "non-digit chars should return -1");
    }

    [Fact]
    public void ParseInt64_ShouldParseValidAndRejectInvalid()
    {
        // Mirror: TestParseSInt64
        ServerUtilities.ParseInt64(ReadOnlySpan<byte>.Empty).ShouldBe(-1L, "empty should return -1");

        var n = "12345678"u8;
        ServerUtilities.ParseInt64(n).ShouldBe(12345678L);

        var bad = "12345invalid678"u8;
        ServerUtilities.ParseInt64(bad).ShouldBe(-1L, "non-digit chars should return -1");
    }

    [Fact]
    public void ParseHostPort_ShouldSplitCorrectly()
    {
        // Mirror: TestParseHostPort
        void Check(string hostPort, int defaultPort, string expectedHost, int expectedPort, bool expectError)
        {
            var (host, port, err) = ServerUtilities.ParseHostPort(hostPort, defaultPort);
            if (expectError)
            {
                err.ShouldNotBeNull($"expected error for hostPort={hostPort}");
                return;
            }
            err.ShouldBeNull($"unexpected error for hostPort={hostPort}: {err?.Message}");
            host.ShouldBe(expectedHost);
            port.ShouldBe(expectedPort);
        }

        Check("addr:1234",     5678, "addr", 1234, false);
        Check(" addr:1234 ",   5678, "addr", 1234, false);
        Check(" addr : 1234 ", 5678, "addr", 1234, false);
        Check("addr",          5678, "addr", 5678, false); // no port → default
        Check(" addr ",        5678, "addr", 5678, false);
        Check("addr:-1",       5678, "addr", 5678, false); // -1 → default
        Check(" addr:-1 ",     5678, "addr", 5678, false);
        Check(" addr : -1 ",   5678, "addr", 5678, false);
        Check("addr:0",        5678, "addr", 5678, false); // 0 → default
        Check(" addr:0 ",      5678, "addr", 5678, false);
        Check(" addr : 0 ",    5678, "addr", 5678, false);
        Check("addr:addr",     0,    "",     0,    true);   // non-numeric port
        Check("addr:::1234",   0,    "",     0,    true);   // ambiguous colons
        Check("",              0,    "",     0,    true);   // empty
    }

    [Fact]
    public void UrlsAreEqual_ShouldCompareCorrectly()
    {
        // Mirror: TestURLsAreEqual
        void Check(string u1Str, string u2Str, bool expectedSame)
        {
            var u1 = new Uri(u1Str);
            var u2 = new Uri(u2Str);
            ServerUtilities.UrlsAreEqual(u1, u2).ShouldBe(expectedSame,
                $"expected {u1Str} and {u2Str} to be {(expectedSame ? "equal" : "different")}");
        }

        Check("nats://localhost:4222",               "nats://localhost:4222",               true);
        Check("nats://ivan:pwd@localhost:4222",       "nats://ivan:pwd@localhost:4222",       true);
        Check("nats://ivan@localhost:4222",           "nats://ivan@localhost:4222",           true);
        Check("nats://ivan:@localhost:4222",          "nats://ivan:@localhost:4222",          true);
        Check("nats://host1:4222",                    "nats://host2:4222",                    false);
    }

    [Fact]
    public void Comma_ShouldFormatWithThousandSeparators()
    {
        // Mirror: TestComma
        var cases = new (long input, string expected)[]
        {
            (0,                          "0"),
            (10,                         "10"),
            (100,                        "100"),
            (1_000,                      "1,000"),
            (10_000,                     "10,000"),
            (100_000,                    "100,000"),
            (10_000_000,                 "10,000,000"),
            (10_100_000,                 "10,100,000"),
            (10_010_000,                 "10,010,000"),
            (10_001_000,                 "10,001,000"),
            (123_456_789,                "123,456,789"),
            (9_223_372_036_854_775_807L, "9,223,372,036,854,775,807"), // long.MaxValue
            (long.MinValue,              "-9,223,372,036,854,775,808"),
            (-123_456_789,               "-123,456,789"),
            (-10_100_000,                "-10,100,000"),
            (-10_010_000,                "-10,010,000"),
            (-10_001_000,                "-10,001,000"),
            (-10_000_000,                "-10,000,000"),
            (-100_000,                   "-100,000"),
            (-10_000,                    "-10,000"),
            (-1_000,                     "-1,000"),
            (-100,                       "-100"),
            (-10,                        "-10"),
        };

        foreach (var (input, expected) in cases)
            ServerUtilities.Comma(input).ShouldBe(expected, $"Comma({input})");
    }

    [Fact]
    public void UrlRedaction_ShouldReplacePasswords()
    {
        // Mirror: TestURLRedaction
        var cases = new (string full, string safe)[]
        {
            ("nats://foo:bar@example.org",      "nats://foo:xxxxx@example.org"),
            ("nats://foo@example.org",           "nats://foo@example.org"),
            ("nats://example.org",               "nats://example.org"),
            ("nats://example.org/foo?bar=1",     "nats://example.org/foo?bar=1"),
        };

        var listFull = new Uri[cases.Length];
        var listSafe = new Uri[cases.Length];

        for (var i = 0; i < cases.Length; i++)
        {
            ServerUtilities.RedactUrlString(cases[i].full).ShouldBe(cases[i].safe,
                $"RedactUrlString[{i}]");
            listFull[i] = new Uri(cases[i].full);
            listSafe[i] = new Uri(cases[i].safe);
        }

        var results = ServerUtilities.RedactUrlList(listFull);
        for (var i = 0; i < results.Length; i++)
            results[i].ToString().ShouldBe(listSafe[i].ToString(), $"RedactUrlList[{i}]");
    }

    [Fact]
    public void VersionAtLeast_ShouldReturnCorrectResult()
    {
        // Mirror: TestVersionAtLeast
        var cases = new (string version, int major, int minor, int update, bool result)[]
        {
            ("2.0.0-beta", 1, 9, 9, true),
            ("2.0.0",      1, 99, 9, true),
            ("2.2.0",      2, 1, 9, true),
            ("2.2.2",      2, 2, 2, true),
            ("2.2.2",      2, 2, 3, false),
            ("2.2.2",      2, 3, 2, false),
            ("2.2.2",      3, 2, 2, false),
            ("2.22.2",     3, 0, 0, false),
            ("2.2.22",     2, 3, 0, false),
            ("bad.version",1, 2, 3, false),
        };

        foreach (var (version, major, minor, update, expected) in cases)
        {
            ServerUtilities.VersionAtLeast(version, major, minor, update)
                .ShouldBe(expected,
                    $"VersionAtLeast({version}, {major}, {minor}, {update})");
        }
    }

    [Fact]
    public void RefCountedUrlSet_Wrappers_ShouldTrackRefCounts()
    {
        var set = new RefCountedUrlSet();
        ServerUtilities.AddUrl(set, "nats://a:4222").ShouldBeTrue();
        ServerUtilities.AddUrl(set, "nats://a:4222").ShouldBeFalse();
        ServerUtilities.AddUrl(set, "nats://b:4222").ShouldBeTrue();

        ServerUtilities.RemoveUrl(set, "nats://a:4222").ShouldBeFalse();
        ServerUtilities.RemoveUrl(set, "nats://a:4222").ShouldBeTrue();

        var urls = ServerUtilities.GetAsStringSlice(set);
        urls.Length.ShouldBe(1);
        urls[0].ShouldBe("nats://b:4222");
    }

    [Fact]
    public async Task NatsDialTimeout_ShouldConnectWithinTimeout()
    {
        using var listener = new System.Net.Sockets.TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;
        var acceptTask = listener.AcceptTcpClientAsync();

        using var client = await ServerUtilities.NatsDialTimeout(
            "tcp",
            $"127.0.0.1:{port}",
            TimeSpan.FromSeconds(2));

        client.Connected.ShouldBeTrue();
        using var accepted = await acceptTask;
        accepted.Connected.ShouldBeTrue();
    }

    [Fact]
    public void GenerateInfoJSON_ShouldEmitInfoLineWithCRLF()
    {
        var info = new ServerInfo
        {
            Id = "S1",
            Name = "n1",
            Host = "127.0.0.1",
            Port = 4222,
            Version = "2.0.0",
            Proto = 1,
            GoVersion = "go1.23",
        };

        var bytes = ServerUtilities.GenerateInfoJSON(info);
        var line = System.Text.Encoding.UTF8.GetString(bytes);
        line.ShouldStartWith("INFO ");
        line.ShouldEndWith("\r\n");

        var json = line["INFO ".Length..^2];
        var payload = JsonSerializer.Deserialize<ServerInfo>(json);
        payload.ShouldNotBeNull();
        payload!.Id.ShouldBe("S1");
    }

    [Fact]
    public async Task ParallelTaskQueue_ShouldExecuteQueuedActions()
    {
        var writer = ServerUtilities.ParallelTaskQueue(maxParallelism: 2);
        var ran = 0;
        var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);

        for (var i = 0; i < 4; i++)
        {
            var accepted = writer.TryWrite(() =>
            {
                if (Interlocked.Increment(ref ran) == 4)
                    tcs.TrySetResult();
            });
            accepted.ShouldBeTrue();
        }

        writer.TryComplete().ShouldBeTrue();
        var finished = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromSeconds(2)));
        finished.ShouldBe(tcs.Task);
        ran.ShouldBe(4);
    }
}
