using System.Net;
using System.Net.Sockets;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Server;
using NATS.Server.Auth;
using NATS.Server.Monitoring;

namespace NATS.Server.Tests.Monitoring;

/// <summary>
/// Tests for ConnzHandler filtering, sorting, pagination, and closed connection
/// ring buffer behavior.
/// Go reference: monitor_test.go — TestConnz, TestConnzSortedByCid, TestConnzSortedByBytesTo,
/// TestConnzFilter, TestConnzWithCID, TestConnzOffsetAndLimit.
/// </summary>
public class ConnzFilterTests : IAsyncLifetime
{
    private readonly NatsServer _server;
    private readonly NatsOptions _opts;
    private readonly CancellationTokenSource _cts = new();
    private readonly List<Socket> _sockets = [];

    public ConnzFilterTests()
    {
        _opts = new NatsOptions
        {
            Port = GetFreePort(),
            MaxClosedClients = 100,
            Users =
            [
                new User { Username = "alice", Password = "pw", Account = "acctA" },
                new User { Username = "bob", Password = "pw", Account = "acctB" },
            ],
        };
        _server = new NatsServer(_opts, NullLoggerFactory.Instance);
    }

    public async Task InitializeAsync()
    {
        _ = _server.StartAsync(_cts.Token);
        await _server.WaitForReadyAsync();
    }

    public async Task DisposeAsync()
    {
        foreach (var s in _sockets)
        {
            try { s.Shutdown(SocketShutdown.Both); } catch { }
            s.Dispose();
        }
        await _cts.CancelAsync();
        _server.Dispose();
    }

    private static int GetFreePort()
    {
        using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        sock.Bind(new IPEndPoint(IPAddress.Loopback, 0));
        return ((IPEndPoint)sock.LocalEndPoint!).Port;
    }

    private async Task<Socket> ConnectAsync(string user, string? subjectToSubscribe = null)
    {
        var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        _sockets.Add(sock);
        await sock.ConnectAsync(IPAddress.Loopback, _opts.Port);

        var buf = new byte[4096];
        await sock.ReceiveAsync(buf, SocketFlags.None); // INFO

        var connect = $"CONNECT {{\"user\":\"{user}\",\"pass\":\"pw\"}}\r\n";
        await sock.SendAsync(Encoding.ASCII.GetBytes(connect));

        if (subjectToSubscribe != null)
        {
            await sock.SendAsync(Encoding.ASCII.GetBytes($"SUB {subjectToSubscribe} sid1\r\n"));
        }

        await sock.SendAsync("PING\r\n"u8.ToArray());
        await ReadUntilAsync(sock, "PONG");
        return sock;
    }

    private Connz GetConnz(string queryString = "")
    {
        var ctx = new DefaultHttpContext();
        ctx.Request.QueryString = new QueryString(queryString);
        return new ConnzHandler(_server).HandleConnz(ctx);
    }

    // --- Sort tests ---

    [Fact]
    public async Task Sort_by_cid_returns_ascending_order()
    {
        await ConnectAsync("alice");
        await ConnectAsync("bob");
        await Task.Delay(50);

        var connz = GetConnz("?sort=cid");
        connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2);

        for (int i = 1; i < connz.Conns.Length; i++)
        {
            connz.Conns[i].Cid.ShouldBeGreaterThan(connz.Conns[i - 1].Cid);
        }
    }

    [Fact]
    public async Task Sort_by_bytes_to_returns_descending_order()
    {
        var sock1 = await ConnectAsync("alice");
        var sock2 = await ConnectAsync("bob");
        await Task.Delay(50);

        // Publish some data through sock1 to accumulate bytes
        await sock1.SendAsync(Encoding.ASCII.GetBytes("SUB test 1\r\nPUB test 10\r\n1234567890\r\n"));
        await Task.Delay(100);

        var connz = GetConnz("?sort=bytes_to");
        connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2);

        for (int i = 1; i < connz.Conns.Length; i++)
        {
            connz.Conns[i].OutBytes.ShouldBeLessThanOrEqualTo(connz.Conns[i - 1].OutBytes);
        }
    }

    [Fact]
    public async Task Sort_by_msgs_from_returns_descending_order()
    {
        var sock1 = await ConnectAsync("alice");
        await Task.Delay(50);

        // Send a PUB to increment InMsgs
        await sock1.SendAsync(Encoding.ASCII.GetBytes("PUB test 3\r\nabc\r\n"));
        await Task.Delay(100);

        var connz = GetConnz("?sort=msgs_from");
        connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(1);

        for (int i = 1; i < connz.Conns.Length; i++)
        {
            connz.Conns[i].InMsgs.ShouldBeLessThanOrEqualTo(connz.Conns[i - 1].InMsgs);
        }
    }

    [Fact]
    public async Task Sort_by_subs_returns_descending_order()
    {
        // Alice has 2 subs, Bob has 1
        var sock1 = await ConnectAsync("alice", "test.a");
        await sock1.SendAsync(Encoding.ASCII.GetBytes("SUB test.b sid2\r\n"));
        var sock2 = await ConnectAsync("bob", "test.c");
        await Task.Delay(100);

        var connz = GetConnz("?sort=subs");
        connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2);

        for (int i = 1; i < connz.Conns.Length; i++)
        {
            connz.Conns[i].NumSubs.ShouldBeLessThanOrEqualTo(connz.Conns[i - 1].NumSubs);
        }
    }

    [Fact]
    public async Task Sort_by_start_returns_ascending_order()
    {
        await ConnectAsync("alice");
        await Task.Delay(20);
        await ConnectAsync("bob");
        await Task.Delay(50);

        var connz = GetConnz("?sort=start");
        connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2);

        for (int i = 1; i < connz.Conns.Length; i++)
        {
            connz.Conns[i].Start.ShouldBeGreaterThanOrEqualTo(connz.Conns[i - 1].Start);
        }
    }

    // --- Filter tests ---

    [Fact]
    public async Task Filter_by_account_returns_only_matching_connections()
    {
        await ConnectAsync("alice");
        await ConnectAsync("bob");
        await Task.Delay(50);

        var connz = GetConnz("?acc=acctA");
        connz.Conns.ShouldAllBe(c => c.Account == "acctA");
        connz.Conns.ShouldNotBeEmpty();
    }

    [Fact]
    public async Task Filter_by_user_returns_only_matching_connections()
    {
        await ConnectAsync("alice");
        await ConnectAsync("bob");
        await Task.Delay(50);

        var connz = GetConnz("?user=bob");
        connz.Conns.ShouldAllBe(c => c.AuthorizedUser == "bob");
        connz.Conns.ShouldNotBeEmpty();
    }

    [Fact]
    public async Task Filter_by_subject_returns_matching_subscribers()
    {
        await ConnectAsync("alice", "orders.>");
        await ConnectAsync("bob", "payments.>");
        await Task.Delay(50);

        var connz = GetConnz("?filter_subject=orders.new&subs=1");
        connz.Conns.ShouldNotBeEmpty();
        connz.Conns.ShouldAllBe(c => c.Subs.Any(s => s.Contains("orders")));
    }

    // --- Pagination tests ---

    [Fact]
    public async Task Offset_and_limit_paginates_results()
    {
        await ConnectAsync("alice");
        await ConnectAsync("bob");
        await ConnectAsync("alice");
        await Task.Delay(50);

        var page1 = GetConnz("?sort=cid&limit=2&offset=0");
        page1.Conns.Length.ShouldBe(2);
        page1.Total.ShouldBeGreaterThanOrEqualTo(3);
        page1.Offset.ShouldBe(0);
        page1.Limit.ShouldBe(2);

        var page2 = GetConnz("?sort=cid&limit=2&offset=2");
        page2.Conns.Length.ShouldBeGreaterThanOrEqualTo(1);
        page2.Offset.ShouldBe(2);

        // Ensure no overlap between pages
        var page1Cids = page1.Conns.Select(c => c.Cid).ToHashSet();
        var page2Cids = page2.Conns.Select(c => c.Cid).ToHashSet();
        page1Cids.Overlaps(page2Cids).ShouldBeFalse();
    }

    // --- CID lookup test ---

    [Fact]
    public async Task Cid_lookup_returns_single_connection()
    {
        await ConnectAsync("alice");
        await ConnectAsync("bob");
        await Task.Delay(50);

        // Get all connections to find a known CID
        var all = GetConnz("?sort=cid");
        all.Conns.ShouldNotBeEmpty();
        var targetCid = all.Conns[0].Cid;

        var single = GetConnz($"?cid={targetCid}");
        single.Conns.Length.ShouldBe(1);
        single.Conns[0].Cid.ShouldBe(targetCid);
    }

    [Fact]
    public void Cid_lookup_nonexistent_returns_empty()
    {
        var result = GetConnz("?cid=99999999");
        result.Conns.Length.ShouldBe(0);
        result.Total.ShouldBe(0);
    }

    // --- Closed connection tests ---

    [Fact]
    public async Task Closed_state_shows_disconnected_clients()
    {
        var sock = await ConnectAsync("alice");
        await Task.Delay(50);

        // Close the connection
        sock.Shutdown(SocketShutdown.Both);
        sock.Close();
        _sockets.Remove(sock);
        await Task.Delay(200);

        var connz = GetConnz("?state=closed");
        connz.Conns.ShouldNotBeEmpty();
        connz.Conns.ShouldAllBe(c => c.Stop != null);
        connz.Conns.ShouldAllBe(c => !string.IsNullOrEmpty(c.Reason));
    }

    [Fact]
    public async Task All_state_shows_both_open_and_closed()
    {
        var sock1 = await ConnectAsync("alice");
        var sock2 = await ConnectAsync("bob");
        await Task.Delay(50);

        // Close one connection
        sock1.Shutdown(SocketShutdown.Both);
        sock1.Close();
        _sockets.Remove(sock1);
        await Task.Delay(200);

        var connz = GetConnz("?state=all");
        connz.Total.ShouldBeGreaterThanOrEqualTo(2);
        // Should have at least one open (bob) and one closed (alice)
        connz.Conns.Any(c => c.Stop == null).ShouldBeTrue("expected at least one open connection");
        connz.Conns.Any(c => c.Stop != null).ShouldBeTrue("expected at least one closed connection");
    }

    [Fact]
    public async Task Closed_ring_buffer_caps_at_max()
    {
        // MaxClosedClients is 100, create and close 5 connections
        for (int i = 0; i < 5; i++)
        {
            var sock = await ConnectAsync("alice");
            await Task.Delay(20);
            sock.Shutdown(SocketShutdown.Both);
            sock.Close();
            _sockets.Remove(sock);
            await Task.Delay(100);
        }

        var connz = GetConnz("?state=closed");
        connz.Total.ShouldBeLessThanOrEqualTo(_opts.MaxClosedClients);
    }

    // --- Sort fallback tests ---

    [Fact]
    public async Task Sort_by_stop_with_open_state_falls_back_to_cid()
    {
        await ConnectAsync("alice");
        await ConnectAsync("bob");
        await Task.Delay(50);

        // sort=stop with state=open should fall back to cid sorting
        var connz = GetConnz("?sort=stop&state=open");
        connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(2);

        for (int i = 1; i < connz.Conns.Length; i++)
        {
            connz.Conns[i].Cid.ShouldBeGreaterThan(connz.Conns[i - 1].Cid);
        }
    }

    // --- Combined filter + sort test ---

    [Fact]
    public async Task Account_filter_with_bytes_sort_and_limit()
    {
        // Connect multiple alice clients
        for (int i = 0; i < 3; i++)
        {
            var sock = await ConnectAsync("alice");
            // Send varying amounts of data
            var data = new string('x', (i + 1) * 100);
            await sock.SendAsync(Encoding.ASCII.GetBytes($"SUB test 1\r\nPUB test {data.Length}\r\n{data}\r\n"));
        }
        await ConnectAsync("bob");
        await Task.Delay(100);

        var connz = GetConnz("?acc=acctA&sort=bytes_to&limit=2");
        connz.Conns.Length.ShouldBeLessThanOrEqualTo(2);
        connz.Conns.ShouldAllBe(c => c.Account == "acctA");
    }

    [Fact]
    public async Task Closed_cid_lookup_returns_from_ring_buffer()
    {
        var sock = await ConnectAsync("alice");
        await Task.Delay(50);

        // Get the CID before closing
        var all = GetConnz("?sort=cid");
        all.Conns.ShouldNotBeEmpty();
        var targetCid = all.Conns.Last().Cid;

        // Close the socket
        sock.Shutdown(SocketShutdown.Both);
        sock.Close();
        _sockets.Remove(sock);
        await Task.Delay(200);

        // Look up closed connection by CID
        var single = GetConnz($"?cid={targetCid}");
        single.Conns.Length.ShouldBe(1);
        single.Conns[0].Cid.ShouldBe(targetCid);
        single.Conns[0].Stop.ShouldNotBeNull();
    }

    private static async Task ReadUntilAsync(Socket sock, string expected)
    {
        var buf = new byte[4096];
        var all = new StringBuilder();
        var deadline = DateTime.UtcNow.AddSeconds(5);
        while (DateTime.UtcNow < deadline)
        {
            if (sock.Available > 0)
            {
                var n = await sock.ReceiveAsync(buf, SocketFlags.None);
                all.Append(Encoding.ASCII.GetString(buf, 0, n));
                if (all.ToString().Contains(expected))
                    return;
            }
            else
            {
                await Task.Delay(10);
            }
        }

        throw new TimeoutException($"Did not receive '{expected}' within 5 seconds. Got: {all}");
    }
}
