// Reference: golang/nats-server/server/route.go — per-account dedicated route registration (Gap 13.2).
// Tests for account-specific dedicated route connections in RouteManager.

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 per-account dedicated route connections (Gap 13.2).
/// Verifies that RouteManager correctly stores, retrieves, and prioritises
/// dedicated routes over pool-based routes for specific accounts.
/// Go reference: server/route.go — per-account dedicated route handling.
/// </summary>
public class AccountRouteTests : IDisposable
{
    // -- Helpers --

    // Track listeners and sockets for cleanup after each test.
    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();
    }

    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 RouteConnection backed by a connected loopback socket pair so
    /// that RouteConnection can construct its internal NetworkStream without
    /// throwing. Both sockets and the listener 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);
    }

    // -- Tests --

    // Go: server/route.go — per-account dedicated route registration.
    [Fact]
    public void RegisterAccountRoute_AddsRoute()
    {
        var manager = CreateManager();
        var connection = MakeConnection();

        manager.RegisterAccountRoute("ACCT-A", connection);

        manager.GetDedicatedAccountRoute("ACCT-A").ShouldBeSameAs(connection);
    }

    // Go: server/route.go — overwrite existing dedicated route for same account.
    [Fact]
    public void RegisterAccountRoute_OverwritesPrevious()
    {
        var manager = CreateManager();
        var first = MakeConnection();
        var second = MakeConnection();

        manager.RegisterAccountRoute("ACCT-A", first);
        manager.RegisterAccountRoute("ACCT-A", second);

        manager.GetDedicatedAccountRoute("ACCT-A").ShouldBeSameAs(second);
    }

    // Go: server/route.go — removing a dedicated route cleans up the entry.
    [Fact]
    public void UnregisterAccountRoute_RemovesRoute()
    {
        var manager = CreateManager();
        var connection = MakeConnection();

        manager.RegisterAccountRoute("ACCT-A", connection);
        manager.UnregisterAccountRoute("ACCT-A");

        manager.GetDedicatedAccountRoute("ACCT-A").ShouldBeNull();
    }

    // Go: server/route.go — unregistering a never-registered account is safe.
    [Fact]
    public void UnregisterAccountRoute_NonExistent_NoOp()
    {
        var manager = CreateManager();

        // Must not throw.
        var ex = Record.Exception(() => manager.UnregisterAccountRoute("NONEXISTENT"));
        ex.ShouldBeNull();
    }

    // Go: server/route.go — lookup on unregistered account returns null.
    [Fact]
    public void GetDedicatedAccountRoute_NotRegistered_ReturnsNull()
    {
        var manager = CreateManager();

        manager.GetDedicatedAccountRoute("UNKNOWN").ShouldBeNull();
    }

    // Go: server/route.go — HasDedicatedRoute returns true for registered account.
    [Fact]
    public void HasDedicatedRoute_RegisteredReturnsTrue()
    {
        var manager = CreateManager();
        var connection = MakeConnection();

        manager.RegisterAccountRoute("ACCT-B", connection);

        manager.HasDedicatedRoute("ACCT-B").ShouldBeTrue();
    }

    // Go: server/route.go — HasDedicatedRoute returns false for unknown account.
    [Fact]
    public void HasDedicatedRoute_NotRegisteredReturnsFalse()
    {
        var manager = CreateManager();

        manager.HasDedicatedRoute("ACCT-B").ShouldBeFalse();
    }

    // Go: server/route.go — listing accounts with dedicated routes.
    [Fact]
    public void GetAccountsWithDedicatedRoutes_ReturnsAllRegistered()
    {
        var manager = CreateManager();

        manager.RegisterAccountRoute("ACCT-1", MakeConnection());
        manager.RegisterAccountRoute("ACCT-2", MakeConnection());
        manager.RegisterAccountRoute("ACCT-3", MakeConnection());

        var accounts = manager.GetAccountsWithDedicatedRoutes();

        accounts.Count.ShouldBe(3);
        accounts.ShouldContain("ACCT-1");
        accounts.ShouldContain("ACCT-2");
        accounts.ShouldContain("ACCT-3");
    }

    // Go: server/route.go — DedicatedRouteCount tracks registered entries.
    [Fact]
    public void DedicatedRouteCount_MatchesRegistrations()
    {
        var manager = CreateManager();

        manager.DedicatedRouteCount.ShouldBe(0);

        manager.RegisterAccountRoute("ACCT-X", MakeConnection());
        manager.DedicatedRouteCount.ShouldBe(1);

        manager.RegisterAccountRoute("ACCT-Y", MakeConnection());
        manager.DedicatedRouteCount.ShouldBe(2);

        manager.UnregisterAccountRoute("ACCT-X");
        manager.DedicatedRouteCount.ShouldBe(1);
    }

    // Go: server/route.go — dedicated route takes priority over pool-based route
    // for the account it is registered against.
    [Fact]
    public void GetRouteForAccount_PrefersDedicatedRoute()
    {
        var manager = CreateManager();

        // Register a dedicated route for "ACCT-PREF".
        var dedicated = MakeConnection();
        manager.RegisterAccountRoute("ACCT-PREF", dedicated);

        // GetRouteForAccount must return the dedicated connection even though
        // no pool routes exist (the dedicated path short-circuits pool lookup).
        var result = manager.GetRouteForAccount("ACCT-PREF");

        result.ShouldBeSameAs(dedicated);
    }
}
