using System.Net;
using System.Net.Sockets;
using System.Text;
using NATS.Server.Mqtt;

namespace NATS.Server.Tests.Mqtt;

public class MqttSessionRuntimeTests
{
    [Fact]
    public async Task Qos1_publish_receives_puback_and_redelivery_on_session_reconnect_when_unacked()
    {
        await using var listener = new MqttListener("127.0.0.1", 0);
        using var cts = new CancellationTokenSource();
        await listener.StartAsync(cts.Token);

        using (var first = new TcpClient())
        {
            await first.ConnectAsync(IPAddress.Loopback, listener.Port);
            var firstStream = first.GetStream();
            await MqttRuntimeWire.WriteLineAsync(firstStream, "CONNECT session-client clean=false");
            (await MqttRuntimeWire.ReadLineAsync(firstStream, 1000)).ShouldBe("CONNACK");

            await MqttRuntimeWire.WriteLineAsync(firstStream, "PUBQ1 21 sensors.temp 99");
            (await MqttRuntimeWire.ReadLineAsync(firstStream, 1000)).ShouldBe("PUBACK 21");
        }

        using var second = new TcpClient();
        await second.ConnectAsync(IPAddress.Loopback, listener.Port);
        var secondStream = second.GetStream();
        await MqttRuntimeWire.WriteLineAsync(secondStream, "CONNECT session-client clean=false");
        (await MqttRuntimeWire.ReadLineAsync(secondStream, 1000)).ShouldBe("CONNACK");
        (await MqttRuntimeWire.ReadLineAsync(secondStream, 1000)).ShouldBe("REDLIVER 21 sensors.temp 99");
    }
}

internal static class MqttRuntimeWire
{
    public static async Task WriteLineAsync(NetworkStream stream, string line)
    {
        var bytes = Encoding.UTF8.GetBytes(line + "\n");
        await stream.WriteAsync(bytes);
        await stream.FlushAsync();
    }

    public static async Task<string?> ReadLineAsync(NetworkStream stream, int timeoutMs)
    {
        using var timeout = new CancellationTokenSource(timeoutMs);
        var bytes = new List<byte>();
        var one = new byte[1];
        try
        {
            while (true)
            {
                var read = await stream.ReadAsync(one.AsMemory(0, 1), timeout.Token);
                if (read == 0)
                    return null;
                if (one[0] == (byte)'\n')
                    break;
                if (one[0] != (byte)'\r')
                    bytes.Add(one[0]);
            }
        }
        catch (OperationCanceledException)
        {
            return null;
        }

        return Encoding.UTF8.GetString([.. bytes]);
    }

    public static async Task<string?> ReadRawAsync(NetworkStream stream, int timeoutMs)
    {
        using var timeout = new CancellationTokenSource(timeoutMs);
        var one = new byte[1];
        try
        {
            var read = await stream.ReadAsync(one.AsMemory(0, 1), timeout.Token);
            if (read == 0)
                return null;

            return Encoding.UTF8.GetString(one, 0, read);
        }
        catch (OperationCanceledException)
        {
            return "__timeout__";
        }
    }
}
