// Go: TestSubsz server/monitor_test.go:1538
// Go: TestMonitorSubszDetails server/monitor_test.go:1609
// Go: TestMonitorSubszWithOffsetAndLimit server/monitor_test.go:1642
// Go: TestMonitorSubszTestPubSubject server/monitor_test.go:1675
// Go: TestMonitorSubszMultiAccount server/monitor_test.go:1709
// Go: TestMonitorSubszMultiAccountWithOffsetAndLimit server/monitor_test.go:1777

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

namespace NATS.Server.Tests.Monitoring;

/// <summary>
/// Tests covering /subz (subscriptionsz) endpoint behavior,
/// ported from the Go server's monitor_test.go.
/// </summary>
public class MonitorSubszTests : IAsyncLifetime
{
    private readonly NatsServer _server;
    private readonly int _natsPort;
    private readonly int _monitorPort;
    private readonly CancellationTokenSource _cts = new();
    private readonly HttpClient _http = new();

    public MonitorSubszTests()
    {
        _natsPort = GetFreePort();
        _monitorPort = GetFreePort();
        _server = new NatsServer(
            new NatsOptions { Port = _natsPort, MonitorPort = _monitorPort },
            NullLoggerFactory.Instance);
    }

    public async Task InitializeAsync()
    {
        _ = _server.StartAsync(_cts.Token);
        await _server.WaitForReadyAsync();
        for (var i = 0; i < 50; i++)
        {
            try
            {
                var probe = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/healthz");
                if (probe.IsSuccessStatusCode) break;
            }
            catch (HttpRequestException) { }
            await Task.Delay(50);
        }
    }

    public async Task DisposeAsync()
    {
        _http.Dispose();
        await _cts.CancelAsync();
        _server.Dispose();
    }

    /// <summary>
    /// Go: TestSubsz (line 1538).
    /// Verifies /subz returns valid JSON with server_id, num_subscriptions fields.
    /// </summary>
    [Fact]
    public async Task Subz_returns_valid_json_with_server_id()
    {
        var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/subz");
        response.StatusCode.ShouldBe(HttpStatusCode.OK);

        var subsz = await response.Content.ReadFromJsonAsync<Subsz>();
        subsz.ShouldNotBeNull();
        subsz.Id.ShouldNotBeNullOrEmpty();
    }

    /// <summary>
    /// Go: TestSubsz (line 1538).
    /// Verifies /subz reports num_subscriptions after clients subscribe.
    /// </summary>
    [Fact]
    public async Task Subz_reports_subscription_count()
    {
        using var sock = await ConnectClientAsync("SUB foo 1\r\n");
        await Task.Delay(200);

        var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz");
        subsz.ShouldNotBeNull();
        subsz.NumSubs.ShouldBeGreaterThanOrEqualTo(1u);
    }

    /// <summary>
    /// Go: TestMonitorSubszDetails (line 1609).
    /// Verifies /subz?subs=1 returns subscription details with subject info.
    /// </summary>
    [Fact]
    public async Task Subz_with_subs_returns_subscription_details()
    {
        using var sock = await ConnectClientAsync("SUB foo.* 1\r\nSUB foo.bar 2\r\nSUB foo.foo 3\r\n");
        await Task.Delay(200);

        var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz?subs=1");
        subsz.ShouldNotBeNull();

        // Go: sl.NumSubs != 3, sl.Total != 3, len(sl.Subs) != 3
        subsz.NumSubs.ShouldBeGreaterThanOrEqualTo(3u);
        subsz.Total.ShouldBeGreaterThanOrEqualTo(3);
        subsz.Subs.Length.ShouldBeGreaterThanOrEqualTo(3);
    }

    /// <summary>
    /// Go: TestMonitorSubszDetails (line 1609).
    /// Verifies subscription detail entries contain the correct subject names.
    /// </summary>
    [Fact]
    public async Task Subz_detail_entries_contain_subject_names()
    {
        using var sock = await ConnectClientAsync("SUB foo.bar 1\r\nSUB foo.baz 2\r\n");
        await Task.Delay(200);

        var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz?subs=1");
        subsz.ShouldNotBeNull();
        subsz.Subs.ShouldContain(s => s.Subject == "foo.bar");
        subsz.Subs.ShouldContain(s => s.Subject == "foo.baz");
    }

    /// <summary>
    /// Go: TestMonitorSubszWithOffsetAndLimit (line 1642).
    /// Verifies /subz pagination with offset and limit parameters.
    /// </summary>
    [Fact]
    public async Task Subz_pagination_with_offset_and_limit()
    {
        // Create many subscriptions
        using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort));
        var buf = new byte[4096];
        _ = await sock.ReceiveAsync(buf, SocketFlags.None);
        await sock.SendAsync("CONNECT {}\r\n"u8.ToArray(), SocketFlags.None);

        for (var i = 0; i < 200; i++)
            await sock.SendAsync(System.Text.Encoding.ASCII.GetBytes($"SUB foo.{i} {i + 1}\r\n"), SocketFlags.None);

        await Task.Delay(300);

        var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz?subs=1&offset=10&limit=100");
        subsz.ShouldNotBeNull();

        // Go: sl.NumSubs != 200, sl.Total != 200, sl.Offset != 10, sl.Limit != 100, len(sl.Subs) != 100
        subsz.NumSubs.ShouldBeGreaterThanOrEqualTo(200u);
        subsz.Total.ShouldBeGreaterThanOrEqualTo(200);
        subsz.Offset.ShouldBe(10);
        subsz.Limit.ShouldBe(100);
        subsz.Subs.Length.ShouldBe(100);
    }

    /// <summary>
    /// Go: TestMonitorSubszTestPubSubject (line 1675).
    /// Verifies /subz?test=foo.foo filters subscriptions matching a concrete subject.
    /// </summary>
    [Fact]
    public async Task Subz_test_subject_filters_matching_subscriptions()
    {
        using var sock = await ConnectClientAsync("SUB foo.* 1\r\nSUB foo.bar 2\r\nSUB foo.foo 3\r\n");
        await Task.Delay(200);

        // foo.foo matches "foo.*" and "foo.foo" but not "foo.bar"
        var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz?subs=1&test=foo.foo");
        subsz.ShouldNotBeNull();

        // Go: sl.Total != 2, len(sl.Subs) != 2
        subsz.Total.ShouldBe(2);
        subsz.Subs.Length.ShouldBe(2);
    }

    /// <summary>
    /// Go: TestMonitorSubszTestPubSubject (line 1675).
    /// Verifies /subz?test=foo returns no matches when no subscription matches exactly.
    /// </summary>
    [Fact]
    public async Task Subz_test_subject_no_match_returns_empty()
    {
        using var sock = await ConnectClientAsync("SUB foo.* 1\r\nSUB foo.bar 2\r\n");
        await Task.Delay(200);

        // "foo" alone does not match "foo.*" or "foo.bar"
        var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz?subs=1&test=foo");
        subsz.ShouldNotBeNull();
        subsz.Subs.Length.ShouldBe(0);
    }

    /// <summary>
    /// Go: TestSubsz (line 1538).
    /// Verifies /subz default has no subscription details (subs not requested).
    /// </summary>
    [Fact]
    public async Task Subz_default_does_not_include_details()
    {
        using var sock = await ConnectClientAsync("SUB foo 1\r\n");
        await Task.Delay(200);

        var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz");
        subsz.ShouldNotBeNull();
        subsz.Subs.Length.ShouldBe(0);
    }

    /// <summary>
    /// Go: TestSubsz (line 1538).
    /// Verifies /subscriptionsz works as an alias for /subz.
    /// </summary>
    [Fact]
    public async Task Subscriptionsz_is_alias_for_subz()
    {
        using var sock = await ConnectClientAsync("SUB foo 1\r\n");
        await Task.Delay(200);

        var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subscriptionsz");
        subsz.ShouldNotBeNull();
        subsz.Id.ShouldNotBeNullOrEmpty();
        subsz.NumSubs.ShouldBeGreaterThanOrEqualTo(1u);
    }

    /// <summary>
    /// Go: TestSubsz (line 1538).
    /// Verifies /subz JSON uses correct Go-compatible field names.
    /// </summary>
    [Fact]
    public async Task Subz_json_uses_go_field_names()
    {
        var body = await _http.GetStringAsync($"http://127.0.0.1:{_monitorPort}/subz");
        body.ShouldContain("\"server_id\"");
        body.ShouldContain("\"num_subscriptions\"");
    }

    /// <summary>
    /// Go: TestMonitorSubszDetails (line 1609).
    /// Verifies subscription details include sid and cid fields.
    /// </summary>
    [Fact]
    public async Task Subz_details_include_sid_and_cid()
    {
        using var sock = await ConnectClientAsync("SUB foo 99\r\n");
        await Task.Delay(200);

        var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz?subs=1");
        subsz.ShouldNotBeNull();
        subsz.Subs.Length.ShouldBeGreaterThanOrEqualTo(1);

        var sub = subsz.Subs.First(s => s.Subject == "foo");
        sub.Sid.ShouldBe("99");
        sub.Cid.ShouldBeGreaterThan(0UL);
    }

    /// <summary>
    /// Go: TestSubsz (line 1538).
    /// Verifies /subz returns HTTP 200 OK.
    /// </summary>
    [Fact]
    public async Task Subz_returns_http_200()
    {
        var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/subz");
        response.StatusCode.ShouldBe(HttpStatusCode.OK);
    }

    /// <summary>
    /// Go: TestSubsz (line 1538).
    /// Verifies /subz num_cache reflects the cache state of the subscription trie.
    /// </summary>
    [Fact]
    public async Task Subz_includes_num_cache()
    {
        var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz");
        subsz.ShouldNotBeNull();
        // num_cache should be >= 0
        subsz.NumCache.ShouldBeGreaterThanOrEqualTo(0);
    }

    /// <summary>
    /// Go: TestMonitorSubszWithOffsetAndLimit (line 1642).
    /// Verifies /subz with offset=0 and limit=0 uses defaults.
    /// </summary>
    [Fact]
    public async Task Subz_offset_zero_uses_default_limit()
    {
        var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz?offset=0");
        subsz.ShouldNotBeNull();
        subsz.Offset.ShouldBe(0);
        subsz.Limit.ShouldBe(1024); // default limit
    }

    /// <summary>
    /// Go: TestMonitorConcurrentMonitoring (line 2148).
    /// Verifies concurrent /subz requests do not cause errors.
    /// </summary>
    [Fact]
    public async Task Subz_handles_concurrent_requests()
    {
        using var sock = await ConnectClientAsync("SUB foo 1\r\n");
        await Task.Delay(200);

        var tasks = Enumerable.Range(0, 10).Select(async _ =>
        {
            var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/subz");
            response.StatusCode.ShouldBe(HttpStatusCode.OK);
        });

        await Task.WhenAll(tasks);
    }

    /// <summary>
    /// Go: TestMonitorSubszTestPubSubject (line 1675).
    /// Verifies /subz?test with wildcard subject foo.* matches foo.bar and foo.baz.
    /// </summary>
    [Fact]
    public async Task Subz_test_wildcard_match()
    {
        using var sock = await ConnectClientAsync("SUB foo.bar 1\r\nSUB foo.baz 2\r\nSUB bar.x 3\r\n");
        await Task.Delay(200);

        // test=foo.bar should match foo.bar literal
        var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz?subs=1&test=foo.bar");
        subsz.ShouldNotBeNull();
        subsz.Total.ShouldBe(1);
        subsz.Subs.Length.ShouldBe(1);
        subsz.Subs[0].Subject.ShouldBe("foo.bar");
    }

    /// <summary>
    /// Go: TestMonitorSubszMultiAccount (line 1709).
    /// Verifies /subz now timestamp is plausible.
    /// </summary>
    [Fact]
    public async Task Subz_now_is_plausible_timestamp()
    {
        var before = DateTime.UtcNow;
        var subsz = await _http.GetFromJsonAsync<Subsz>($"http://127.0.0.1:{_monitorPort}/subz");
        var after = DateTime.UtcNow;

        subsz.ShouldNotBeNull();
        subsz.Now.ShouldBeGreaterThanOrEqualTo(before.AddSeconds(-1));
        subsz.Now.ShouldBeLessThanOrEqualTo(after.AddSeconds(1));
    }

    private async Task<Socket> ConnectClientAsync(string extraCommands)
    {
        var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort));
        var buf = new byte[4096];
        _ = await sock.ReceiveAsync(buf, SocketFlags.None);
        await sock.SendAsync(System.Text.Encoding.ASCII.GetBytes($"CONNECT {{}}\r\n{extraCommands}"), SocketFlags.None);
        return sock;
    }

    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;
    }
}
