// Copyright 2013-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Net;
using System.Text;
using Shouldly;
using ZB.MOM.NatsNet.Server;

namespace ZB.MOM.NatsNet.Server.IntegrationTests;

/// <summary>
/// End-to-end server boot tests that validate the full
/// Start() → AcceptLoop → client connection lifecycle.
/// </summary>
[Trait("Category", "Integration")]
public sealed class ServerBootTests : IDisposable
{
    private readonly string _storeDir;

    public ServerBootTests()
    {
        _storeDir = Path.Combine(Path.GetTempPath(), $"natsnet-test-{Guid.NewGuid():N}");
    }

    public void Dispose()
    {
        try { Directory.Delete(_storeDir, true); } catch { }
    }

    /// <summary>
    /// Validates that a server can boot, accept a TCP connection, and send
    /// a NATS protocol INFO line. This proves the full Start() → AcceptLoop
    /// → CreateClient pipeline works end-to-end.
    /// </summary>
    /// <summary>
    /// Validates that a server can boot, bind a port, and accept a TCP connection.
    /// Note: GenerateClientInfoJSON is currently a stub (returns empty), so we only
    /// verify the TCP handshake succeeds — not the INFO protocol line.
    /// </summary>
    [Fact]
    public async Task ServerBoot_AcceptsTcpConnection_ShouldSucceed()
    {
        // Arrange — create server with ephemeral port
        var opts = new ServerOptions
        {
            Host = "127.0.0.1",
            Port = 0, // ephemeral
        };

        var (server, err) = NatsServer.NewServer(opts);
        err.ShouldBeNull("NewServer should succeed");
        server.ShouldNotBeNull();

        try
        {
            // Act — start the server
            server!.Start();

            // Get the actual bound port
            var addr = server.Addr() as IPEndPoint;
            addr.ShouldNotBeNull("Server should have a listener address after Start()");
            addr!.Port.ShouldBeGreaterThan(0);

            // Connect a raw TCP client — proves AcceptLoop is working
            using var tcp = new System.Net.Sockets.TcpClient();
            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
            await tcp.ConnectAsync(addr.Address, addr.Port, cts.Token);

            tcp.Connected.ShouldBeTrue();

            // Verify the server registered the client
            await Task.Delay(100); // Give CreateClient a moment to run
            server.NumClients().ShouldBeGreaterThan(0);
        }
        finally
        {
            server!.Shutdown();
        }
    }

    /// <summary>
    /// Validates that a server can boot with an MQTT listener on an ephemeral port,
    /// accept a TCP connection on the MQTT port, and register it as a client.
    /// </summary>
    [Fact]
    public async Task MqttBoot_AcceptsConnection_ShouldSucceed()
    {
        var opts = new ServerOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Mqtt = { Port = -1, Host = "127.0.0.1" },
        };

        var (server, err) = NatsServer.NewServer(opts);
        err.ShouldBeNull("NewServer should succeed");
        server.ShouldNotBeNull();

        try
        {
            server!.Start();

            // Verify MQTT listener is up
            var mqttAddr = server.MqttAddr();
            mqttAddr.ShouldNotBeNull("MqttAddr should return the MQTT listener address");
            mqttAddr!.Port.ShouldBeGreaterThan(0);

            // ReadyForConnections should include MQTT
            server.ReadyForConnections(TimeSpan.FromSeconds(5)).ShouldBeTrue();

            // Connect a raw TCP client to the MQTT port
            using var tcp = new System.Net.Sockets.TcpClient();
            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
            await tcp.ConnectAsync(mqttAddr.Address, mqttAddr.Port, cts.Token);
            tcp.Connected.ShouldBeTrue();

            // Give CreateMqttClient a moment to register
            await Task.Delay(100);
            server.NumClients().ShouldBeGreaterThan(0);
        }
        finally
        {
            server!.Shutdown();
        }
    }

    /// <summary>
    /// Validates that an MQTT listener starts and shuts down cleanly.
    /// </summary>
    [Fact]
    public void MqttBoot_StartAndShutdown_ShouldSucceed()
    {
        var opts = new ServerOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            DontListen = true,
            Mqtt = { Port = -1, Host = "127.0.0.1" },
        };

        var (server, err) = NatsServer.NewServer(opts);
        err.ShouldBeNull();
        server.ShouldNotBeNull();

        server!.Start();
        server.Running().ShouldBeTrue();
        server.MqttAddr().ShouldNotBeNull();

        server.Shutdown();
        server.Running().ShouldBeFalse();
    }

    /// <summary>
    /// End-to-end: TCP connect → send MQTT CONNECT → receive CONNACK.
    /// Validates the full MQTT handshake over the wire.
    /// </summary>
    [Fact]
    public async Task MqttBoot_ConnectHandshake_ShouldReceiveConnAck()
    {
        var opts = new ServerOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Mqtt = { Port = -1, Host = "127.0.0.1" },
        };

        var (server, err) = NatsServer.NewServer(opts);
        err.ShouldBeNull();
        server.ShouldNotBeNull();

        try
        {
            server!.Start();
            var mqttAddr = server.MqttAddr();
            mqttAddr.ShouldNotBeNull();

            using var tcp = new System.Net.Sockets.TcpClient();
            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
            await tcp.ConnectAsync(mqttAddr!.Address, mqttAddr.Port, cts.Token);

            var stream = tcp.GetStream();

            // Build and send MQTT CONNECT packet.
            var connectPacket = BuildMqttConnectPacket("integration-test");
            await stream.WriteAsync(connectPacket, cts.Token);
            await stream.FlushAsync(cts.Token);

            // Read CONNACK response (4 bytes).
            var response = new byte[4];
            var totalRead = 0;
            while (totalRead < 4)
            {
                var n = await stream.ReadAsync(response.AsMemory(totalRead, 4 - totalRead), cts.Token);
                if (n == 0) break;
                totalRead += n;
            }

            totalRead.ShouldBe(4, "Should receive 4-byte CONNACK");
            response[0].ShouldBe((byte)0x20, "Packet type should be CONNACK");
            response[1].ShouldBe((byte)0x02, "Remaining length should be 2");
            response[3].ShouldBe((byte)0x00, "Return code should be Accepted (0)");
        }
        finally
        {
            server!.Shutdown();
        }
    }

    /// <summary>Builds a minimal MQTT CONNECT packet.</summary>
    private static byte[] BuildMqttConnectPacket(string clientId)
    {
        var payload = new List<byte>();
        // Protocol name "MQTT"
        payload.AddRange(new byte[] { 0x00, 0x04 });
        payload.AddRange(System.Text.Encoding.UTF8.GetBytes("MQTT"));
        // Protocol level 4
        payload.Add(0x04);
        // Flags: clean session
        payload.Add(0x02);
        // Keep alive: 60s
        payload.AddRange(new byte[] { 0x00, 0x3C });
        // Client ID
        var cidBytes = System.Text.Encoding.UTF8.GetBytes(clientId);
        payload.Add((byte)(cidBytes.Length >> 8));
        payload.Add((byte)(cidBytes.Length & 0xFF));
        payload.AddRange(cidBytes);

        // Fixed header
        var result = new List<byte>();
        result.Add(0x10); // CONNECT type
        var remLen = payload.Count;
        do
        {
            var b = (byte)(remLen & 0x7F);
            remLen >>= 7;
            if (remLen > 0) b |= 0x80;
            result.Add(b);
        } while (remLen > 0);
        result.AddRange(payload);
        return result.ToArray();
    }

    /// <summary>
    /// End-to-end: CONNECT → SUBSCRIBE → verify SUBACK over the wire.
    /// </summary>
    [Fact]
    public async Task MqttBoot_SubscribeHandshake_ShouldReceiveSubAck()
    {
        var opts = new ServerOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            Mqtt = { Port = -1, Host = "127.0.0.1" },
        };

        var (server, err) = NatsServer.NewServer(opts);
        err.ShouldBeNull();
        server.ShouldNotBeNull();

        try
        {
            server!.Start();
            var mqttAddr = server.MqttAddr();
            mqttAddr.ShouldNotBeNull();

            using var tcp = new System.Net.Sockets.TcpClient();
            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
            await tcp.ConnectAsync(mqttAddr!.Address, mqttAddr.Port, cts.Token);

            var stream = tcp.GetStream();

            // 1. CONNECT → CONNACK
            var connectPacket = BuildMqttConnectPacket("sub-test");
            await stream.WriteAsync(connectPacket, cts.Token);
            await stream.FlushAsync(cts.Token);

            var connack = new byte[4];
            await ReadExactAsync(stream, connack, cts.Token);
            connack[0].ShouldBe((byte)0x20); // CONNACK
            connack[3].ShouldBe((byte)0x00); // Accepted

            // 2. SUBSCRIBE to "test/sub" QoS 0 → SUBACK
            var subscribePacket = BuildMqttSubscribePacket(packetId: 1, topic: "test/sub", qos: 0);
            await stream.WriteAsync(subscribePacket, cts.Token);
            await stream.FlushAsync(cts.Token);

            // SUBACK: [0x90] [0x03] [PI high] [PI low] [granted QoS]
            var suback = new byte[5];
            await ReadExactAsync(stream, suback, cts.Token);
            suback[0].ShouldBe((byte)0x90); // SUBACK
            suback[1].ShouldBe((byte)0x03); // remaining length = 3
            suback[2].ShouldBe((byte)0x00); // PI high
            suback[3].ShouldBe((byte)0x01); // PI low = 1
            suback[4].ShouldBe((byte)0x00); // granted QoS 0
        }
        finally
        {
            server!.Shutdown();
        }
    }

    /// <summary>Builds a minimal MQTT SUBSCRIBE packet.</summary>
    private static byte[] BuildMqttSubscribePacket(ushort packetId, string topic, byte qos)
    {
        var topicBytes = System.Text.Encoding.UTF8.GetBytes(topic);
        var payload = new List<byte>();
        payload.Add((byte)(packetId >> 8));
        payload.Add((byte)(packetId & 0xFF));
        payload.Add((byte)(topicBytes.Length >> 8));
        payload.Add((byte)(topicBytes.Length & 0xFF));
        payload.AddRange(topicBytes);
        payload.Add(qos);

        var result = new List<byte>();
        result.Add(0x82); // SUBSCRIBE + flags 0x02
        var remLen = payload.Count;
        do
        {
            var b = (byte)(remLen & 0x7F);
            remLen >>= 7;
            if (remLen > 0) b |= 0x80;
            result.Add(b);
        } while (remLen > 0);
        result.AddRange(payload);
        return result.ToArray();
    }

    /// <summary>Reads exactly <paramref name="buffer"/>.Length bytes from the stream.</summary>
    private static async Task ReadExactAsync(System.Net.Sockets.NetworkStream stream, byte[] buffer, CancellationToken ct)
    {
        var totalRead = 0;
        while (totalRead < buffer.Length)
        {
            var n = await stream.ReadAsync(buffer.AsMemory(totalRead, buffer.Length - totalRead), ct);
            if (n == 0) break;
            totalRead += n;
        }
        totalRead.ShouldBe(buffer.Length);
    }

    /// <summary>
    /// Validates that Shutdown() after Start() completes cleanly.
    /// Uses DontListen to skip TCP binding — tests lifecycle only.
    /// </summary>
    [Fact]
    public void ServerBoot_StartAndShutdown_ShouldSucceed()
    {
        var opts = new ServerOptions
        {
            Host = "127.0.0.1",
            Port = 0,
            DontListen = true,
        };

        var (server, err) = NatsServer.NewServer(opts);
        err.ShouldBeNull();
        server.ShouldNotBeNull();

        server!.Start();
        server.Running().ShouldBeTrue();

        server.Shutdown();
        server.Running().ShouldBeFalse();
    }
}
