// Reference: golang/nats-server/server/route.go — route hash map for O(1) server-ID lookup.
// Tests for Gap 13.4: hash-based route storage added to RouteManager.

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

namespace NATS.Server.Clustering.Tests.Routes;

/// <summary>
/// Tests for the FNV-1a hash-based route storage on <see cref="RouteManager"/>.
/// Covers <c>ComputeRouteHash</c>, <c>RegisterRouteByHash</c>,
/// <c>UnregisterRouteByHash</c>, <c>GetRouteByHash</c>,
/// <c>GetRouteByServerId</c>, and <c>HashedRouteCount</c>.
/// Go reference: server/route.go — server-ID-keyed route hash map.
/// </summary>
public class RouteHashStorageTests
{
    // Helper: build a RouteManager instance that is NOT started (no listener).
    // Only hash-map methods are exercised; StartAsync is never called.
    private static RouteManager CreateManager() =>
        new(
            new ClusterOptions { Host = "127.0.0.1", Port = 0 },
            new ServerStats(),
            serverId: Guid.NewGuid().ToString("N"),
            remoteSubSink: static _ => { },
            routedMessageSink: static _ => { },
            logger: NullLogger<RouteManager>.Instance);

    // Helper: create a RouteConnection backed by a loopback-connected socket.
    // RouteConnection's constructor wraps the socket in NetworkStream, which
    // requires a connected socket, so we use a listener/accept pair.
    private static RouteConnection MakeRouteConnection()
    {
        using var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        client.Connect(IPAddress.Loopback, ((IPEndPoint)listener.LocalEndpoint).Port);
        // Accept server side so the TCP handshake completes; we hand the client
        // socket to RouteConnection and let the server-side socket be GC'd.
        _ = listener.AcceptSocket();
        return new RouteConnection(client);
    }

    // 1 -----------------------------------------------------------------------
    [Fact]
    public void ComputeRouteHash_Deterministic()
    {
        // Go reference: server/route.go — hash derivation must be stable across calls.
        const string serverId = "server-abc-123";

        var h1 = RouteManager.ComputeRouteHash(serverId);
        var h2 = RouteManager.ComputeRouteHash(serverId);
        var h3 = RouteManager.ComputeRouteHash(serverId);

        h1.ShouldBe(h2);
        h2.ShouldBe(h3);
    }

    // 2 -----------------------------------------------------------------------
    [Fact]
    public void ComputeRouteHash_DifferentInputs_DifferentHashes()
    {
        // Go reference: server/route.go — distinct server IDs must not collide.
        var h1 = RouteManager.ComputeRouteHash("server-1");
        var h2 = RouteManager.ComputeRouteHash("server-2");

        h1.ShouldNotBe(h2);
    }

    // 3 -----------------------------------------------------------------------
    [Fact]
    public void ComputeRouteHash_EmptyString_DoesNotThrow()
    {
        // Go reference: server/route.go — empty server ID is a degenerate but
        // valid input; the FNV offset basis is returned.
        var ex = Record.Exception(() => RouteManager.ComputeRouteHash(string.Empty));
        ex.ShouldBeNull();

        // The hash of an empty string equals the FNV-1a 64-bit offset basis.
        const ulong fnvOffsetBasis = 14695981039346656037UL;
        RouteManager.ComputeRouteHash(string.Empty).ShouldBe(fnvOffsetBasis);
    }

    // 4 -----------------------------------------------------------------------
    [Fact]
    public async Task RegisterRouteByHash_CanRetrieve()
    {
        // Go reference: server/route.go — after registration the connection must
        // be retrievable by its hash key.
        var mgr = CreateManager();
        await using var conn = MakeRouteConnection();

        mgr.RegisterRouteByHash("srv-A", conn);

        var hash = RouteManager.ComputeRouteHash("srv-A");
        mgr.GetRouteByHash(hash).ShouldBeSameAs(conn);
    }

    // 5 -----------------------------------------------------------------------
    [Fact]
    public async Task UnregisterRouteByHash_RemovesEntry()
    {
        // Go reference: server/route.go — deregistration removes the hash entry.
        var mgr = CreateManager();
        await using var conn = MakeRouteConnection();

        mgr.RegisterRouteByHash("srv-B", conn);
        mgr.UnregisterRouteByHash("srv-B");

        var hash = RouteManager.ComputeRouteHash("srv-B");
        mgr.GetRouteByHash(hash).ShouldBeNull();
    }

    // 6 -----------------------------------------------------------------------
    [Fact]
    public async Task GetRouteByServerId_FindsRegistered()
    {
        // Go reference: server/route.go — string-based lookup computes hash internally.
        var mgr = CreateManager();
        await using var conn = MakeRouteConnection();

        mgr.RegisterRouteByHash("srv-C", conn);

        mgr.GetRouteByServerId("srv-C").ShouldBeSameAs(conn);
    }

    // 7 -----------------------------------------------------------------------
    [Fact]
    public void GetRouteByServerId_NotRegistered_ReturnsNull()
    {
        // Go reference: server/route.go — unknown server ID yields null.
        var mgr = CreateManager();

        mgr.GetRouteByServerId("unknown-server").ShouldBeNull();
    }

    // 8 -----------------------------------------------------------------------
    [Fact]
    public async Task HashedRouteCount_MatchesRegistrations()
    {
        // Go reference: server/route.go — count reflects registered entries.
        var mgr = CreateManager();
        await using var c1 = MakeRouteConnection();
        await using var c2 = MakeRouteConnection();
        await using var c3 = MakeRouteConnection();

        mgr.HashedRouteCount.ShouldBe(0);

        mgr.RegisterRouteByHash("srv-1", c1);
        mgr.HashedRouteCount.ShouldBe(1);

        mgr.RegisterRouteByHash("srv-2", c2);
        mgr.HashedRouteCount.ShouldBe(2);

        mgr.RegisterRouteByHash("srv-3", c3);
        mgr.HashedRouteCount.ShouldBe(3);

        mgr.UnregisterRouteByHash("srv-2");
        mgr.HashedRouteCount.ShouldBe(2);
    }

    // 9 -----------------------------------------------------------------------
    [Fact]
    public async Task RegisterRouteByHash_OverwritesPrevious()
    {
        // Go reference: server/route.go — re-registering the same server ID
        // replaces the stale connection with the new one.
        var mgr = CreateManager();
        await using var old = MakeRouteConnection();
        await using var replacement = MakeRouteConnection();

        mgr.RegisterRouteByHash("srv-D", old);
        mgr.RegisterRouteByHash("srv-D", replacement);

        mgr.GetRouteByServerId("srv-D").ShouldBeSameAs(replacement);
        mgr.HashedRouteCount.ShouldBe(1);
    }

    // 10 ----------------------------------------------------------------------
    [Fact]
    public async Task UnregisterRouteByHash_NonExistent_NoOp()
    {
        // Go reference: server/route.go — removing a hash key that was never
        // registered must not throw and must not alter the count.
        var mgr = CreateManager();
        await using var conn = MakeRouteConnection();

        mgr.RegisterRouteByHash("srv-E", conn);

        var ex = Record.Exception(() => mgr.UnregisterRouteByHash("does-not-exist"));
        ex.ShouldBeNull();

        mgr.HashedRouteCount.ShouldBe(1);
    }
}
