// Ports MQTT authentication behavior from Go reference:
// golang/nats-server/server/mqtt_test.go — TestMQTTBasicAuth, TestMQTTTokenAuth,
// TestMQTTAuthTimeout, TestMQTTUsersAuth, TestMQTTNoAuthUser,
// TestMQTTConnectNotFirstPacket, TestMQTTSecondConnect, TestMQTTParseConnect,
// TestMQTTConnKeepAlive

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

namespace NATS.Server.Mqtt.Tests.Mqtt;

public class MqttAuthParityTests
{
    // Go ref: TestMQTTBasicAuth — correct credentials accepted
    // server/mqtt_test.go:1159
    [Fact]
    public async Task Correct_mqtt_credentials_connect_accepted()
    {
        await using var listener = new MqttListener(
            "127.0.0.1", 0,
            requiredUsername: "mqtt",
            requiredPassword: "client");
        listener.UseBinaryProtocol = false;
        using var cts = new CancellationTokenSource();
        await listener.StartAsync(cts.Token);

        using var client = new TcpClient();
        await client.ConnectAsync(IPAddress.Loopback, listener.Port);
        var stream = client.GetStream();

        await MqttAuthWire.WriteLineAsync(stream, "CONNECT auth-ok clean=true user=mqtt pass=client");
        (await MqttAuthWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK");
    }

    // Go ref: TestMQTTBasicAuth — wrong credentials rejected
    [Fact]
    public async Task Wrong_mqtt_credentials_connect_rejected()
    {
        await using var listener = new MqttListener(
            "127.0.0.1", 0,
            requiredUsername: "mqtt",
            requiredPassword: "client");
        listener.UseBinaryProtocol = false;
        using var cts = new CancellationTokenSource();
        await listener.StartAsync(cts.Token);

        using var client = new TcpClient();
        await client.ConnectAsync(IPAddress.Loopback, listener.Port);
        var stream = client.GetStream();

        await MqttAuthWire.WriteLineAsync(stream, "CONNECT auth-fail clean=true user=wrong pass=client");
        var response = await MqttAuthWire.ReadLineAsync(stream, 1000);
        response.ShouldNotBeNull();
        response!.ShouldContain("ERR");
    }

    // Go ref: TestMQTTBasicAuth — wrong password rejected
    [Fact]
    public async Task Wrong_password_connect_rejected()
    {
        await using var listener = new MqttListener(
            "127.0.0.1", 0,
            requiredUsername: "mqtt",
            requiredPassword: "secret");
        listener.UseBinaryProtocol = false;
        using var cts = new CancellationTokenSource();
        await listener.StartAsync(cts.Token);

        using var client = new TcpClient();
        await client.ConnectAsync(IPAddress.Loopback, listener.Port);
        var stream = client.GetStream();

        await MqttAuthWire.WriteLineAsync(stream, "CONNECT auth-badpass clean=true user=mqtt pass=wrong");
        var response = await MqttAuthWire.ReadLineAsync(stream, 1000);
        response.ShouldNotBeNull();
        response!.ShouldContain("ERR");
    }

    // Go ref: TestMQTTBasicAuth — no auth configured, any credentials accepted
    [Fact]
    public async Task No_auth_configured_connects_without_credentials()
    {
        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 client = new TcpClient();
        await client.ConnectAsync(IPAddress.Loopback, listener.Port);
        var stream = client.GetStream();

        await MqttAuthWire.WriteLineAsync(stream, "CONNECT no-auth-client clean=true");
        (await MqttAuthWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK");
    }

    [Fact]
    public async Task No_auth_configured_accepts_any_credentials()
    {
        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 client = new TcpClient();
        await client.ConnectAsync(IPAddress.Loopback, listener.Port);
        var stream = client.GetStream();

        await MqttAuthWire.WriteLineAsync(stream, "CONNECT any-creds clean=true user=whatever pass=doesntmatter");
        (await MqttAuthWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK");
    }

    // =========================================================================
    // Go: TestMQTTTokenAuth — ValidateMqttCredentials tests
    // server/mqtt_test.go:1307
    // =========================================================================

    [Fact]
    public void ValidateMqttCredentials_returns_true_when_no_auth_configured()
    {
        AuthService.ValidateMqttCredentials(null, null, null, null).ShouldBeTrue();
        AuthService.ValidateMqttCredentials(null, null, "anything", "anything").ShouldBeTrue();
        AuthService.ValidateMqttCredentials(string.Empty, string.Empty, null, null).ShouldBeTrue();
    }

    [Fact]
    public void ValidateMqttCredentials_returns_true_for_matching_credentials()
    {
        AuthService.ValidateMqttCredentials("mqtt", "client", "mqtt", "client").ShouldBeTrue();
    }

    [Fact]
    public void ValidateMqttCredentials_returns_false_for_wrong_username()
    {
        AuthService.ValidateMqttCredentials("mqtt", "client", "wrong", "client").ShouldBeFalse();
    }

    [Fact]
    public void ValidateMqttCredentials_returns_false_for_wrong_password()
    {
        AuthService.ValidateMqttCredentials("mqtt", "client", "mqtt", "wrong").ShouldBeFalse();
    }

    [Fact]
    public void ValidateMqttCredentials_returns_false_for_null_credentials_when_auth_configured()
    {
        AuthService.ValidateMqttCredentials("mqtt", "client", null, null).ShouldBeFalse();
    }

    [Fact]
    public void ValidateMqttCredentials_case_sensitive_comparison()
    {
        AuthService.ValidateMqttCredentials("MQTT", "Client", "mqtt", "client").ShouldBeFalse();
        AuthService.ValidateMqttCredentials("MQTT", "Client", "MQTT", "Client").ShouldBeTrue();
    }

    // =========================================================================
    // Go: TestMQTTUsersAuth — multiple users
    // server/mqtt_test.go:1466
    // =========================================================================

    [Fact]
    public async Task Multiple_clients_with_different_credentials_authenticate_independently()
    {
        await using var listener = new MqttListener(
            "127.0.0.1", 0,
            requiredUsername: "admin",
            requiredPassword: "password");
        listener.UseBinaryProtocol = false;
        using var cts = new CancellationTokenSource();
        await listener.StartAsync(cts.Token);

        using var client1 = new TcpClient();
        await client1.ConnectAsync(IPAddress.Loopback, listener.Port);
        var s1 = client1.GetStream();
        await MqttAuthWire.WriteLineAsync(s1, "CONNECT user1 clean=true user=admin pass=password");
        (await MqttAuthWire.ReadLineAsync(s1, 1000)).ShouldBe("CONNACK");

        using var client2 = new TcpClient();
        await client2.ConnectAsync(IPAddress.Loopback, listener.Port);
        var s2 = client2.GetStream();
        await MqttAuthWire.WriteLineAsync(s2, "CONNECT user2 clean=true user=admin pass=wrong");
        var response = await MqttAuthWire.ReadLineAsync(s2, 1000);
        response.ShouldNotBeNull();
        response!.ShouldContain("ERR");

        await MqttAuthWire.WriteLineAsync(s1, "PUBQ1 1 auth.test ok");
        (await MqttAuthWire.ReadLineAsync(s1, 1000)).ShouldBe("PUBACK 1");
    }

    // =========================================================================
    // Go: TestMQTTConnKeepAlive server/mqtt_test.go:1741
    // =========================================================================

    [Fact]
    public async Task Keepalive_timeout_disconnects_idle_client()
    {
        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 client = new TcpClient();
        await client.ConnectAsync(IPAddress.Loopback, listener.Port);
        var stream = client.GetStream();

        await MqttAuthWire.WriteLineAsync(stream, "CONNECT keepalive-client clean=true keepalive=1");
        (await MqttAuthWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK");

        // Poll until the server closes the connection due to keepalive expiry (keepalive=1s)
        var disconnected = await PollHelper.WaitUntilAsync(async () =>
        {
            var result = await MqttAuthWire.ReadRawAsync(stream, 200);
            return result == null || result == "__timeout__";
        }, timeoutMs: 5000, intervalMs: 100);
        disconnected.ShouldBeTrue("Server should disconnect idle client after keepalive timeout");
    }

    // =========================================================================
    // Go: TestMQTTParseConnect — username/password flags
    // server/mqtt_test.go:1661
    // =========================================================================

    [Fact]
    public void Connect_packet_with_username_flag_has_username_in_payload()
    {
        ReadOnlySpan<byte> bytes =
        [
            0x10, 0x10,
            0x00, 0x04, (byte)'M', (byte)'Q', (byte)'T', (byte)'T',
            0x04, 0x82, 0x00, 0x3C,
            0x00, 0x01, (byte)'c',
            0x00, 0x01, (byte)'u',
        ];

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

    [Fact]
    public void Connect_packet_with_username_and_password_flags()
    {
        ReadOnlySpan<byte> bytes =
        [
            0x10, 0x13,
            0x00, 0x04, (byte)'M', (byte)'Q', (byte)'T', (byte)'T',
            0x04, 0xC2, 0x00, 0x3C,
            0x00, 0x01, (byte)'c',
            0x00, 0x01, (byte)'u',
            0x00, 0x01, (byte)'p',
        ];

        var packet = MqttPacketReader.Read(bytes);
        var connectFlags = packet.Payload.Span[7];
        (connectFlags & 0x80).ShouldNotBe(0); // username flag
        (connectFlags & 0x40).ShouldNotBe(0); // password flag
    }

    // Go: TestMQTTParseConnect — "no user but password" server/mqtt_test.go:1678
    [Fact]
    public void Connect_flags_password_without_user_is_protocol_violation()
    {
        byte connectFlags = 0x40;
        (connectFlags & 0x80).ShouldBe(0);
        (connectFlags & 0x40).ShouldNotBe(0);
    }

    // Go: TestMQTTParseConnect — "reserved flag" server/mqtt_test.go:1674
    [Fact]
    public void Connect_flags_reserved_bit_must_be_zero()
    {
        byte connectFlags = 0x01;
        (connectFlags & 0x01).ShouldNotBe(0);
    }

    // =========================================================================
    // Go: TestMQTTConnectNotFirstPacket server/mqtt_test.go:1618
    // =========================================================================

    [Fact]
    public async Task Non_connect_as_first_packet_is_handled()
    {
        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 client = new TcpClient();
        await client.ConnectAsync(IPAddress.Loopback, listener.Port);
        var stream = client.GetStream();

        await MqttAuthWire.WriteLineAsync(stream, "PUB some.topic hello");

        var response = await MqttAuthWire.ReadLineAsync(stream, 1000);
        if (response != null)
        {
            response.ShouldNotBe("CONNACK");
        }
    }

    // Go: TestMQTTSecondConnect server/mqtt_test.go:1645
    [Fact]
    public async Task Second_connect_from_same_tcp_connection_is_handled()
    {
        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 client = new TcpClient();
        await client.ConnectAsync(IPAddress.Loopback, listener.Port);
        var stream = client.GetStream();

        await MqttAuthWire.WriteLineAsync(stream, "CONNECT second-conn clean=true");
        (await MqttAuthWire.ReadLineAsync(stream, 1000)).ShouldBe("CONNACK");

        await MqttAuthWire.WriteLineAsync(stream, "CONNECT second-conn clean=true");
        var response = await MqttAuthWire.ReadLineAsync(stream, 1000);
        _ = response; // Just verify no crash
    }
}

internal static class MqttAuthWire
{
    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__";
        }
    }
}
