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

namespace NATS.Server.LeafNodes.Tests.LeafNodes;

/// <summary>
/// Unit tests for leaf node reconnect state validation (Gap 12.3).
/// Verifies that <see cref="LeafNodeManager.ValidateRemoteLeafNode"/>,
/// <see cref="LeafNodeManager.IsSelfConnect"/>, <see cref="LeafNodeManager.HasConnection"/>,
/// and <see cref="LeafNodeManager.GetConnectionByRemoteId"/> enforce self-connect detection,
/// duplicate-connection rejection, and JetStream domain conflict detection.
/// Go reference: leafnode.go addLeafNodeConnection — duplicate and domain checks.
/// </summary>
public class LeafValidationTests
{
    private static LeafNodeManager CreateManager(string serverId = "server-A") =>
        new(
            options: new LeafNodeOptions { Host = "127.0.0.1", Port = 0 },
            stats: new ServerStats(),
            serverId: serverId,
            remoteSubSink: _ => { },
            messageSink: _ => { },
            logger: NullLogger<LeafNodeManager>.Instance);

    /// <summary>
    /// Creates a connected socket pair and returns the server-side socket.
    /// The caller is responsible for disposing both sockets.
    /// </summary>
    private static async Task<(Socket serverSide, Socket clientSide, TcpListener listener)> CreateSocketPairAsync()
    {
        var tcpListener = new TcpListener(IPAddress.Loopback, 0);
        tcpListener.Start();
        var clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await clientSocket.ConnectAsync(IPAddress.Loopback, ((IPEndPoint)tcpListener.LocalEndpoint).Port);
        var serverSocket = await tcpListener.AcceptSocketAsync();
        tcpListener.Stop();
        return (serverSocket, clientSocket, tcpListener);
    }

    private static async Task<LeafConnection> CreateConnectionWithRemoteIdAsync(string remoteId, string? jsDomain = null)
    {
        var (serverSide, clientSide, _) = await CreateSocketPairAsync();
        clientSide.Dispose(); // only need the server side for the LeafConnection
        var conn = new LeafConnection(serverSide)
        {
            RemoteId = remoteId,
            JetStreamDomain = jsDomain,
        };
        return conn;
    }

    // Go: leafnode.go addLeafNodeConnection — happy path with distinct server IDs
    [Fact]
    public async Task ValidateRemoteLeafNode_Valid_ReturnsValid()
    {
        var manager = CreateManager("server-A");
        await using var conn = await CreateConnectionWithRemoteIdAsync("server-B");
        manager.InjectConnectionForTesting(conn);

        var result = manager.ValidateRemoteLeafNode("server-C", "$G", null);

        result.Valid.ShouldBeTrue();
        result.Error.ShouldBeNull();
        result.ErrorCode.ShouldBe(LeafValidationError.None);
    }

    // Go: leafnode.go — loop detection: reject when remote ID matches own server ID
    [Fact]
    public void ValidateRemoteLeafNode_SelfConnect_ReturnsError()
    {
        var manager = CreateManager("server-A");

        var result = manager.ValidateRemoteLeafNode("server-A", "$G", null);

        result.Valid.ShouldBeFalse();
        result.ErrorCode.ShouldBe(LeafValidationError.SelfConnect);
        result.Error.ShouldNotBeNull();
    }

    // Go: leafnode.go addLeafNodeConnection checkForDup — existing connection from same remote
    [Fact]
    public async Task ValidateRemoteLeafNode_DuplicateConnection_ReturnsError()
    {
        var manager = CreateManager("server-A");
        await using var conn = await CreateConnectionWithRemoteIdAsync("server-B");
        manager.InjectConnectionForTesting(conn);

        var result = manager.ValidateRemoteLeafNode("server-B", "$G", null);

        result.Valid.ShouldBeFalse();
        result.ErrorCode.ShouldBe(LeafValidationError.DuplicateConnection);
        result.Error.ShouldNotBeNull();
    }

    // Go: leafnode.go addLeafNodeConnection — JetStream domain conflict between existing and incoming
    [Fact]
    public async Task ValidateRemoteLeafNode_JsDomainConflict_ReturnsError()
    {
        var manager = CreateManager("server-A");
        await using var conn = await CreateConnectionWithRemoteIdAsync("server-B", jsDomain: "domain-hub");
        manager.InjectConnectionForTesting(conn);

        // server-C tries to connect with a different JS domain
        var result = manager.ValidateRemoteLeafNode("server-C", "$G", "domain-spoke");

        result.Valid.ShouldBeFalse();
        result.ErrorCode.ShouldBe(LeafValidationError.JetStreamDomainConflict);
        result.Error.ShouldNotBeNull();
    }

    // Go: leafnode.go — null JS domain is never a conflict
    [Fact]
    public async Task ValidateRemoteLeafNode_NullJsDomain_Valid()
    {
        var manager = CreateManager("server-A");
        await using var conn = await CreateConnectionWithRemoteIdAsync("server-B", jsDomain: "domain-hub");
        manager.InjectConnectionForTesting(conn);

        // Incoming with null domain — no domain conflict check performed
        var result = manager.ValidateRemoteLeafNode("server-C", "$G", null);

        result.Valid.ShouldBeTrue();
        result.ErrorCode.ShouldBe(LeafValidationError.None);
    }

    // Go: leafnode.go — IsSelfConnect true when IDs match
    [Fact]
    public void IsSelfConnect_MatchingId_ReturnsTrue()
    {
        var manager = CreateManager("server-A");

        manager.IsSelfConnect("server-A").ShouldBeTrue();
    }

    // Go: leafnode.go — IsSelfConnect false when IDs differ
    [Fact]
    public void IsSelfConnect_DifferentId_ReturnsFalse()
    {
        var manager = CreateManager("server-A");

        manager.IsSelfConnect("server-B").ShouldBeFalse();
    }

    // Go: leafnode.go — HasConnection true when remote ID is registered
    [Fact]
    public async Task HasConnection_Existing_ReturnsTrue()
    {
        var manager = CreateManager("server-A");
        await using var conn = await CreateConnectionWithRemoteIdAsync("server-B");
        manager.InjectConnectionForTesting(conn);

        manager.HasConnection("server-B").ShouldBeTrue();
    }

    // Go: leafnode.go — HasConnection false when remote ID is not registered
    [Fact]
    public void HasConnection_Missing_ReturnsFalse()
    {
        var manager = CreateManager("server-A");

        manager.HasConnection("server-B").ShouldBeFalse();
    }

    // Go: leafnode.go — GetConnectionByRemoteId returns the matching connection
    [Fact]
    public async Task GetConnectionByRemoteId_Found_ReturnsConnection()
    {
        var manager = CreateManager("server-A");
        await using var conn = await CreateConnectionWithRemoteIdAsync("server-B");
        manager.InjectConnectionForTesting(conn);

        var found = manager.GetConnectionByRemoteId("server-B");

        found.ShouldNotBeNull();
        found.RemoteId.ShouldBe("server-B");
    }
}
