// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System;
using System.Collections.ObjectModel;
using System.Linq;
using Microsoft.CodeAnalysis.ExpressionEvaluator;
using Microsoft.VisualStudio.Debugger.Evaluation.ClrCompilation;
using Roslyn.Test.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.CSharp.ExpressionEvaluator.UnitTests
{
    public class DynamicFlagsCustomTypeInfoTests : CSharpResultProviderTestBase
    {
        [Fact]
        public void ToBytes()
        {
            ValidateToBytes(new bool[0]);

            ValidateToBytes(new bool[] { false });
            ValidateToBytes(new bool[] { true }, 0x01);
            ValidateToBytes(new bool[] { false, false });
            ValidateToBytes(new bool[] { true, false }, 0x01);
            ValidateToBytes(new bool[] { false, true }, 0x02);
            ValidateToBytes(new bool[] { true, true }, 0x03);

            ValidateToBytes(new bool[] { false, false, true }, 0x04);
            ValidateToBytes(new bool[] { false, false, false, true }, 0x08);
            ValidateToBytes(new bool[] { false, false, false, false, true }, 0x10);
            ValidateToBytes(new bool[] { false, false, false, false, false, true }, 0x20);
            ValidateToBytes(new bool[] { false, false, false, false, false, false, true }, 0x40);
            ValidateToBytes(new bool[] { false, false, false, false, false, false, false, true }, 0x80);
            ValidateToBytes(new bool[] { false, false, false, false, false, false, false, false, true }, 0x00, 0x01);
        }

        [Fact]
        public void CopyTo()
        {
            ValidateCopyTo(new byte[0]);

            ValidateCopyTo(new byte[] { 0x00 }, false, false, false, false, false, false, false, false);
            ValidateCopyTo(new byte[] { 0x01 }, true, false, false, false, false, false, false, false);
            ValidateCopyTo(new byte[] { 0x02 }, false, true, false, false, false, false, false, false);
            ValidateCopyTo(new byte[] { 0x03 }, true, true, false, false, false, false, false, false);

            ValidateCopyTo(new byte[] { 0x04 }, false, false, true, false, false, false, false, false);
            ValidateCopyTo(new byte[] { 0x08 }, false, false, false, true, false, false, false, false);
            ValidateCopyTo(new byte[] { 0x10 }, false, false, false, false, true, false, false, false);
            ValidateCopyTo(new byte[] { 0x20 }, false, false, false, false, false, true, false, false);
            ValidateCopyTo(new byte[] { 0x40 }, false, false, false, false, false, false, true, false);
            ValidateCopyTo(new byte[] { 0x80 }, false, false, false, false, false, false, false, true);
            ValidateCopyTo(new byte[] { 0x00, 0x01 }, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false);
        }

        [Fact]
        public void EncodeAndDecode()
        {
            var encoded = CustomTypeInfo.Encode(null, null);
            Assert.Null(encoded);

            ReadOnlyCollection<byte> bytes;
            ReadOnlyCollection<string> names;

            // Exceed max bytes.
            bytes = GetBytesInRange(0, 256);
            encoded = CustomTypeInfo.Encode(bytes, null);
            Assert.Null(encoded);

            // Max bytes.
            bytes = GetBytesInRange(0, 255);
            encoded = CustomTypeInfo.Encode(bytes, null);
            Assert.Equal(256, encoded.Count);
            Assert.Equal(255, encoded[0]);
            ReadOnlyCollection<byte> dynamicFlags;
            ReadOnlyCollection<string> tupleElementNames;
            CustomTypeInfo.Decode(CustomTypeInfo.PayloadTypeId, encoded, out dynamicFlags, out tupleElementNames);
            Assert.Equal(bytes, dynamicFlags);
            Assert.Null(tupleElementNames);

            // Empty dynamic flags collection
            bytes = new ReadOnlyCollection<byte>(new byte[0]);
            // ... with names.
            names = new ReadOnlyCollection<string>(new[] { "A" });
            encoded = CustomTypeInfo.Encode(bytes, names);
            CustomTypeInfo.Decode(CustomTypeInfo.PayloadTypeId, encoded, out dynamicFlags, out tupleElementNames);
            Assert.Null(dynamicFlags);
            Assert.Equal(names, tupleElementNames);
            // ... without names.
            encoded = CustomTypeInfo.Encode(bytes, null);
            CustomTypeInfo.Decode(CustomTypeInfo.PayloadTypeId, encoded, out dynamicFlags, out tupleElementNames);
            Assert.Null(dynamicFlags);
            Assert.Null(tupleElementNames);

            // Empty names collection
            names = new ReadOnlyCollection<string>(new string[0]);
            // ... with dynamic flags.
            bytes = GetBytesInRange(0, 255);
            encoded = CustomTypeInfo.Encode(bytes, names);
            CustomTypeInfo.Decode(CustomTypeInfo.PayloadTypeId, encoded, out dynamicFlags, out tupleElementNames);
            Assert.Equal(bytes, dynamicFlags);
            Assert.Null(tupleElementNames);
            // ... without dynamic flags.
            encoded = CustomTypeInfo.Encode(null, names);
            CustomTypeInfo.Decode(CustomTypeInfo.PayloadTypeId, encoded, out dynamicFlags, out tupleElementNames);
            Assert.Null(dynamicFlags);
            Assert.Null(tupleElementNames);

            // Single null name
            names = new ReadOnlyCollection<string>(new string[] { null });
            // ... with dynamic flags.
            bytes = GetBytesInRange(0, 255);
            encoded = CustomTypeInfo.Encode(bytes, names);
            Assert.Equal(255, encoded[0]);
            CustomTypeInfo.Decode(CustomTypeInfo.PayloadTypeId, encoded, out dynamicFlags, out tupleElementNames);
            Assert.Equal(bytes, dynamicFlags);
            Assert.Equal(names, tupleElementNames);
            // ... without dynamic flags.
            encoded = CustomTypeInfo.Encode(null, names);
            CustomTypeInfo.Decode(CustomTypeInfo.PayloadTypeId, encoded, out dynamicFlags, out tupleElementNames);
            Assert.Null(dynamicFlags);
            Assert.Equal(names, tupleElementNames);

            // Multiple names
            names = new ReadOnlyCollection<string>(new[] { null, "A", null, "B" });
            // ... with dynamic flags.
            bytes = GetBytesInRange(0, 255);
            encoded = CustomTypeInfo.Encode(bytes, names);
            Assert.Equal(255, encoded[0]);
            CustomTypeInfo.Decode(CustomTypeInfo.PayloadTypeId, encoded, out dynamicFlags, out tupleElementNames);
            Assert.Equal(bytes, dynamicFlags);
            Assert.Equal(names, tupleElementNames);
            // ... without dynamic flags.
            encoded = CustomTypeInfo.Encode(null, names);
            CustomTypeInfo.Decode(CustomTypeInfo.PayloadTypeId, encoded, out dynamicFlags, out tupleElementNames);
            Assert.Null(dynamicFlags);
            Assert.Equal(names, tupleElementNames);
        }

        private static ReadOnlyCollection<byte> GetBytesInRange(int start, int length)
        {
            return new ReadOnlyCollection<byte>(Enumerable.Range(start, length).Select(i => (byte)(i % 256)).ToArray());
        }

        [Fact]
        public void CustomTypeInfoConstructor()
        {
            ValidateCustomTypeInfo();

            ValidateCustomTypeInfo(0x00);
            ValidateCustomTypeInfo(0x01);
            ValidateCustomTypeInfo(0x02);
            ValidateCustomTypeInfo(0x03);

            ValidateCustomTypeInfo(0x04);
            ValidateCustomTypeInfo(0x08);
            ValidateCustomTypeInfo(0x10);
            ValidateCustomTypeInfo(0x20);
            ValidateCustomTypeInfo(0x40);
            ValidateCustomTypeInfo(0x80);
            ValidateCustomTypeInfo(0x00, 0x01);
        }

        [Fact]
        public void CustomTypeInfoConstructor_OtherGuid()
        {
            var customTypeInfo = DkmClrCustomTypeInfo.Create(Guid.NewGuid(), new ReadOnlyCollection<byte>(new byte[] { 0x01 }));
            ReadOnlyCollection<byte> dynamicFlags;
            ReadOnlyCollection<string> tupleElementNames;
            CustomTypeInfo.Decode(
                customTypeInfo.PayloadTypeId,
                customTypeInfo.Payload,
                out dynamicFlags,
                out tupleElementNames);
            Assert.Null(dynamicFlags);
            Assert.Null(tupleElementNames);
        }

        [Fact]
        public void Indexer()
        {
            ValidateIndexer(null);
            ValidateIndexer(false);
            ValidateIndexer(true);
            ValidateIndexer(false, false);
            ValidateIndexer(false, true);
            ValidateIndexer(true, false);
            ValidateIndexer(true, true);

            ValidateIndexer(false, false, true);
            ValidateIndexer(false, false, false, true);
            ValidateIndexer(false, false, false, false, true);
            ValidateIndexer(false, false, false, false, false, true);
            ValidateIndexer(false, false, false, false, false, false, true);
            ValidateIndexer(false, false, false, false, false, false, false, true);
            ValidateIndexer(false, false, false, false, false, false, false, false, true);
        }

        [Fact]
        public void SkipOne()
        {
            ValidateBytes(DynamicFlagsCustomTypeInfo.SkipOne(null));

            var dynamicFlagsCustomTypeInfo = new ReadOnlyCollection<byte>(new byte[] { 0x80 });

            dynamicFlagsCustomTypeInfo = DynamicFlagsCustomTypeInfo.SkipOne(dynamicFlagsCustomTypeInfo);
            ValidateBytes(dynamicFlagsCustomTypeInfo, 0x40);
            dynamicFlagsCustomTypeInfo = DynamicFlagsCustomTypeInfo.SkipOne(dynamicFlagsCustomTypeInfo);
            ValidateBytes(dynamicFlagsCustomTypeInfo, 0x20);
            dynamicFlagsCustomTypeInfo = DynamicFlagsCustomTypeInfo.SkipOne(dynamicFlagsCustomTypeInfo);
            ValidateBytes(dynamicFlagsCustomTypeInfo, 0x10);
            dynamicFlagsCustomTypeInfo = DynamicFlagsCustomTypeInfo.SkipOne(dynamicFlagsCustomTypeInfo);
            ValidateBytes(dynamicFlagsCustomTypeInfo, 0x08);
            dynamicFlagsCustomTypeInfo = DynamicFlagsCustomTypeInfo.SkipOne(dynamicFlagsCustomTypeInfo);
            ValidateBytes(dynamicFlagsCustomTypeInfo, 0x04);
            dynamicFlagsCustomTypeInfo = DynamicFlagsCustomTypeInfo.SkipOne(dynamicFlagsCustomTypeInfo);
            ValidateBytes(dynamicFlagsCustomTypeInfo, 0x02);
            dynamicFlagsCustomTypeInfo = DynamicFlagsCustomTypeInfo.SkipOne(dynamicFlagsCustomTypeInfo);
            ValidateBytes(dynamicFlagsCustomTypeInfo, 0x01);
            dynamicFlagsCustomTypeInfo = DynamicFlagsCustomTypeInfo.SkipOne(dynamicFlagsCustomTypeInfo);
            ValidateBytes(dynamicFlagsCustomTypeInfo);

            dynamicFlagsCustomTypeInfo = new ReadOnlyCollection<byte>(new byte[] { 0x00, 0x02 });

            dynamicFlagsCustomTypeInfo = DynamicFlagsCustomTypeInfo.SkipOne(dynamicFlagsCustomTypeInfo);
            ValidateBytes(dynamicFlagsCustomTypeInfo, 0x00, 0x01);
            dynamicFlagsCustomTypeInfo = DynamicFlagsCustomTypeInfo.SkipOne(dynamicFlagsCustomTypeInfo);
            ValidateBytes(dynamicFlagsCustomTypeInfo, 0x80, 0x00);
            dynamicFlagsCustomTypeInfo = DynamicFlagsCustomTypeInfo.SkipOne(dynamicFlagsCustomTypeInfo);
            ValidateBytes(dynamicFlagsCustomTypeInfo, 0x40, 0x00);
        }

        private static void ValidateCustomTypeInfo(params byte[] payload)
        {
            Assert.NotNull(payload);

            var dkmClrCustomTypeInfo = CustomTypeInfo.Create(new ReadOnlyCollection<byte>(payload), null);
            Assert.Equal(CustomTypeInfo.PayloadTypeId, dkmClrCustomTypeInfo.PayloadTypeId);
            Assert.NotNull(dkmClrCustomTypeInfo.Payload);

            ReadOnlyCollection<byte> dynamicFlags;
            ReadOnlyCollection<string> tupleElementNames;
            CustomTypeInfo.Decode(
                dkmClrCustomTypeInfo.PayloadTypeId,
                dkmClrCustomTypeInfo.Payload,
                out dynamicFlags,
                out tupleElementNames);

            ValidateBytes(dynamicFlags, payload);
            Assert.Null(tupleElementNames);
        }

        private static void ValidateIndexer(params bool[] dynamicFlags)
        {
            if (dynamicFlags == null)
            {
                Assert.False(DynamicFlagsCustomTypeInfo.GetFlag(null, 0));
            }
            else
            {
                var builder = ArrayBuilder<bool>.GetInstance(dynamicFlags.Length);
                builder.AddRange(dynamicFlags);
                var customTypeInfo = DynamicFlagsCustomTypeInfo.ToBytes(builder);
                builder.Free();

                AssertEx.All(dynamicFlags.Select((f, i) => f == DynamicFlagsCustomTypeInfo.GetFlag(customTypeInfo, i)), x => x);
                Assert.False(DynamicFlagsCustomTypeInfo.GetFlag(customTypeInfo, dynamicFlags.Length));
            }
        }

        private static void ValidateToBytes(bool[] dynamicFlags, params byte[] expectedBytes)
        {
            Assert.NotNull(dynamicFlags);
            Assert.NotNull(expectedBytes);

            var builder = ArrayBuilder<bool>.GetInstance(dynamicFlags.Length);
            builder.AddRange(dynamicFlags);
            var actualBytes = DynamicFlagsCustomTypeInfo.ToBytes(builder);
            builder.Free();
            ValidateBytes(actualBytes, expectedBytes);
        }

        private static void ValidateCopyTo(byte[] dynamicFlags, params bool[] expectedFlags)
        {
            var builder = ArrayBuilder<bool>.GetInstance();
            DynamicFlagsCustomTypeInfo.CopyTo(new ReadOnlyCollection<byte>(dynamicFlags), builder);
            var actualFlags = builder.ToArrayAndFree();
            Assert.Equal(expectedFlags, actualFlags);
        }

        private static void ValidateBytes(ReadOnlyCollection<byte> actualBytes, params byte[] expectedBytes)
        {
            Assert.NotNull(expectedBytes);

            if (expectedBytes.Length == 0)
            {
                Assert.Null(actualBytes);
            }
            else
            {
                Assert.Equal(expectedBytes, actualBytes);
            }
        }
    }
}
