// 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/client_test.go in the NATS server Go source.

using System.Text;
using System.Linq;
using Shouldly;
using Xunit;
using ZB.MOM.NatsNet.Server.Auth;
using ZB.MOM.NatsNet.Server.Internal;
using ZB.MOM.NatsNet.Server.Protocol;

namespace ZB.MOM.NatsNet.Server.Tests;

/// <summary>
/// Standalone unit tests for <see cref="ClientConnection"/> helper functions.
/// Adapted from server/client_test.go.
/// </summary>
public sealed class ClientTests
{
    // =========================================================================
    // TestSplitSubjectQueue — Test ID 200
    // =========================================================================

    [Theory]
    [InlineData("foo",        "foo", null,  false)]
    [InlineData("foo bar",    "foo", "bar", false)]
    [InlineData(" foo bar ",  "foo", "bar", false)]
    [InlineData("foo  bar",   "foo", "bar", false)]
    [InlineData("foo  bar fizz", null, null, true)]
    public void SplitSubjectQueue_TableDriven(
        string sq, string? wantSubject, string? wantQueue, bool wantErr)
    {
        if (wantErr)
        {
            Should.Throw<Exception>(() => ClientConnection.SplitSubjectQueue(sq));
        }
        else
        {
            var (subject, queue) = ClientConnection.SplitSubjectQueue(sq);
            subject.ShouldBe(wantSubject is null ? null : Encoding.ASCII.GetBytes(wantSubject));
            queue.ShouldBe(wantQueue is null ? null : Encoding.ASCII.GetBytes(wantQueue));
        }
    }

    // =========================================================================
    // TestTypeString — Test ID 201
    // =========================================================================

    [Theory]
    [InlineData(ClientKind.Client,    "Client")]
    [InlineData(ClientKind.Router,    "Router")]
    [InlineData(ClientKind.Gateway,   "Gateway")]
    [InlineData(ClientKind.Leaf,      "Leafnode")]
    [InlineData(ClientKind.JetStream, "JetStream")]
    [InlineData(ClientKind.Account,   "Account")]
    [InlineData(ClientKind.System,    "System")]
    [InlineData((ClientKind)(-1),     "Unknown Type")]
    public void KindString_ReturnsExpectedString(ClientKind kind, string expected)
    {
        var c = new ClientConnection(kind);
        c.KindString().ShouldBe(expected);
    }

    [Fact]
    public void IsInternalClient_SystemJetStreamAccount_ShouldBeTrue()
    {
        ClientKindHelpers.IsInternalClient(ClientKind.System).ShouldBeTrue();
        ClientKindHelpers.IsInternalClient(ClientKind.JetStream).ShouldBeTrue();
        ClientKindHelpers.IsInternalClient(ClientKind.Account).ShouldBeTrue();
        ClientKindHelpers.IsInternalClient(ClientKind.Client).ShouldBeFalse();
    }

    [Fact]
    public void ClientFlags_SetClearIsSetSetIfNotSet_ShouldBehave()
    {
        var flags = ClientFlags.None;
        flags = flags.Set(ClientFlags.ConnectReceived);
        flags.IsSet(ClientFlags.ConnectReceived).ShouldBeTrue();

        ClientFlagExtensions.SetIfNotSet(ref flags, ClientFlags.ConnectReceived).ShouldBeFalse();
        ClientFlagExtensions.SetIfNotSet(ref flags, ClientFlags.InfoReceived).ShouldBeTrue();
        flags.IsSet(ClientFlags.InfoReceived).ShouldBeTrue();

        flags = flags.Clear(ClientFlags.ConnectReceived);
        flags.IsSet(ClientFlags.ConnectReceived).ShouldBeFalse();
    }

    [Fact]
    public void ReadCacheFlags_SetClearIsSet_ShouldBehave()
    {
        var flags = ReadCacheFlags.None;
        flags = flags.Set(ReadCacheFlags.HasMappings);
        flags.IsSet(ReadCacheFlags.HasMappings).ShouldBeTrue();
        flags = flags.Clear(ReadCacheFlags.HasMappings);
        flags.IsSet(ReadCacheFlags.HasMappings).ShouldBeFalse();
    }

    [Fact]
    public void WriteTimeoutPolicy_String_ShouldMatchGoValues()
    {
        WriteTimeoutPolicy.Close.String().ShouldBe("close");
        WriteTimeoutPolicy.Retry.String().ShouldBe("retry");
        WriteTimeoutPolicy.Default.String().ShouldBe(string.Empty);
    }

    [Fact]
    public void NbPool_GetPut_ShouldReturnExpectedBucketSizes()
    {
        var small = NbPool.Get(10);
        var medium = NbPool.Get(NbPool.SmallSize + 1);
        var large = NbPool.Get(NbPool.MediumSize + 1);

        small.Length.ShouldBeGreaterThanOrEqualTo(NbPool.SmallSize);
        medium.Length.ShouldBeGreaterThanOrEqualTo(NbPool.MediumSize);
        large.Length.ShouldBeGreaterThanOrEqualTo(NbPool.LargeSize);

        NbPool.Put(small);
        NbPool.Put(medium);
        NbPool.Put(large);
    }

    [Fact]
    public void Connection_StringKindAndTlsAccessors_ShouldReflectState()
    {
        var c = new ClientConnection(ClientKind.Router);
        c.GetKind().ShouldBe(ClientKind.Router);
        c.String().ShouldBe(string.Empty);
        c.GetTLSConnectionState().ShouldBeNull();
    }

    [Fact]
    public void PublicPermissions_MergeAndFilters_ShouldBehave()
    {
        var c = new ClientConnection(ClientKind.Client);
        c.RegisterUser(new User
        {
            Permissions = new Permissions
            {
                Publish = new SubjectPermission { Allow = ["foo"], Deny = ["deny.once"] },
                Subscribe = new SubjectPermission { Allow = ["bar"], Deny = ["sub.deny"] },
                Response = new ResponsePermission { MaxMsgs = 10, Expires = TimeSpan.FromSeconds(1) },
            },
        });

        var initial = c.PublicPermissions();
        initial.ShouldNotBeNull();
        initial!.Publish!.Allow.ShouldContain("foo");
        initial.Publish.Deny.ShouldContain("deny.once");
        initial.Subscribe!.Allow.ShouldContain("bar");
        initial.Subscribe.Deny.ShouldContain("sub.deny");
        initial.Response.ShouldNotBeNull();

        c.MergeDenyPermissions(DenyType.Pub, ["deny.once", "deny.two"]);
        c.MergeDenyPermissionsLocked(DenyType.Sub, ["sub.two"]);

        var merged = c.PublicPermissions();
        merged.ShouldNotBeNull();
        merged!.Publish!.Deny!.Count(s => s == "deny.once").ShouldBe(1);
        merged.Publish.Deny.ShouldContain("deny.two");
        merged.Subscribe!.Deny.ShouldContain("sub.two");

        c.LoadMsgDenyFilter();
        c.MPerms.ShouldNotBeNull();
    }

    [Fact]
    public void SetExpiration_WithValidForAndPastClaims_ShouldUseValidForAndCloseWhenPast()
    {
        var c = new ClientConnection(ClientKind.Client);
        c.SetExpiration(0, TimeSpan.FromMilliseconds(30));
        c.IsClosed().ShouldBeFalse();

        var wait = SpinWait.SpinUntil(c.IsClosed, TimeSpan.FromSeconds(2));
        wait.ShouldBeTrue();

        var c2 = new ClientConnection(ClientKind.Client);
        c2.SetExpiration(DateTimeOffset.UtcNow.AddSeconds(-1).ToUnixTimeSeconds(), TimeSpan.Zero);
        SpinWait.SpinUntil(c2.IsClosed, TimeSpan.FromSeconds(2)).ShouldBeTrue();
    }

    [Fact]
    public void ReplyHelpers_ServiceAndReserved_ShouldClassifyPrefixes()
    {
        ClientConnection.IsServiceReply(Encoding.ASCII.GetBytes("_R_.A.B")).ShouldBeTrue();
        ClientConnection.IsServiceReply(Encoding.ASCII.GetBytes("foo.bar")).ShouldBeFalse();
    }
}

/// <summary>
/// Standalone unit tests for <see cref="NatsMessageHeaders"/> functions.
/// Adapted from server/client_test.go (header utility tests).
/// </summary>
public sealed class NatsMessageHeadersTests
{
    // =========================================================================
    // TestRemoveHeaderIfPrefixPresent — Test ID 247
    // =========================================================================

    [Fact]
    public void RemoveHeaderIfPrefixPresent_RemovesMatchingHeaders()
    {
        byte[]? hdr = null;
        hdr = NatsMessageHeaders.GenHeader(hdr, "a", "1");
        hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedStream, "my-stream");
        hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSeq, "22");
        hdr = NatsMessageHeaders.GenHeader(hdr, "b", "2");
        hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeq, "24");
        hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastMsgId, "1");
        hdr = NatsMessageHeaders.GenHeader(hdr, "c", "3");

        hdr = NatsMessageHeaders.RemoveHeaderIfPrefixPresent(hdr!, "Nats-Expected-");

        var expected = Encoding.ASCII.GetBytes("NATS/1.0\r\na: 1\r\nb: 2\r\nc: 3\r\n\r\n");
        hdr.ShouldBe(expected);
    }

    // =========================================================================
    // TestSliceHeader — Test ID 248
    // =========================================================================

    [Fact]
    public void SliceHeader_ReturnsCorrectSlice()
    {
        byte[]? hdr = null;
        hdr = NatsMessageHeaders.GenHeader(hdr, "a", "1");
        hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedStream, "my-stream");
        hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSeq, "22");
        hdr = NatsMessageHeaders.GenHeader(hdr, "b", "2");
        hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeq, "24");
        hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastMsgId, "1");
        hdr = NatsMessageHeaders.GenHeader(hdr, "c", "3");

        var sliced = NatsMessageHeaders.SliceHeader(NatsHeaderConstants.JsExpectedLastSubjSeq, hdr!);
        var copied = NatsMessageHeaders.GetHeader(NatsHeaderConstants.JsExpectedLastSubjSeq, hdr!);

        sliced.ShouldNotBeNull();
        sliced!.Value.Length.ShouldBe(2); // "24" is 2 bytes
        copied.ShouldNotBeNull();
        sliced.Value.ToArray().ShouldBe(copied!);
    }

    // =========================================================================
    // TestSliceHeaderOrderingPrefix — Test ID 249
    // =========================================================================

    [Fact]
    public void SliceHeader_OrderingPrefix_LongerHeaderDoesNotPreemptShorter()
    {
        byte[]? hdr = null;
        hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeqSubj, "foo");
        hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeq, "24");

        var sliced = NatsMessageHeaders.SliceHeader(NatsHeaderConstants.JsExpectedLastSubjSeq, hdr!);
        var copied = NatsMessageHeaders.GetHeader(NatsHeaderConstants.JsExpectedLastSubjSeq, hdr!);

        sliced.ShouldNotBeNull();
        sliced!.Value.Length.ShouldBe(2);
        copied.ShouldNotBeNull();
        sliced.Value.ToArray().ShouldBe(copied!);
    }

    // =========================================================================
    // TestSliceHeaderOrderingSuffix — Test ID 250
    // =========================================================================

    [Fact]
    public void SliceHeader_OrderingSuffix_LongerHeaderDoesNotPreemptShorter()
    {
        byte[]? hdr = null;
        hdr = NatsMessageHeaders.GenHeader(hdr, "Previous-Nats-Msg-Id", "user");
        hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsMsgId, "control");

        var sliced = NatsMessageHeaders.SliceHeader(NatsHeaderConstants.JsMsgId, hdr!);
        var copied = NatsMessageHeaders.GetHeader(NatsHeaderConstants.JsMsgId, hdr!);

        sliced.ShouldNotBeNull();
        copied.ShouldNotBeNull();
        sliced!.Value.ToArray().ShouldBe(copied!);
        Encoding.ASCII.GetString(copied!).ShouldBe("control");
    }

    // =========================================================================
    // TestRemoveHeaderIfPresentOrderingPrefix — Test ID 251
    // =========================================================================

    [Fact]
    public void RemoveHeaderIfPresent_OrderingPrefix_OnlyRemovesExactKey()
    {
        byte[]? hdr = null;
        hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeqSubj, "foo");
        hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeq, "24");

        hdr = NatsMessageHeaders.RemoveHeaderIfPresent(hdr!, NatsHeaderConstants.JsExpectedLastSubjSeq);
        var expected = NatsMessageHeaders.GenHeader(null, NatsHeaderConstants.JsExpectedLastSubjSeqSubj, "foo");
        hdr!.ShouldBe(expected);
    }

    // =========================================================================
    // TestRemoveHeaderIfPresentOrderingSuffix — Test ID 252
    // =========================================================================

    [Fact]
    public void RemoveHeaderIfPresent_OrderingSuffix_OnlyRemovesExactKey()
    {
        byte[]? hdr = null;
        hdr = NatsMessageHeaders.GenHeader(hdr, "Previous-Nats-Msg-Id", "user");
        hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsMsgId, "control");

        hdr = NatsMessageHeaders.RemoveHeaderIfPresent(hdr!, NatsHeaderConstants.JsMsgId);
        var expected = NatsMessageHeaders.GenHeader(null, "Previous-Nats-Msg-Id", "user");
        hdr!.ShouldBe(expected);
    }

    // =========================================================================
    // TestMsgPartsCapsHdrSlice — Test ID 253
    // =========================================================================

    [Fact]
    public void MsgParts_HeaderSliceIsIsolatedCopy()
    {
        const string hdrContent = NatsHeaderConstants.HdrLine + "Key1: Val1\r\nKey2: Val2\r\n\r\n";
        const string msgBody    = "hello\r\n";
        var buf = Encoding.ASCII.GetBytes(hdrContent + msgBody);

        var c = new ClientConnection(ClientKind.Client);
        c.ParseCtx.Pa.HeaderSize = hdrContent.Length;

        var (hdr, msg) = c.MsgParts(buf);

        // Header and body should have correct content.
        Encoding.ASCII.GetString(hdr).ShouldBe(hdrContent);
        Encoding.ASCII.GetString(msg).ShouldBe(msgBody);

        // hdr should be shorter than buf (cap(hdr) < cap(buf) in Go).
        hdr.Length.ShouldBeLessThan(buf.Length);

        // Appending to hdr should not affect msg.
        var extended = hdr.Concat(Encoding.ASCII.GetBytes("test")).ToArray();
        Encoding.ASCII.GetString(extended).ShouldBe(hdrContent + "test");
        Encoding.ASCII.GetString(msg).ShouldBe("hello\r\n");
    }

    // =========================================================================
    // TestSetHeaderDoesNotOverwriteUnderlyingBuffer — Test ID 254
    // =========================================================================

    [Theory]
    [InlineData("Key1", "Val1Updated", "NATS/1.0\r\nKey1: Val1Updated\r\nKey2: Val2\r\n\r\n", true)]
    [InlineData("Key1", "v1",          "NATS/1.0\r\nKey1: v1\r\nKey2: Val2\r\n\r\n",          false)]
    [InlineData("Key3", "Val3",        "NATS/1.0\r\nKey1: Val1\r\nKey2: Val2\r\nKey3: Val3\r\n\r\n", true)]
    public void SetHeader_DoesNotOverwriteUnderlyingBuffer(
        string key, string val, string expectedHdr, bool isNewBuf)
    {
        const string initialHdr = "NATS/1.0\r\nKey1: Val1\r\nKey2: Val2\r\n\r\n";
        const string msgBody    = "this is the message body\r\n";

        var buf = new byte[initialHdr.Length + msgBody.Length];
        Encoding.ASCII.GetBytes(initialHdr).CopyTo(buf, 0);
        Encoding.ASCII.GetBytes(msgBody).CopyTo(buf, initialHdr.Length);

        var hdrSlice = buf[..initialHdr.Length];
        var msgSlice = buf[initialHdr.Length..];

        var updatedHdr = NatsMessageHeaders.SetHeader(key, val, hdrSlice);

        Encoding.ASCII.GetString(updatedHdr).ShouldBe(expectedHdr);
        Encoding.ASCII.GetString(msgSlice).ShouldBe(msgBody);

        if (isNewBuf)
        {
            // New allocation: original buf's header portion must be unchanged.
            Encoding.ASCII.GetString(buf, 0, initialHdr.Length).ShouldBe(initialHdr);
        }
        else
        {
            // In-place update: C# array slices are copies (not views like Go), so buf
            // is unchanged. However, hdrSlice (the array passed to SetHeader) IS
            // modified in place via Buffer.BlockCopy.
            Encoding.ASCII.GetString(hdrSlice, 0, expectedHdr.Length).ShouldBe(expectedHdr);
        }
    }

    // =========================================================================
    // TestSetHeaderOrderingPrefix — Test ID 255
    // =========================================================================

    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public void SetHeader_OrderingPrefix_LongerHeaderDoesNotPreemptShorter(bool withSpaces)
    {
        byte[]? hdr = null;
        hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeqSubj, "foo");
        hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeq, "24");
        if (!withSpaces)
            hdr = hdr!.Where(b => b != (byte)' ').ToArray();

        hdr = NatsMessageHeaders.SetHeader(NatsHeaderConstants.JsExpectedLastSubjSeq, "12", hdr!);

        byte[]? expected = null;
        expected = NatsMessageHeaders.GenHeader(expected, NatsHeaderConstants.JsExpectedLastSubjSeqSubj, "foo");
        expected = NatsMessageHeaders.GenHeader(expected, NatsHeaderConstants.JsExpectedLastSubjSeq, "12");
        if (!withSpaces)
            expected = expected!.Where(b => b != (byte)' ').ToArray();

        hdr!.ShouldBe(expected!);
    }

    // =========================================================================
    // TestSetHeaderOrderingSuffix — Test ID 256
    // =========================================================================

    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public void SetHeader_OrderingSuffix_LongerHeaderDoesNotPreemptShorter(bool withSpaces)
    {
        byte[]? hdr = null;
        hdr = NatsMessageHeaders.GenHeader(hdr, "Previous-Nats-Msg-Id", "user");
        hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsMsgId, "control");
        if (!withSpaces)
            hdr = hdr!.Where(b => b != (byte)' ').ToArray();

        hdr = NatsMessageHeaders.SetHeader(NatsHeaderConstants.JsMsgId, "other", hdr!);

        byte[]? expected = null;
        expected = NatsMessageHeaders.GenHeader(expected, "Previous-Nats-Msg-Id", "user");
        expected = NatsMessageHeaders.GenHeader(expected, NatsHeaderConstants.JsMsgId, "other");
        if (!withSpaces)
            expected = expected!.Where(b => b != (byte)' ').ToArray();

        hdr!.ShouldBe(expected!);
    }
}
