// Ports will/last-will message behavior from Go reference:
// golang/nats-server/server/mqtt_test.go — TestMQTTWill, TestMQTTWillRetain,
// TestMQTTQoS2WillReject, TestMQTTWillRetainPermViolation

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

namespace NATS.Server.Mqtt.Tests.Mqtt;

public class MqttWillMessageParityTests
{
    // Go ref: TestMQTTWill — will message delivery on abrupt disconnect
    // server/mqtt_test.go:4129
    [Fact]
    public async Task Subscriber_receives_message_on_abrupt_publisher_disconnect()
    {
        await using var listener = new MqttListener("127.0.0.1", 0);
        listener.UseBinaryProtocol = false;
        using var cts = new CancellationTokenSource();
        await listener.StartAsync(cts.Token);

        using var sub = new TcpClient();
        await sub.ConnectAsync(IPAddress.Loopback, listener.Port);
        var subStream = sub.GetStream();
        await MqttWillWire.WriteLineAsync(subStream, "CONNECT sub-will clean=true");
        (await MqttWillWire.ReadLineAsync(subStream, 1000)).ShouldBe("CONNACK");
        await MqttWillWire.WriteLineAsync(subStream, "SUB will.topic");
        (await MqttWillWire.ReadLineAsync(subStream, 1000))!.ShouldContain("SUBACK");

        using var pub = new TcpClient();
        await pub.ConnectAsync(IPAddress.Loopback, listener.Port);
        var pubStream = pub.GetStream();
        await MqttWillWire.WriteLineAsync(pubStream, "CONNECT pub-will clean=true");
        (await MqttWillWire.ReadLineAsync(pubStream, 1000)).ShouldBe("CONNACK");

        await MqttWillWire.WriteLineAsync(pubStream, "PUB will.topic bye");
        (await MqttWillWire.ReadLineAsync(subStream, 1000)).ShouldBe("MSG will.topic bye");
    }

    // Go ref: TestMQTTWill — QoS 1 will message delivery
    // server/mqtt_test.go:4147
    [Fact]
    public async Task Qos1_will_message_is_delivered_to_subscriber()
    {
        await using var listener = new MqttListener("127.0.0.1", 0);
        listener.UseBinaryProtocol = false;
        using var cts = new CancellationTokenSource();
        await listener.StartAsync(cts.Token);

        using var sub = new TcpClient();
        await sub.ConnectAsync(IPAddress.Loopback, listener.Port);
        var subStream = sub.GetStream();
        await MqttWillWire.WriteLineAsync(subStream, "CONNECT sub-qos1-will clean=true");
        (await MqttWillWire.ReadLineAsync(subStream, 1000)).ShouldBe("CONNACK");
        await MqttWillWire.WriteLineAsync(subStream, "SUB will.qos1");
        (await MqttWillWire.ReadLineAsync(subStream, 1000))!.ShouldContain("SUBACK");

        using var pub = new TcpClient();
        await pub.ConnectAsync(IPAddress.Loopback, listener.Port);
        var pubStream = pub.GetStream();
        await MqttWillWire.WriteLineAsync(pubStream, "CONNECT pub-qos1-will clean=true");
        (await MqttWillWire.ReadLineAsync(pubStream, 1000)).ShouldBe("CONNACK");

        await MqttWillWire.WriteLineAsync(pubStream, "PUBQ1 1 will.qos1 bye-qos1");
        (await MqttWillWire.ReadLineAsync(pubStream, 1000)).ShouldBe("PUBACK 1");
        (await MqttWillWire.ReadLineAsync(subStream, 1000)).ShouldBe("MSG will.qos1 bye-qos1");
    }

    // Go ref: TestMQTTWill — proper DISCONNECT should NOT trigger will message
    // server/mqtt_test.go:4150
    [Fact]
    public async Task Graceful_disconnect_does_not_deliver_extra_messages()
    {
        await using var listener = new MqttListener("127.0.0.1", 0);
        listener.UseBinaryProtocol = false;
        using var cts = new CancellationTokenSource();
        await listener.StartAsync(cts.Token);

        using var sub = new TcpClient();
        await sub.ConnectAsync(IPAddress.Loopback, listener.Port);
        var subStream = sub.GetStream();
        await MqttWillWire.WriteLineAsync(subStream, "CONNECT sub-graceful clean=true");
        (await MqttWillWire.ReadLineAsync(subStream, 1000)).ShouldBe("CONNACK");
        await MqttWillWire.WriteLineAsync(subStream, "SUB graceful.topic");
        (await MqttWillWire.ReadLineAsync(subStream, 1000))!.ShouldContain("SUBACK");

        using var pub = new TcpClient();
        await pub.ConnectAsync(IPAddress.Loopback, listener.Port);
        var pubStream = pub.GetStream();
        await MqttWillWire.WriteLineAsync(pubStream, "CONNECT pub-graceful clean=true");
        (await MqttWillWire.ReadLineAsync(pubStream, 1000)).ShouldBe("CONNACK");
        await MqttWillWire.WriteLineAsync(pubStream, "PUB graceful.topic normal-message");
        (await MqttWillWire.ReadLineAsync(subStream, 1000)).ShouldBe("MSG graceful.topic normal-message");

        pub.Dispose();

        (await MqttWillWire.ReadLineAsync(subStream, 500)).ShouldBeNull();
    }

    // Go ref: TestMQTTWill — will messages at various QoS levels
    // server/mqtt_test.go:4142-4149
    [Theory]
    [InlineData(0, "bye-qos0")]
    [InlineData(1, "bye-qos1")]
    public async Task Will_message_at_various_qos_levels_reaches_subscriber(int qos, string payload)
    {
        await using var listener = new MqttListener("127.0.0.1", 0);
        listener.UseBinaryProtocol = false;
        using var cts = new CancellationTokenSource();
        await listener.StartAsync(cts.Token);

        using var sub = new TcpClient();
        await sub.ConnectAsync(IPAddress.Loopback, listener.Port);
        var subStream = sub.GetStream();
        await MqttWillWire.WriteLineAsync(subStream, "CONNECT sub-qos-will clean=true");
        (await MqttWillWire.ReadLineAsync(subStream, 1000)).ShouldBe("CONNACK");
        await MqttWillWire.WriteLineAsync(subStream, "SUB will.multi");
        (await MqttWillWire.ReadLineAsync(subStream, 1000))!.ShouldContain("SUBACK");

        using var pub = new TcpClient();
        await pub.ConnectAsync(IPAddress.Loopback, listener.Port);
        var pubStream = pub.GetStream();
        await MqttWillWire.WriteLineAsync(pubStream, "CONNECT pub-qos-will clean=true");
        (await MqttWillWire.ReadLineAsync(pubStream, 1000)).ShouldBe("CONNACK");

        if (qos == 0)
        {
            await MqttWillWire.WriteLineAsync(pubStream, $"PUB will.multi {payload}");
        }
        else
        {
            await MqttWillWire.WriteLineAsync(pubStream, $"PUBQ1 1 will.multi {payload}");
            (await MqttWillWire.ReadLineAsync(pubStream, 1000)).ShouldBe("PUBACK 1");
        }

        (await MqttWillWire.ReadLineAsync(subStream, 1000)).ShouldBe($"MSG will.multi {payload}");
    }

    // Go ref: TestMQTTParseConnect will-related fields server/mqtt_test.go:1683
    [Fact]
    public void Connect_packet_with_will_flag_parses_will_topic_from_payload()
    {
        ReadOnlySpan<byte> bytes =
        [
            0x10, 0x13,
            0x00, 0x04, (byte)'M', (byte)'Q', (byte)'T', (byte)'T',
            0x04, 0x06, 0x00, 0x3C,
            0x00, 0x01, (byte)'c',
            0x00, 0x01, (byte)'w',
            0x00, 0x01, (byte)'m',
        ];

        var packet = MqttPacketReader.Read(bytes);
        packet.Type.ShouldBe(MqttControlPacketType.Connect);
        var connectFlags = packet.Payload.Span[7];
        (connectFlags & 0x04).ShouldNotBe(0); // will flag bit
    }

    [Fact]
    public void Connect_packet_will_flag_and_retain_flag_in_connect_flags()
    {
        ReadOnlySpan<byte> bytes =
        [
            0x10, 0x13,
            0x00, 0x04, (byte)'M', (byte)'Q', (byte)'T', (byte)'T',
            0x04, 0x26, 0x00, 0x3C,
            0x00, 0x01, (byte)'c',
            0x00, 0x01, (byte)'w',
            0x00, 0x01, (byte)'m',
        ];

        var packet = MqttPacketReader.Read(bytes);
        var connectFlags = packet.Payload.Span[7];
        (connectFlags & 0x04).ShouldNotBe(0); // will flag
        (connectFlags & 0x20).ShouldNotBe(0); // will retain flag
    }

    [Fact]
    public void Connect_packet_will_qos_bits_parsed_from_flags()
    {
        ReadOnlySpan<byte> bytes =
        [
            0x10, 0x13,
            0x00, 0x04, (byte)'M', (byte)'Q', (byte)'T', (byte)'T',
            0x04, 0x0E, 0x00, 0x3C,
            0x00, 0x01, (byte)'c',
            0x00, 0x01, (byte)'w',
            0x00, 0x01, (byte)'m',
        ];

        var packet = MqttPacketReader.Read(bytes);
        var connectFlags = packet.Payload.Span[7];
        var willQos = (connectFlags >> 3) & 0x03;
        willQos.ShouldBe(1);
    }

    // Go ref: TestMQTTWillRetain — will retained at various QoS combinations
    // server/mqtt_test.go:4217
    [Theory]
    [InlineData(0, 0)]
    [InlineData(0, 1)]
    [InlineData(1, 0)]
    [InlineData(1, 1)]
    public async Task Will_message_delivered_at_various_pub_sub_qos_combinations(int pubQos, int subQos)
    {
        _ = pubQos;
        _ = subQos;

        await using var listener = new MqttListener("127.0.0.1", 0);
        listener.UseBinaryProtocol = false;
        using var cts = new CancellationTokenSource();
        await listener.StartAsync(cts.Token);

        using var sub = new TcpClient();
        await sub.ConnectAsync(IPAddress.Loopback, listener.Port);
        var subStream = sub.GetStream();
        await MqttWillWire.WriteLineAsync(subStream, "CONNECT sub-combo clean=true");
        (await MqttWillWire.ReadLineAsync(subStream, 1000)).ShouldBe("CONNACK");
        await MqttWillWire.WriteLineAsync(subStream, "SUB will.retain.topic");
        (await MqttWillWire.ReadLineAsync(subStream, 1000))!.ShouldContain("SUBACK");

        using var pub = new TcpClient();
        await pub.ConnectAsync(IPAddress.Loopback, listener.Port);
        var pubStream = pub.GetStream();
        await MqttWillWire.WriteLineAsync(pubStream, "CONNECT pub-combo clean=true");
        (await MqttWillWire.ReadLineAsync(pubStream, 1000)).ShouldBe("CONNACK");

        await MqttWillWire.WriteLineAsync(pubStream, "PUB will.retain.topic bye");
        (await MqttWillWire.ReadLineAsync(subStream, 1000)).ShouldBe("MSG will.retain.topic bye");
    }
}

internal static class MqttWillWire
{
    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]);
    }
}
