using StandardScene.Magnetic.Protocol;
using StandardScene.Magnetic.Tests.TestHelpers;
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Xunit;

namespace StandardScene.Magnetic.Tests.Protocol
{
    public sealed class Fass2UdpHubMultiCarTests : IDisposable
    {
        private readonly int _hubPort;
        private readonly TestUdpCar _car1;
        private readonly TestUdpCar _car2;

        public Fass2UdpHubMultiCarTests()
        {
            _hubPort = AllocateUdpPort();
            _car1 = new TestUdpCar(1, AllocateUdpPort());
            _car2 = new TestUdpCar(2, AllocateUdpPort());

            Fass2UdpHub.Register(_car1);
            Fass2UdpHub.Register(_car2);
            Fass2UdpHub.EnsureStarted(_hubPort);
            Thread.Sleep(100);
        }

        [Fact]
        public void Hub_RoutesStateToRegisteredCar_AndRepliesWith0x10()
        {
            SendState(_car1.VehicleCode, 2, 10, 100);
            var ack1 = _car1.WaitForAck();
            Assert.NotNull(ack1);
            Assert.Equal(Fass2Protocol.ControlFrameLength, ack1.Length);
            Assert.Equal(Fass2Protocol.CmdStateResponse, ack1[1]);
            Assert.Equal(0x01, ack1[2]);
            Assert.Single(_car1.Reports);
            Assert.Equal((ushort)10, _car1.Reports[0].Node.Node);
            Assert.Empty(_car2.Reports);
        }

        [Fact]
        public void Hub_DispatchesMultipleCarsIndependently()
        {
            SendState(1, 1, 11, 201);
            SendState(2, 2, 22, 202);

            Assert.NotNull(_car1.WaitForAck());
            Assert.NotNull(_car2.WaitForAck());

            Assert.Single(_car1.Reports);
            Assert.Single(_car2.Reports);
            Assert.Equal((ushort)11, _car1.Reports[0].Node.Node);
            Assert.Equal((ushort)22, _car2.Reports[0].Node.Node);
            Assert.Equal((ulong)201, _car1.Reports[0].Task);
            Assert.Equal((ulong)202, _car2.Reports[0].Task);
        }

        [Fact]
        public void Hub_IgnoresUnregisteredCarCode()
        {
            SendState(99, 2, 30, 0);
            Thread.Sleep(300);
            Assert.Empty(_car1.Reports);
            Assert.Empty(_car2.Reports);
        }

        public void Dispose()
        {
            Fass2UdpHub.Unregister(_car1);
            Fass2UdpHub.Unregister(_car2);
            Fass2UdpHub.Stop();
            _car1.Dispose();
            _car2.Dispose();
        }

        private void SendState(ushort carCode, byte state, ushort nodeId, ulong taskId)
        {
            var frame = Fass2TestFrameBuilder.BuildStateFrame(carCode, state, nodeId, taskId);
            using var sender = new UdpClient();
            sender.Send(frame, frame.Length, new IPEndPoint(IPAddress.Loopback, _hubPort));
            Thread.Sleep(80);
        }

        private static int AllocateUdpPort()
        {
            var listener = new TcpListener(IPAddress.Loopback, 0);
            listener.Start();
            var port = ((IPEndPoint)listener.LocalEndpoint).Port;
            listener.Stop();
            return port;
        }
    }
}
