// Reference: golang/nats-server/server/route.go getRoutesExcludePool — no-pool fallback for
// backward compatibility with pre-pool peers (Gap 13.6).

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 no-pool (legacy) route fallback — Gap 13.6.
/// Verifies that <see cref="RouteConnection.SupportsPooling"/>,
/// <see cref="RouteConnection.IsLegacyRoute"/>, and the new
/// <see cref="RouteManager"/> legacy-route helpers behave correctly, and that
/// <see cref="RouteManager.GetRouteForAccount"/> falls back to a legacy route
/// when neither a dedicated nor a pool route exists.
/// Go reference: server/route.go getRoutesExcludePool.
/// </summary>
public class NoPoolFallbackTests : IDisposable
{
    private readonly List<TcpListener> _listeners = [];
    private readonly List<Socket> _sockets = [];

    public void Dispose()
    {
        foreach (var s in _sockets) s.Dispose();
        foreach (var l in _listeners) l.Stop();
    }

    // -----------------------------------------------------------------------
    // Helpers
    // -----------------------------------------------------------------------

    private static RouteManager CreateManager() =>
        new(
            new ClusterOptions { Host = "127.0.0.1", Port = 0, Routes = [] },
            new ServerStats(),
            Guid.NewGuid().ToString("N"),
            _ => { },
            _ => { },
            NullLogger<RouteManager>.Instance);

    /// <summary>
    /// Creates a <see cref="RouteConnection"/> backed by a connected loopback
    /// socket pair so that the internal <see cref="System.Net.Sockets.NetworkStream"/>
    /// can be constructed without throwing. Both sockets are tracked for disposal.
    /// </summary>
    private RouteConnection MakeConnection()
    {
        var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        _listeners.Add(listener);

        var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        _sockets.Add(client);
        client.Connect((IPEndPoint)listener.LocalEndpoint);

        var server = listener.AcceptSocket();
        _sockets.Add(server);

        return new RouteConnection(server);
    }

    // -----------------------------------------------------------------------
    // RouteConnection.SupportsPooling
    // -----------------------------------------------------------------------

    // Go reference: server/route.go — pool-capable route: NegotiatedPoolSize > 0.
    [Fact]
    public void SupportsPooling_WhenNegotiated_ReturnsTrue()
    {
        var conn = MakeConnection();
        conn.SetNegotiatedPoolSize(3);
        conn.SupportsPooling.ShouldBeTrue();
    }

    // Go reference: server/route.go — default state is pre-negotiation (no pooling).
    [Fact]
    public void SupportsPooling_Default_ReturnsFalse()
    {
        var conn = MakeConnection();
        // NegotiatedPoolSize defaults to 0 — pooling not yet established.
        conn.SupportsPooling.ShouldBeFalse();
    }

    // -----------------------------------------------------------------------
    // RouteConnection.IsLegacyRoute
    // -----------------------------------------------------------------------

    // Go reference: server/route.go getRoutesExcludePool — NegotiatedPoolSize == 0 means legacy.
    [Fact]
    public void IsLegacyRoute_Default_ReturnsTrue()
    {
        var conn = MakeConnection();
        // A freshly created connection has NegotiatedPoolSize == 0, making it legacy.
        conn.IsLegacyRoute.ShouldBeTrue();
    }

    // Go reference: server/route.go — pool-negotiated connection is not legacy.
    [Fact]
    public void IsLegacyRoute_WhenNegotiated_ReturnsFalse()
    {
        var conn = MakeConnection();
        conn.SetNegotiatedPoolSize(2);
        conn.IsLegacyRoute.ShouldBeFalse();
    }

    // -----------------------------------------------------------------------
    // RouteManager.GetLegacyRoute
    // -----------------------------------------------------------------------

    // Go reference: server/route.go getRoutesExcludePool — returns first legacy connection.
    [Fact]
    public void GetLegacyRoute_ReturnsLegacyConnection()
    {
        var manager = CreateManager();
        var legacy = MakeConnection();
        // NegotiatedPoolSize == 0 by default — this is a legacy route.
        manager.RegisterRoute("server-legacy", legacy);

        var result = manager.GetLegacyRoute();

        result.ShouldBeSameAs(legacy);
    }

    // Go reference: server/route.go getRoutesExcludePool — null when all routes support pooling.
    [Fact]
    public void GetLegacyRoute_NoLegacy_ReturnsNull()
    {
        var manager = CreateManager();
        var pooled = MakeConnection();
        pooled.SetNegotiatedPoolSize(3);
        manager.RegisterRoute("server-pooled", pooled);

        var result = manager.GetLegacyRoute();

        result.ShouldBeNull();
    }

    // -----------------------------------------------------------------------
    // RouteManager.GetLegacyRoutes
    // -----------------------------------------------------------------------

    // Go reference: server/route.go getRoutesExcludePool — returns all legacy connections.
    [Fact]
    public void GetLegacyRoutes_ReturnsAllLegacy()
    {
        var manager = CreateManager();

        var legacy1 = MakeConnection(); // NegotiatedPoolSize == 0 → legacy
        var legacy2 = MakeConnection(); // NegotiatedPoolSize == 0 → legacy
        var pooled = MakeConnection();
        pooled.SetNegotiatedPoolSize(3);

        manager.RegisterRoute("server-legacy-1", legacy1);
        manager.RegisterRoute("server-legacy-2", legacy2);
        manager.RegisterRoute("server-pooled", pooled);

        var result = manager.GetLegacyRoutes();

        result.Count.ShouldBe(2);
        result.ShouldContain(legacy1);
        result.ShouldContain(legacy2);
        result.ShouldNotContain(pooled);
    }

    // -----------------------------------------------------------------------
    // RouteManager.HasLegacyRoutes
    // -----------------------------------------------------------------------

    // Go reference: server/route.go — any legacy route present returns true.
    [Fact]
    public void HasLegacyRoutes_WhenPresent_ReturnsTrue()
    {
        var manager = CreateManager();
        var legacy = MakeConnection(); // IsLegacyRoute == true (default)
        manager.RegisterRoute("server-legacy", legacy);

        manager.HasLegacyRoutes.ShouldBeTrue();
    }

    // Go reference: server/route.go — no legacy routes returns false.
    [Fact]
    public void HasLegacyRoutes_WhenAbsent_ReturnsFalse()
    {
        var manager = CreateManager();
        var pooled = MakeConnection();
        pooled.SetNegotiatedPoolSize(5);
        manager.RegisterRoute("server-pooled", pooled);

        manager.HasLegacyRoutes.ShouldBeFalse();
    }

    // -----------------------------------------------------------------------
    // RouteManager.GetRouteForAccount — legacy fallback (Gap 13.6)
    // -----------------------------------------------------------------------

    // Go reference: server/route.go — when no dedicated or pool route exists,
    // fall back to the first legacy route for the account.
    [Fact]
    public void GetRouteForAccount_FallsBackToLegacy()
    {
        var manager = CreateManager();

        // Register only a legacy route (NegotiatedPoolSize == 0).
        var legacy = MakeConnection();
        manager.RegisterRoute("server-legacy", legacy);

        // No dedicated account route and no pool-capable route registered.
        // GetRouteForAccount must return the legacy connection.
        var result = manager.GetRouteForAccount("$G");

        result.ShouldBeSameAs(legacy);
    }
}
