// Reference: golang/nats-server/server/route.go — negotiateRoutePool (pooling handshake logic)
// Tests for route pool size negotiation between local and remote peers.

using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Server.Configuration;
using NATS.Server.Routes;

namespace NATS.Server.Clustering.Tests.Routes;

/// <summary>
/// Tests for route pool size negotiation (Gap 13.3).
/// Covers RouteConnection.NegotiatePoolSize static method,
/// RouteConnection.NegotiatedPoolSize default, RouteManager.ConfiguredPoolSize,
/// and RouteManager.GetEffectivePoolSize.
/// Go reference: server/route.go negotiateRoutePool.
/// </summary>
public class PoolSizeNegotiationTests
{
    /// <summary>
    /// Creates a connected socket pair so RouteConnection can build its NetworkStream.
    /// Returns the RouteConnection (owns the server-side socket) and the client-side
    /// socket, which the caller must dispose to avoid resource leaks.
    /// </summary>
    private static (RouteConnection Route, Socket ClientSocket) CreateConnectedRoute()
    {
        var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
        listener.Listen(1);
        var port = ((IPEndPoint)listener.LocalEndPoint!).Port;

        var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        client.Connect(IPAddress.Loopback, port);
        var server = listener.Accept();
        listener.Dispose();

        return (new RouteConnection(server), client);
    }

    // -----------------------------------------------------------------------
    // RouteConnection.NegotiatePoolSize static method
    // -----------------------------------------------------------------------

    [Fact]
    public void NegotiatePoolSize_BothNonZero_ReturnsMin()
    {
        // Go reference: server/route.go negotiateRoutePool — min(local, remote)
        var result = RouteConnection.NegotiatePoolSize(3, 5);
        result.ShouldBe(3);
    }

    [Fact]
    public void NegotiatePoolSize_Equal_ReturnsSame()
    {
        // Go reference: server/route.go negotiateRoutePool — equal sizes
        var result = RouteConnection.NegotiatePoolSize(3, 3);
        result.ShouldBe(3);
    }

    [Fact]
    public void NegotiatePoolSize_LocalZero_ReturnsZero()
    {
        // Go reference: server/route.go negotiateRoutePool — backward compat:
        // if either peer sends 0 (no pooling advertised), result is 0.
        var result = RouteConnection.NegotiatePoolSize(0, 5);
        result.ShouldBe(0);
    }

    [Fact]
    public void NegotiatePoolSize_RemoteZero_ReturnsZero()
    {
        // Go reference: server/route.go negotiateRoutePool — backward compat:
        // remote peer without pool support sends 0; result must be 0.
        var result = RouteConnection.NegotiatePoolSize(3, 0);
        result.ShouldBe(0);
    }

    [Fact]
    public void NegotiatePoolSize_BothZero_ReturnsZero()
    {
        // Go reference: server/route.go negotiateRoutePool — both disabled
        var result = RouteConnection.NegotiatePoolSize(0, 0);
        result.ShouldBe(0);
    }

    [Fact]
    public void NegotiatePoolSize_LocalLarger_ReturnsRemote()
    {
        // Go reference: server/route.go negotiateRoutePool — min(10, 3) = 3
        var result = RouteConnection.NegotiatePoolSize(10, 3);
        result.ShouldBe(3);
    }

    [Fact]
    public void NegotiatePoolSize_RemoteLarger_ReturnsLocal()
    {
        // Go reference: server/route.go negotiateRoutePool — min(3, 10) = 3
        var result = RouteConnection.NegotiatePoolSize(3, 10);
        result.ShouldBe(3);
    }

    [Fact]
    public void NegotiatePoolSize_OneIsOne_ReturnsOne()
    {
        // Go reference: server/route.go negotiateRoutePool — min(1, 5) = 1
        var result = RouteConnection.NegotiatePoolSize(1, 5);
        result.ShouldBe(1);
    }

    // -----------------------------------------------------------------------
    // RouteConnection.NegotiatedPoolSize default value
    // -----------------------------------------------------------------------

    [Fact]
    public void NegotiatedPoolSize_Default_IsZero()
    {
        // A newly created RouteConnection (not yet handshaked) must report
        // NegotiatedPoolSize == 0 to signal that negotiation has not occurred.
        // Go reference: server/route.go — pool size is 0 until negotiateRoutePool runs.
        var (conn, clientSocket) = CreateConnectedRoute();
        try
        {
            conn.NegotiatedPoolSize.ShouldBe(0);
        }
        finally
        {
            clientSocket.Dispose();
        }
    }

    // -----------------------------------------------------------------------
    // RouteManager.ComputeRoutePoolIdx determinism (regression guard)
    // -----------------------------------------------------------------------

    [Fact]
    public void ComputeRoutePoolIdx_Deterministic()
    {
        // The same account name must always map to the same pool index.
        // Go reference: server/route.go computeRoutePoolIdx (FNV-1a 32-bit hash).
        const int poolSize = 5;
        const string account = "test-account";

        var first = RouteManager.ComputeRoutePoolIdx(poolSize, account);
        var second = RouteManager.ComputeRoutePoolIdx(poolSize, account);
        var third = RouteManager.ComputeRoutePoolIdx(poolSize, account);

        first.ShouldBe(second);
        second.ShouldBe(third);
        first.ShouldBeGreaterThanOrEqualTo(0);
        first.ShouldBeLessThan(poolSize);
    }

    // -----------------------------------------------------------------------
    // RouteManager.ConfiguredPoolSize and GetEffectivePoolSize
    // -----------------------------------------------------------------------

    private static RouteManager MakeManager(int poolSize = 3)
    {
        var opts = new ClusterOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            PoolSize = poolSize,
        };
        return new RouteManager(
            opts,
            new ServerStats(),
            Guid.NewGuid().ToString("N"),
            _ => { },
            _ => { },
            NullLogger<RouteManager>.Instance);
    }

    [Fact]
    public void ConfiguredPoolSize_ReturnsOptionsPoolSize()
    {
        // RouteManager should expose the configured pool size from ClusterOptions.
        // Go reference: server/route.go opts.Cluster.PoolSize default is 3.
        var manager = MakeManager(poolSize: 5);
        manager.ConfiguredPoolSize.ShouldBe(5);
    }

    [Fact]
    public void ConfiguredPoolSize_DefaultThreeWhenOptionsIsZero()
    {
        // When ClusterOptions.PoolSize is 0 (not explicitly set), the effective
        // configured pool size should fall back to 3 (Go's default).
        // Go reference: server/route.go default pool size of 3.
        var manager = MakeManager(poolSize: 0);
        manager.ConfiguredPoolSize.ShouldBe(3);
    }

    [Fact]
    public void GetEffectivePoolSize_NullRemoteId_ReturnsConfigured()
    {
        // With no remote server ID, GetEffectivePoolSize returns the configured pool size.
        var manager = MakeManager(poolSize: 4);
        manager.GetEffectivePoolSize(null).ShouldBe(4);
    }

    [Fact]
    public void GetEffectivePoolSize_UnknownRemoteId_ReturnsConfigured()
    {
        // For a remote server ID that has no connected (and negotiated) route,
        // GetEffectivePoolSize should return the configured pool size.
        var manager = MakeManager(poolSize: 3);
        manager.GetEffectivePoolSize("unknown-server-id").ShouldBe(3);
    }
}
