using System.Net;
using System.Net.Sockets;
using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Client.Core;
using NATS.Server.Auth;
using NATS.Server.Configuration;
using NATS.Server.Routes;

namespace NATS.Server.Clustering.Tests.Routes;

/// <summary>
/// Tests for route configuration validation, compression options, topology gossip,
/// connect info JSON, and route manager behavior.
/// Ported from Go: server/routes_test.go.
/// </summary>
public class RouteConfigValidationTests
{
    // -- Helpers --

    private static async Task<(NatsServer Server, CancellationTokenSource Cts)> StartServerAsync(
        NatsOptions opts)
    {
        var server = new NatsServer(opts, NullLoggerFactory.Instance);
        var cts = new CancellationTokenSource();
        _ = server.StartAsync(cts.Token);
        await server.WaitForReadyAsync();
        return (server, cts);
    }

    private static NatsOptions MakeClusterOpts(string? clusterName = null, string? seed = null)
    {
        return new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Cluster = new ClusterOptions
            {
                Name = clusterName ?? Guid.NewGuid().ToString("N"),
                Host = "127.0.0.1",
                Port = 0,
                Routes = seed is null ? [] : [seed],
            },
        };
    }

    private static async Task WaitForRouteFormation(NatsServer a, NatsServer b, int timeoutMs = 5000)
    {
        using var timeout = new CancellationTokenSource(timeoutMs);
        while (!timeout.IsCancellationRequested &&
               (Interlocked.Read(ref a.Stats.Routes) == 0 ||
                Interlocked.Read(ref b.Stats.Routes) == 0))
        {
            await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default);
        }
    }

    private static async Task WaitForCondition(Func<bool> predicate, int timeoutMs = 5000)
    {
        using var cts = new CancellationTokenSource(timeoutMs);
        while (!cts.IsCancellationRequested)
        {
            if (predicate()) return;
            await Task.Delay(20, cts.Token).ContinueWith(_ => { }, TaskScheduler.Default);
        }

        throw new TimeoutException("Condition not met.");
    }

    private static async Task DisposeServers(params (NatsServer Server, CancellationTokenSource Cts)[] servers)
    {
        foreach (var (server, cts) in servers)
        {
            await cts.CancelAsync();
            server.Dispose();
            cts.Dispose();
        }
    }

    // -- Tests: Configuration validation --

    // Go: TestRouteConfig server/routes_test.go:86
    [Fact]
    public void ClusterOptions_defaults_are_correct()
    {
        var opts = new ClusterOptions();
        opts.Host.ShouldBe("0.0.0.0");
        opts.Port.ShouldBe(6222);
        opts.PoolSize.ShouldBe(3);
        opts.Routes.ShouldNotBeNull();
        opts.Routes.Count.ShouldBe(0);
        opts.Accounts.ShouldNotBeNull();
        opts.Accounts.Count.ShouldBe(0);
        opts.Compression.ShouldBe(RouteCompression.None);
    }

    // Go: TestRouteConfig server/routes_test.go:86
    [Fact]
    public void ClusterOptions_can_set_all_fields()
    {
        var opts = new ClusterOptions
        {
            Name = "my-cluster",
            Host = "192.168.1.1",
            Port = 7244,
            PoolSize = 5,
            Routes = ["127.0.0.1:7245", "127.0.0.1:7246"],
            Accounts = ["A", "B"],
            Compression = RouteCompression.None,
        };

        opts.Name.ShouldBe("my-cluster");
        opts.Host.ShouldBe("192.168.1.1");
        opts.Port.ShouldBe(7244);
        opts.PoolSize.ShouldBe(5);
        opts.Routes.Count.ShouldBe(2);
        opts.Accounts.Count.ShouldBe(2);
    }

    // Go: TestRoutePoolAndPerAccountErrors server/routes_test.go:1906
    [Fact]
    public void NatsOptions_with_cluster_sets_cluster_listen()
    {
        var opts = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Cluster = new ClusterOptions
            {
                Host = "127.0.0.1",
                Port = 0,
            },
        };
        var server = new NatsServer(opts, NullLoggerFactory.Instance);
        // ClusterListen is null until StartAsync is called since listen port binds then
        // But the property should be available
        server.Dispose();
    }

    // Go: TestRouteCompressionOptions server/routes_test.go:3801
    [Fact]
    public void RouteCompression_enum_has_expected_values()
    {
        RouteCompression.None.ShouldBe(RouteCompression.None);
        // Verify the enum is parseable from a string value
        Enum.TryParse<RouteCompression>("None", out var result).ShouldBeTrue();
        result.ShouldBe(RouteCompression.None);
    }

    // Go: TestRouteCompressionOptions server/routes_test.go:3801
    [Fact]
    public void RouteCompressionCodec_round_trips_payload()
    {
        var payload = Encoding.UTF8.GetBytes("This is a test payload for compression round-trip.");
        var compressed = RouteCompressionCodec.Compress(payload);
        var decompressed = RouteCompressionCodec.Decompress(compressed);
        decompressed.ShouldBe(payload);
    }

    // Go: TestRouteCompressionOptions server/routes_test.go:3801
    [Fact]
    public void RouteCompressionCodec_handles_empty_payload()
    {
        var payload = Array.Empty<byte>();
        var compressed = RouteCompressionCodec.Compress(payload);
        var decompressed = RouteCompressionCodec.Decompress(compressed);
        decompressed.ShouldBe(payload);
    }

    // Go: TestRouteCompressionOptions server/routes_test.go:3801
    [Fact]
    public void RouteCompressionCodec_handles_large_payload()
    {
        var payload = new byte[64 * 1024];
        Random.Shared.NextBytes(payload);
        var compressed = RouteCompressionCodec.Compress(payload);
        var decompressed = RouteCompressionCodec.Decompress(compressed);
        decompressed.ShouldBe(payload);
    }

    // Go: TestRouteCompressionOptions server/routes_test.go:3801
    [Fact]
    public void RouteCompressionCodec_compresses_redundant_data()
    {
        var payload = Encoding.UTF8.GetBytes(new string('x', 1024));
        var compressed = RouteCompressionCodec.Compress(payload);
        // Redundant data should compress smaller than original
        compressed.Length.ShouldBeLessThan(payload.Length);
    }

    // Go: Route connect info JSON
    [Fact]
    public void BuildConnectInfoJson_includes_server_id()
    {
        var json = RouteConnection.BuildConnectInfoJson("S1", null, null);
        json.ShouldContain("\"server_id\":\"S1\"");
    }

    // Go: Route connect info JSON with accounts
    [Fact]
    public void BuildConnectInfoJson_includes_accounts()
    {
        var json = RouteConnection.BuildConnectInfoJson("S1", ["A", "B"], null);
        json.ShouldContain("\"accounts\":[\"A\",\"B\"]");
    }

    // Go: Route connect info JSON with topology
    [Fact]
    public void BuildConnectInfoJson_includes_topology()
    {
        var json = RouteConnection.BuildConnectInfoJson("S1", null, "topo-v1");
        json.ShouldContain("\"topology\":\"topo-v1\"");
    }

    // Go: Route connect info JSON empty accounts
    [Fact]
    public void BuildConnectInfoJson_empty_accounts_when_null()
    {
        var json = RouteConnection.BuildConnectInfoJson("S1", null, null);
        json.ShouldContain("\"accounts\":[]");
    }

    // Go: Topology snapshot
    [Fact]
    public void RouteManager_topology_snapshot_reports_initial_state()
    {
        var manager = new RouteManager(
            new ClusterOptions { Host = "127.0.0.1", Port = 0 },
            new ServerStats(),
            "test-server-id",
            _ => { },
            _ => { },
            NullLogger<RouteManager>.Instance);

        var snapshot = manager.BuildTopologySnapshot();
        snapshot.ServerId.ShouldBe("test-server-id");
        snapshot.RouteCount.ShouldBe(0);
        snapshot.ConnectedServerIds.ShouldBeEmpty();
    }

    // Go: TestRoutePerAccountDefaultForSysAccount server/routes_test.go:2705
    [Fact]
    public async Task Cluster_with_accounts_list_still_forms_routes()
    {
        var cluster = Guid.NewGuid().ToString("N");
        var optsA = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Cluster = new ClusterOptions
            {
                Name = cluster,
                Host = "127.0.0.1",
                Port = 0,
                Accounts = ["A"],
            },
        };
        var a = await StartServerAsync(optsA);

        var optsB = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Cluster = new ClusterOptions
            {
                Name = cluster,
                Host = "127.0.0.1",
                Port = 0,
                Accounts = ["A"],
                Routes = [a.Server.ClusterListen!],
            },
        };
        var b = await StartServerAsync(optsB);

        try
        {
            await WaitForRouteFormation(a.Server, b.Server);
            Interlocked.Read(ref a.Server.Stats.Routes).ShouldBeGreaterThan(0);
        }
        finally
        {
            await DisposeServers(a, b);
        }
    }

    // Go: TestRoutePoolSizeDifferentOnEachServer server/routes_test.go:2254
    [Fact]
    public async Task Different_pool_sizes_form_routes()
    {
        var cluster = Guid.NewGuid().ToString("N");
        var optsA = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Cluster = new ClusterOptions
            {
                Name = cluster,
                Host = "127.0.0.1",
                Port = 0,
                PoolSize = 1,
            },
        };
        var a = await StartServerAsync(optsA);

        var optsB = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Cluster = new ClusterOptions
            {
                Name = cluster,
                Host = "127.0.0.1",
                Port = 0,
                PoolSize = 5,
                Routes = [a.Server.ClusterListen!],
            },
        };
        var b = await StartServerAsync(optsB);

        try
        {
            await WaitForRouteFormation(a.Server, b.Server);
            Interlocked.Read(ref a.Server.Stats.Routes).ShouldBeGreaterThan(0);
            Interlocked.Read(ref b.Server.Stats.Routes).ShouldBeGreaterThan(0);
        }
        finally
        {
            await DisposeServers(a, b);
        }
    }

    // Go: TestRoutePoolAndPerAccountErrors server/routes_test.go:1906
    [Fact]
    public async Task Server_with_cluster_reports_route_count_in_stats()
    {
        var cluster = Guid.NewGuid().ToString("N");
        var a = await StartServerAsync(MakeClusterOpts(cluster));
        var b = await StartServerAsync(MakeClusterOpts(cluster, a.Server.ClusterListen!));

        try
        {
            await WaitForRouteFormation(a.Server, b.Server);
            a.Server.Stats.Routes.ShouldBeGreaterThan(0);
            b.Server.Stats.Routes.ShouldBeGreaterThan(0);
        }
        finally
        {
            await DisposeServers(a, b);
        }
    }

    // Go: TestRouteConfigureWriteDeadline server/routes_test.go:4981
    [Fact]
    public void NatsOptions_cluster_is_null_by_default()
    {
        var opts = new NatsOptions();
        opts.Cluster.ShouldBeNull();
    }

    // Go: TestRouteUseIPv6 server/routes_test.go:658 (IPv4 variant)
    [Fact]
    public async Task Cluster_with_127_0_0_1_binds_and_forms_route()
    {
        var cluster = Guid.NewGuid().ToString("N");
        var optsA = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Cluster = new ClusterOptions
            {
                Name = cluster,
                Host = "127.0.0.1",
                Port = 0,
            },
        };
        var a = await StartServerAsync(optsA);
        a.Server.ClusterListen.ShouldNotBeNull();
        a.Server.ClusterListen.ShouldStartWith("127.0.0.1:");

        var optsB = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Cluster = new ClusterOptions
            {
                Name = cluster,
                Host = "127.0.0.1",
                Port = 0,
                Routes = [a.Server.ClusterListen!],
            },
        };
        var b = await StartServerAsync(optsB);

        try
        {
            await WaitForRouteFormation(a.Server, b.Server);
            Interlocked.Read(ref a.Server.Stats.Routes).ShouldBeGreaterThan(0);
        }
        finally
        {
            await DisposeServers(a, b);
        }
    }

    // Go: TestRoutePerAccountGossipWorks server/routes_test.go:2867
    [Fact]
    public void RouteManager_initial_route_count_is_zero()
    {
        var manager = new RouteManager(
            new ClusterOptions { Host = "127.0.0.1", Port = 0 },
            new ServerStats(),
            "S1",
            _ => { },
            _ => { },
            NullLogger<RouteManager>.Instance);

        manager.RouteCount.ShouldBe(0);
    }

    // Go: TestRouteSaveTLSName server/routes_test.go:1816 (server ID tracking)
    [Fact]
    public async Task Server_has_unique_server_id_after_start()
    {
        var cluster = Guid.NewGuid().ToString("N");
        var a = await StartServerAsync(MakeClusterOpts(cluster));
        var b = await StartServerAsync(MakeClusterOpts(cluster, a.Server.ClusterListen!));

        try
        {
            a.Server.ServerId.ShouldNotBeNullOrEmpty();
            b.Server.ServerId.ShouldNotBeNullOrEmpty();
            a.Server.ServerId.ShouldNotBe(b.Server.ServerId);
        }
        finally
        {
            await DisposeServers(a, b);
        }
    }

    // Go: TestRoutePerAccount server/routes_test.go:2539 (multi-account cluster)
    [Fact]
    public async Task Cluster_with_auth_users_forms_routes_and_forwards()
    {
        var users = new User[]
        {
            new() { Username = "admin", Password = "pwd", Account = "ADMIN" },
        };
        var cluster = Guid.NewGuid().ToString("N");

        var optsA = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Users = users,
            Cluster = new ClusterOptions
            {
                Name = cluster,
                Host = "127.0.0.1",
                Port = 0,
            },
        };
        var a = await StartServerAsync(optsA);

        var optsB = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Users = users,
            Cluster = new ClusterOptions
            {
                Name = cluster,
                Host = "127.0.0.1",
                Port = 0,
                Routes = [a.Server.ClusterListen!],
            },
        };
        var b = await StartServerAsync(optsB);

        try
        {
            await WaitForRouteFormation(a.Server, b.Server);

            await using var subscriber = new NatsConnection(new NatsOpts
            {
                Url = $"nats://admin:pwd@127.0.0.1:{a.Server.Port}",
            });
            await subscriber.ConnectAsync();
            await using var sub = await subscriber.SubscribeCoreAsync<string>("auth.test");
            await subscriber.PingAsync();

            await WaitForCondition(() => b.Server.HasRemoteInterest("ADMIN", "auth.test"));

            await using var publisher = new NatsConnection(new NatsOpts
            {
                Url = $"nats://admin:pwd@127.0.0.1:{b.Server.Port}",
            });
            await publisher.ConnectAsync();
            await publisher.PublishAsync("auth.test", "authenticated");

            using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
            var msg = await sub.Msgs.ReadAsync(timeout.Token);
            msg.Data.ShouldBe("authenticated");
        }
        finally
        {
            await DisposeServers(a, b);
        }
    }

    // Go: TestRoutePoolBadAuthNoRunawayCreateRoute server/routes_test.go:3745
    [Fact]
    public async Task Route_ephemeral_port_resolves_correctly()
    {
        var cluster = Guid.NewGuid().ToString("N");
        var optsA = new NatsOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Cluster = new ClusterOptions
            {
                Name = cluster,
                Host = "127.0.0.1",
                Port = 0, // ephemeral
            },
        };
        var a = await StartServerAsync(optsA);

        try
        {
            a.Server.ClusterListen.ShouldNotBeNull();
            var parts = a.Server.ClusterListen!.Split(':');
            parts.Length.ShouldBe(2);
            int.TryParse(parts[1], out var port).ShouldBeTrue();
            port.ShouldBeGreaterThan(0);
        }
        finally
        {
            await a.Cts.CancelAsync();
            a.Server.Dispose();
            a.Cts.Dispose();
        }
    }

    // Go: TestRouteNoRaceOnClusterNameNegotiation server/routes_test.go:4775
    [Fact]
    public async Task Cluster_name_is_preserved_across_route()
    {
        var clusterName = "test-cluster-name-preservation";
        var a = await StartServerAsync(MakeClusterOpts(clusterName));
        var b = await StartServerAsync(MakeClusterOpts(clusterName, a.Server.ClusterListen!));

        try
        {
            await WaitForRouteFormation(a.Server, b.Server);
            // Both servers should be operational
            Interlocked.Read(ref a.Server.Stats.Routes).ShouldBeGreaterThan(0);
            Interlocked.Read(ref b.Server.Stats.Routes).ShouldBeGreaterThan(0);
        }
        finally
        {
            await DisposeServers(a, b);
        }
    }
}
