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

namespace NATS.Server.Tests;

public class IntegrationTests : IAsyncLifetime
{
    private readonly NatsServer _server;
    private readonly int _port;
    private readonly CancellationTokenSource _cts = new();
    private Task _serverTask = null!;

    public IntegrationTests()
    {
        _port = GetFreePort();
        _server = new NatsServer(new NatsOptions { Port = _port }, NullLoggerFactory.Instance);
    }

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

    public async Task DisposeAsync()
    {
        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 NatsConnection CreateClient()
    {
        var opts = new NatsOpts { Url = $"nats://127.0.0.1:{_port}" };
        return new NatsConnection(opts);
    }

    [Fact]
    public async Task PubSub_basic()
    {
        await using var pub = CreateClient();
        await using var sub = CreateClient();

        await pub.ConnectAsync();
        await sub.ConnectAsync();

        await using var subscription = await sub.SubscribeCoreAsync<string>("test.subject");
        await sub.PingAsync();

        await pub.PublishAsync("test.subject", "Hello NATS!");

        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var msg = await subscription.Msgs.ReadAsync(timeout.Token);
        msg.Data.ShouldBe("Hello NATS!");
    }

    [Fact]
    public async Task PubSub_wildcard_star()
    {
        await using var pub = CreateClient();
        await using var sub = CreateClient();

        await pub.ConnectAsync();
        await sub.ConnectAsync();

        await using var subscription = await sub.SubscribeCoreAsync<string>("test.*");
        await sub.PingAsync();

        await pub.PublishAsync("test.hello", "data");

        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var msg = await subscription.Msgs.ReadAsync(timeout.Token);
        msg.Subject.ShouldBe("test.hello");
    }

    [Fact]
    public async Task PubSub_wildcard_gt()
    {
        await using var pub = CreateClient();
        await using var sub = CreateClient();

        await pub.ConnectAsync();
        await sub.ConnectAsync();

        await using var subscription = await sub.SubscribeCoreAsync<string>("test.>");
        await sub.PingAsync();

        await pub.PublishAsync("test.foo.bar.baz", "data");

        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var msg = await subscription.Msgs.ReadAsync(timeout.Token);
        msg.Subject.ShouldBe("test.foo.bar.baz");
    }

    [Fact]
    public async Task PubSub_fan_out()
    {
        await using var pub = CreateClient();
        await using var sub1 = CreateClient();
        await using var sub2 = CreateClient();

        await pub.ConnectAsync();
        await sub1.ConnectAsync();
        await sub2.ConnectAsync();

        await using var s1 = await sub1.SubscribeCoreAsync<string>("fanout");
        await using var s2 = await sub2.SubscribeCoreAsync<string>("fanout");
        await sub1.PingAsync();
        await sub2.PingAsync();

        await pub.PublishAsync("fanout", "hello");

        using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var msg1 = await s1.Msgs.ReadAsync(timeout.Token);
        var msg2 = await s2.Msgs.ReadAsync(timeout.Token);
        msg1.Data.ShouldBe("hello");
        msg2.Data.ShouldBe("hello");
    }

    [Fact]
    public async Task PingPong()
    {
        await using var client = CreateClient();
        await client.ConnectAsync();

        // If we got here, the connection is alive and PING/PONG works
        await client.PingAsync();
    }
}
