// Port of Go client_test.go: TestClientConnect, TestClientConnectProto, TestAuthorizationTimeout
// Reference: golang/nats-server/server/client_test.go lines 475, 537, 1260

using System.Net;
using System.Net.Sockets;
using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Server;
using NATS.Server.TestUtilities;

namespace NATS.Server.Core.Tests;

/// <summary>
/// Tests for client lifecycle: connection handshake, CONNECT proto parsing,
/// subscription limits, and auth timeout enforcement.
/// Reference: Go TestClientConnect, TestClientConnectProto, TestAuthorizationTimeout
/// </summary>
public class ClientLifecycleTests
{


    /// <summary>
    /// TestClientConnectProto: Sends CONNECT with verbose:false, pedantic:false, name:"test-client"
    /// and verifies the server responds with PONG, confirming the connection is accepted.
    /// Reference: Go client_test.go TestClientConnectProto (line 537)
    /// </summary>
    [Fact]
    public async Task Connect_proto_accepted()
    {
        var port = TestPortAllocator.GetFreePort();
        using var cts = new CancellationTokenSource();
        var server = new NatsServer(new NatsOptions { Port = port }, NullLoggerFactory.Instance);
        _ = server.StartAsync(cts.Token);
        await server.WaitForReadyAsync();

        try
        {
            using var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            await client.ConnectAsync(IPAddress.Loopback, port);

            // Read INFO
            var buf = new byte[4096];
            var n = await client.ReceiveAsync(buf, SocketFlags.None);
            var info = Encoding.ASCII.GetString(buf, 0, n);
            info.ShouldStartWith("INFO ");

            // Send CONNECT with client name, then PING to flush
            var connectMsg = """CONNECT {"verbose":false,"pedantic":false,"name":"test-client"}""" + "\r\nPING\r\n";
            await client.SendAsync(Encoding.ASCII.GetBytes(connectMsg));

            // Should receive PONG confirming connection is accepted
            var response = await SocketTestHelper.ReadUntilAsync(client, "PONG");
            response.ShouldContain("PONG\r\n");
        }
        finally
        {
            await cts.CancelAsync();
            server.Dispose();
        }
    }

    /// <summary>
    /// Max_subscriptions_enforced: Creates a server with MaxSubs=10, subscribes 10 times,
    /// then verifies that the 11th SUB triggers a -ERR 'Maximum Subscriptions Exceeded'
    /// and the connection is closed.
    /// Reference: Go client_test.go — MaxSubs enforcement in NatsClient.cs line 527
    /// </summary>
    [Fact]
    public async Task Max_subscriptions_enforced()
    {
        const int maxSubs = 10;
        var port = TestPortAllocator.GetFreePort();
        using var cts = new CancellationTokenSource();
        var server = new NatsServer(
            new NatsOptions { Port = port, MaxSubs = maxSubs },
            NullLoggerFactory.Instance);
        _ = server.StartAsync(cts.Token);
        await server.WaitForReadyAsync();

        try
        {
            using var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            await client.ConnectAsync(IPAddress.Loopback, port);

            // Read INFO
            var buf = new byte[4096];
            await client.ReceiveAsync(buf, SocketFlags.None);

            // Send CONNECT
            await client.SendAsync(Encoding.ASCII.GetBytes("CONNECT {}\r\n"));

            // Subscribe up to the limit
            var subsBuilder = new StringBuilder();
            for (int i = 1; i <= maxSubs; i++)
            {
                subsBuilder.Append($"SUB foo.{i} {i}\r\n");
            }
            // Send the 11th subscription (one over the limit)
            subsBuilder.Append($"SUB foo.overflow {maxSubs + 1}\r\n");

            await client.SendAsync(Encoding.ASCII.GetBytes(subsBuilder.ToString()));

            // Server should send -ERR 'Maximum Subscriptions Exceeded' and close
            var response = await SocketTestHelper.ReadUntilAsync(client, "-ERR", timeoutMs: 5000);
            response.ShouldContain("-ERR 'Maximum Subscriptions Exceeded'");

            // Connection should be closed after the error
            using var readCts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
            var n = await client.ReceiveAsync(buf, SocketFlags.None, readCts.Token);
            n.ShouldBe(0);
        }
        finally
        {
            await cts.CancelAsync();
            server.Dispose();
        }
    }

    /// <summary>
    /// Auth_timeout_closes_connection_if_no_connect: Creates a server with auth
    /// (token-based) and a short AuthTimeout of 500ms. Connects a raw socket,
    /// reads INFO, but does NOT send CONNECT. Verifies the server closes the
    /// connection with -ERR 'Authentication Timeout' after the timeout expires.
    /// Reference: Go client_test.go TestAuthorizationTimeout (line 1260)
    /// </summary>
    [Fact]
    public async Task Auth_timeout_closes_connection_if_no_connect()
    {
        var port = TestPortAllocator.GetFreePort();
        using var cts = new CancellationTokenSource();
        var server = new NatsServer(
            new NatsOptions
            {
                Port = port,
                Authorization = "my_secret_token",
                AuthTimeout = TimeSpan.FromMilliseconds(500),
            },
            NullLoggerFactory.Instance);
        _ = server.StartAsync(cts.Token);
        await server.WaitForReadyAsync();

        try
        {
            using var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            await client.ConnectAsync(IPAddress.Loopback, port);

            // Read INFO — server requires auth so INFO will have auth_required:true
            var buf = new byte[4096];
            var n = await client.ReceiveAsync(buf, SocketFlags.None);
            var info = Encoding.ASCII.GetString(buf, 0, n);
            info.ShouldStartWith("INFO ");

            // Do NOT send CONNECT — wait for auth timeout to fire
            // AuthTimeout is 500ms; wait up to 3x that for the error
            var response = await SocketTestHelper.ReadUntilAsync(client, "Authentication Timeout", timeoutMs: 3000);
            response.ShouldContain("-ERR 'Authentication Timeout'");

            // Connection should be closed after the auth timeout error
            using var readCts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
            n = await client.ReceiveAsync(buf, SocketFlags.None, readCts.Token);
            n.ShouldBe(0);
        }
        finally
        {
            await cts.CancelAsync();
            server.Dispose();
        }
    }
}
