using StandardScene.Magnetic.Protocol;
using StandardScene.Magnetic.Tasking;
using StandardScene.Magnetic.Tests.TestHelpers;
using System;
using Xunit;

namespace StandardScene.Magnetic.Tests.Protocol
{
    public sealed class Fass2ProtocolGoldenTests
    {
        [Fact]
        public void BuildControl_Start_Car1_Param0_MatchesGoldenLayout()
        {
            var frame = Fass2Protocol.BuildControl(Fass2Protocol.CmdStart, 1, 0);

            Assert.Equal(Fass2Protocol.ControlFrameLength, frame.Length);
            Assert.Equal(Fass2Protocol.Begin, frame[0]);
            Assert.Equal(Fass2Protocol.CmdStart, frame[1]);
            Assert.Equal(0x01, frame[2]);
            Assert.Equal(0x00, frame[3]);
            Assert.Equal(0x00, frame[4]);
            Assert.Equal(0x00, frame[5]);
            Assert.Equal(Fass2Protocol.Xor(frame, 1, 47), frame[48]);
            Assert.Equal(Fass2Protocol.End, frame[49]);

            const string goldenPrefix = "BB 01 01 00 00 00";
            Assert.StartsWith(goldenPrefix, Fass2TestFrameBuilder.ToHex(frame));
        }

        [Fact]
        public void BuildNodes_TwoNodes_LittleEndianAndXor()
        {
            var nodes = new[]
            {
                new Fass2NodeMessage
                {
                    Node = 1,
                    Distance = 500,
                    StartStop = 1,
                    Speed = 120
                },
                new Fass2NodeMessage
                {
                    Node = 2,
                    StartStop = 2,
                    Lift = 1
                }
            };

            var frame = Fass2Protocol.BuildNodes(1, 0x0102030405060708UL, nodes);

            Assert.Equal(Fass2Protocol.NodesFrameLength, frame.Length);
            Assert.Equal(Fass2Protocol.CmdNodes, frame[1]);
            Assert.Equal(0x08, frame[4]);
            Assert.Equal(0x01, frame[11]);
            Assert.Equal(0x02, frame[12]);
            Assert.Equal(0x00, frame[13]);
            Assert.Equal(Fass2Protocol.Xor(frame, 1, 297), frame[298]);
            Assert.Equal(Fass2Protocol.End, frame[299]);

            var parsedNode0 = Fass2NodeMessage.FromBytes(CopyRange(frame, 14, 25));
            Assert.Equal((ushort)1, parsedNode0.Node);
            Assert.Equal((ushort)500, parsedNode0.Distance);
            Assert.Equal((byte)1, parsedNode0.StartStop);
            Assert.Equal((ushort)120, parsedNode0.Speed);

            var parsedNode1 = Fass2NodeMessage.FromBytes(CopyRange(frame, 39, 25));
            Assert.Equal((ushort)2, parsedNode1.Node);
            Assert.Equal((byte)2, parsedNode1.StartStop);
            Assert.Equal((byte)1, parsedNode1.Lift);
        }

        [Fact]
        public void BuildAction_RoundTripsNodeBlock()
        {
            var node = new Fass2NodeMessage
            {
                Node = 3,
                StartStop = 22,
                Lift = 2,
                Speed = 120
            };

            var frame = Fass2Protocol.BuildAction(2, 999, node);

            Assert.Equal(Fass2Protocol.ActionFrameLength, frame.Length);
            Assert.Equal(Fass2Protocol.CmdAction, frame[1]);
            Assert.Equal(0x02, frame[2]);
            Assert.Equal(0x00, frame[3]);
            Assert.Equal(Fass2Protocol.Xor(frame, 1, 97), frame[98]);

            var parsed = Fass2NodeMessage.FromBytes(CopyRange(frame, 14, 25));
            Assert.Equal(node.Node, parsed.Node);
            Assert.Equal(node.StartStop, parsed.StartStop);
            Assert.Equal(node.Lift, parsed.Lift);
            Assert.Equal(node.Speed, parsed.Speed);
        }

        [Fact]
        public void BuildStateResponse_FixedTimestamp_EncodesBeijingCalendar()
        {
            var frame = Fass2Protocol.BuildStateResponse(1, 1_704_067_200_000UL);

            Assert.Equal(Fass2Protocol.CmdStateResponse, frame[1]);
            Assert.Equal(0x01, frame[2]);
            Assert.Equal(0x00, frame[3]);
            Assert.Equal(Fass2Protocol.Xor(frame, 1, 47), frame[48]);
            Assert.Equal(Fass2Protocol.End, frame[49]);
        }

        [Fact]
        public void ParseState_RoundTripsMinimalFrame()
        {
            var frame = Fass2TestFrameBuilder.BuildStateFrame(5, 2, 7, 42);

            Assert.True(Fass2Protocol.TryExtractStateFrame(frame, frame.Length, out var extracted));
            Assert.Equal(frame, extracted);

            var report = Fass2Protocol.ParseState(frame);
            Assert.Equal((ushort)5, report.Car);
            Assert.Equal((byte)2, report.State);
            Assert.Equal((ulong)42, report.Task);
            Assert.Equal((ushort)7, report.Node.Node);
            Assert.Equal((ushort)500, report.Node.Distance);
        }

        [Fact]
        public void NodeMessage_ToBytes_FromBytes_RoundTrip()
        {
            var source = new Fass2NodeMessage
            {
                Node = 11,
                Distance = 1234,
                StartStop = 2,
                Direction = 3,
                Orientation = 64,
                Byroad = 2,
                Speed = 120,
                Lift = 1,
                Roll = 2,
                Shutdown = 1
            };

            var roundTrip = Fass2NodeMessage.FromBytes(source.ToBytes());
            Assert.Equal(source.Node, roundTrip.Node);
            Assert.Equal(source.Distance, roundTrip.Distance);
            Assert.Equal(source.StartStop, roundTrip.StartStop);
            Assert.Equal(source.Direction, roundTrip.Direction);
            Assert.Equal(source.Orientation, roundTrip.Orientation);
            Assert.Equal(source.Byroad, roundTrip.Byroad);
            Assert.Equal(source.Speed, roundTrip.Speed);
            Assert.Equal(source.Lift, roundTrip.Lift);
            Assert.Equal(source.Roll, roundTrip.Roll);
            Assert.Equal(source.Shutdown, roundTrip.Shutdown);
        }

        [Fact]
        public void SpeedToProtocol_UsesMPerMinTimesTen()
        {
            Assert.Equal((ushort)120, Fass2TaskBuilder.SpeedToProtocol(12));
            Assert.Equal((ushort)120, Fass2TaskBuilder.SpeedToProtocol(0.2));
        }

        private static byte[] CopyRange(byte[] source, int offset, int length)
        {
            var copy = new byte[length];
            Buffer.BlockCopy(source, offset, copy, 0, length);
            return copy;
        }
    }
}
