﻿// 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.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.UnitTests
{
    public sealed class ObjectSerializationTests
    {
        static ObjectSerializationTests()
        {
            // Register appropriate deserialization methods.
            new PrimitiveArrayMemberTest();
            new PrimitiveMemberTest();
            new PrimitiveValueTest();
        }

        [Fact]
        public void TestInvalidStreamVersion()
        {
            var stream = new MemoryStream();
            stream.WriteByte(0);
            stream.WriteByte(0);

            stream.Position = 0;

            var reader = ObjectReader.TryGetReader(stream);
            Assert.Null(reader);
        }

        private void RoundTrip(Action<ObjectWriter> writeAction, Action<ObjectReader> readAction, bool recursive)
        {
            using var stream = new MemoryStream();

            using (var writer = new ObjectWriter(stream, leaveOpen: true))
            {
                writeAction(writer);
            }

            stream.Position = 0;
            using var reader = ObjectReader.TryGetReader(stream);
            readAction(reader);
        }

        private void TestRoundTrip(Action<ObjectWriter> writeAction, Action<ObjectReader> readAction)
        {
            RoundTrip(writeAction, readAction, recursive: true);
            RoundTrip(writeAction, readAction, recursive: false);
        }

        private T RoundTrip<T>(T value, Action<ObjectWriter, T> writeAction, Func<ObjectReader, T> readAction, bool recursive)
        {
            using var stream = new MemoryStream();

            using (var writer = new ObjectWriter(stream, leaveOpen: true))
            {
                writeAction(writer, value);
            }

            stream.Position = 0;
            using var reader = ObjectReader.TryGetReader(stream);
            return readAction(reader);
        }

        private void TestRoundTrip<T>(T value, Action<ObjectWriter, T> writeAction, Func<ObjectReader, T> readAction, bool recursive)
        {
            var newValue = RoundTrip(value, writeAction, readAction, recursive);
            Assert.True(Equalish(value, newValue));
        }

        private void TestRoundTrip<T>(T value, Action<ObjectWriter, T> writeAction, Func<ObjectReader, T> readAction)
        {
            TestRoundTrip(value, writeAction, readAction, recursive: true);
            TestRoundTrip(value, writeAction, readAction, recursive: false);
        }

        private T RoundTripValue<T>(T value, bool recursive)
        {
            return RoundTrip(value,
                (w, v) =>
                {
                    if (v != null && v.GetType().IsEnum)
                    {
                        w.WriteInt64(Convert.ToInt64((object)v));
                    }
                    else
                    {
                        w.WriteValue(v);
                    }
                },
                r => value != null && value.GetType().IsEnum
                    ? (T)Enum.ToObject(typeof(T), r.ReadInt64())
                    : (T)r.ReadValue(), recursive);
        }

        private void TestRoundTripValue<T>(T value, bool recursive)
        {
            var newValue = RoundTripValue(value, recursive);
            Assert.True(Equalish(value, newValue));
        }

        private void TestRoundTripValue<T>(T value)
        {
            TestRoundTripValue(value, recursive: true);
            TestRoundTripValue(value, recursive: false);
        }

        private static bool Equalish<T>(T value1, T value2)
        {
            return object.Equals(value1, value2)
                || (value1 is Array && value2 is Array && ArrayEquals((Array)(object)value1, (Array)(object)value2));
        }

        private static bool ArrayEquals(Array seq1, Array seq2)
        {
            if (seq1 == null && seq2 == null)
            {
                return true;
            }
            else if (seq1 == null || seq2 == null)
            {
                return false;
            }

            if (seq1.Length != seq2.Length)
            {
                return false;
            }

            for (int i = 0; i < seq1.Length; i++)
            {
                if (!Equalish(seq1.GetValue(i), seq2.GetValue(i)))
                {
                    return false;
                }
            }

            return true;
        }

        private class TypeWithOneMember<T> : IObjectWritable, IEquatable<TypeWithOneMember<T>>
        {
            private readonly T _member;

            public TypeWithOneMember(T value)
            {
                _member = value;
            }

            private TypeWithOneMember(ObjectReader reader)
            {
                _member = typeof(T).IsEnum
                    ? (T)Enum.ToObject(typeof(T), reader.ReadInt64())
                    : (T)reader.ReadValue();
            }

            bool IObjectWritable.ShouldReuseInSerialization => true;

            void IObjectWritable.WriteTo(ObjectWriter writer)
            {
                if (typeof(T).IsEnum)
                {
                    writer.WriteInt64(Convert.ToInt64(_member));
                }
                else
                {
                    writer.WriteValue(_member);
                }
            }

            static TypeWithOneMember()
            {
                ObjectBinder.RegisterTypeReader(typeof(TypeWithOneMember<T>), r => new TypeWithOneMember<T>(r));
            }

            public override Int32 GetHashCode()
            {
                if (_member == null)
                {
                    return 0;
                }
                else
                {
                    return _member.GetHashCode();
                }
            }

            public override Boolean Equals(Object obj)
            {
                return Equals(obj as TypeWithOneMember<T>);
            }

            public bool Equals(TypeWithOneMember<T> other)
            {
                return other != null && Equalish(_member, other._member);
            }
        }

        private class TypeWithTwoMembers<T, S> : IObjectWritable, IEquatable<TypeWithTwoMembers<T, S>>
        {
            private readonly T _member1;
            private readonly S _member2;

            public TypeWithTwoMembers(T value1, S value2)
            {
                _member1 = value1;
                _member2 = value2;
            }

            private TypeWithTwoMembers(ObjectReader reader)
            {
                _member1 = (T)reader.ReadValue();
                _member2 = (S)reader.ReadValue();
            }

            bool IObjectWritable.ShouldReuseInSerialization => true;

            void IObjectWritable.WriteTo(ObjectWriter writer)
            {
                writer.WriteValue(_member1);
                writer.WriteValue(_member2);
            }

            static TypeWithTwoMembers()
            {
                ObjectBinder.RegisterTypeReader(typeof(TypeWithTwoMembers<T, S>), r => new TypeWithTwoMembers<T, S>(r));
            }

            public override int GetHashCode()
            {
                if (_member1 == null)
                {
                    return 0;
                }
                else
                {
                    return _member1.GetHashCode();
                }
            }

            public override Boolean Equals(Object obj)
            {
                return Equals(obj as TypeWithTwoMembers<T, S>);
            }

            public bool Equals(TypeWithTwoMembers<T, S> other)
            {
                return other != null
                    && Equalish(_member1, other._member1)
                    && Equalish(_member2, other._member2);
            }
        }

        // this type simulates a class with many members.. 
        // it serializes each member individually, not as an array.
        private class TypeWithManyMembers<T> : IObjectWritable, IEquatable<TypeWithManyMembers<T>>
        {
            private readonly T[] _members;

            public TypeWithManyMembers(T[] values)
            {
                _members = values;
            }

            private TypeWithManyMembers(ObjectReader reader)
            {
                var count = reader.ReadInt32();
                _members = new T[count];

                for (int i = 0; i < count; i++)
                {
                    _members[i] = (T)reader.ReadValue();
                }
            }

            bool IObjectWritable.ShouldReuseInSerialization => true;

            void IObjectWritable.WriteTo(ObjectWriter writer)
            {
                writer.WriteInt32(_members.Length);

                for (int i = 0; i < _members.Length; i++)
                {
                    writer.WriteValue(_members[i]);
                }
            }

            static TypeWithManyMembers()
            {
                ObjectBinder.RegisterTypeReader(typeof(TypeWithManyMembers<T>), r => new TypeWithManyMembers<T>(r));
            }

            public override int GetHashCode()
            {
                return _members.Length;
            }

            public override Boolean Equals(Object obj)
            {
                return Equals(obj as TypeWithManyMembers<T>);
            }

            public bool Equals(TypeWithManyMembers<T> other)
            {
                if (other == null)
                {
                    return false;
                }

                if (_members.Length != other._members.Length)
                {
                    return false;
                }

                return Equalish(_members, other._members);
            }
        }

        private void TestRoundTripMember<T>(T value)
        {
            TestRoundTripValue(new TypeWithOneMember<T>(value));
        }

        private void TestRoundTripMembers<T, S>(T value1, S value2)
        {
            TestRoundTripValue(new TypeWithTwoMembers<T, S>(value1, value2));
        }

        private void TestRoundTripMembers<T>(params T[] values)
        {
            TestRoundTripValue(new TypeWithManyMembers<T>(values));
        }

        [Fact]
        public void TestValueInt32()
        {
            TestRoundTripValue(123);
        }

        [Fact]
        public void TestMemberInt32()
        {
            TestRoundTripMember(123);
        }

        [Fact]
        public void TestMemberIntString()
        {
            TestRoundTripMembers(123, "Hello");
        }

        [Fact]
        public void TestManyMembersInt32()
        {
            TestRoundTripMembers(Enumerable.Range(0, 1000).ToArray());
        }

        [Fact]
        public void TestSmallArrayMember()
        {
            TestRoundTripMember(Enumerable.Range(0, 3).ToArray());
        }

        [Fact]
        public void TestEmptyArrayMember()
        {
            TestRoundTripMember(new int[] { });
        }

        [Fact]
        public void TestNullArrayMember()
        {
            TestRoundTripMember<int[]>(null);
        }

        [Fact]
        public void TestLargeArrayMember()
        {
            TestRoundTripMember(Enumerable.Range(0, 1000).ToArray());
        }

        [Fact]
        public void TestEnumMember()
        {
            TestRoundTripMember(EByte.Value);
        }

        [Fact]
        public void TestInt32TypeCodes()
        {
            Assert.Equal(ObjectWriter.TypeCode.Int32_1, ObjectWriter.TypeCode.Int32_0 + 1);
            Assert.Equal(ObjectWriter.TypeCode.Int32_2, ObjectWriter.TypeCode.Int32_0 + 2);
            Assert.Equal(ObjectWriter.TypeCode.Int32_3, ObjectWriter.TypeCode.Int32_0 + 3);
            Assert.Equal(ObjectWriter.TypeCode.Int32_4, ObjectWriter.TypeCode.Int32_0 + 4);
            Assert.Equal(ObjectWriter.TypeCode.Int32_5, ObjectWriter.TypeCode.Int32_0 + 5);
            Assert.Equal(ObjectWriter.TypeCode.Int32_6, ObjectWriter.TypeCode.Int32_0 + 6);
            Assert.Equal(ObjectWriter.TypeCode.Int32_7, ObjectWriter.TypeCode.Int32_0 + 7);
            Assert.Equal(ObjectWriter.TypeCode.Int32_8, ObjectWriter.TypeCode.Int32_0 + 8);
            Assert.Equal(ObjectWriter.TypeCode.Int32_9, ObjectWriter.TypeCode.Int32_0 + 9);
            Assert.Equal(ObjectWriter.TypeCode.Int32_10, ObjectWriter.TypeCode.Int32_0 + 10);
        }

        [Fact]
        public void TestUInt32TypeCodes()
        {
            Assert.Equal(ObjectWriter.TypeCode.UInt32_1, ObjectWriter.TypeCode.UInt32_0 + 1);
            Assert.Equal(ObjectWriter.TypeCode.UInt32_2, ObjectWriter.TypeCode.UInt32_0 + 2);
            Assert.Equal(ObjectWriter.TypeCode.UInt32_3, ObjectWriter.TypeCode.UInt32_0 + 3);
            Assert.Equal(ObjectWriter.TypeCode.UInt32_4, ObjectWriter.TypeCode.UInt32_0 + 4);
            Assert.Equal(ObjectWriter.TypeCode.UInt32_5, ObjectWriter.TypeCode.UInt32_0 + 5);
            Assert.Equal(ObjectWriter.TypeCode.UInt32_6, ObjectWriter.TypeCode.UInt32_0 + 6);
            Assert.Equal(ObjectWriter.TypeCode.UInt32_7, ObjectWriter.TypeCode.UInt32_0 + 7);
            Assert.Equal(ObjectWriter.TypeCode.UInt32_8, ObjectWriter.TypeCode.UInt32_0 + 8);
            Assert.Equal(ObjectWriter.TypeCode.UInt32_9, ObjectWriter.TypeCode.UInt32_0 + 9);
            Assert.Equal(ObjectWriter.TypeCode.UInt32_10, ObjectWriter.TypeCode.UInt32_0 + 10);
        }

        private void TestRoundTripType(Type type)
        {
            TestRoundTrip(type, (w, v) => w.WriteType(v), r => r.ReadType());
        }

        [Fact]
        public void TestTypes()
        {
            TestRoundTripType(typeof(int));
            TestRoundTripType(typeof(string));
            TestRoundTripType(typeof(ObjectSerializationTests));
        }

        private void TestRoundTripCompressedUint(uint value)
        {
            TestRoundTrip(value, (w, v) => ((ObjectWriter)w).WriteCompressedUInt(v), r => ((ObjectReader)r).ReadCompressedUInt());
        }

        [Fact]
        public void TestCompressedUInt()
        {
            TestRoundTripCompressedUint(0);
            TestRoundTripCompressedUint(0x01u);
            TestRoundTripCompressedUint(0x0123u);     // unique bytes tests order
            TestRoundTripCompressedUint(0x012345u);   // unique bytes tests order
            TestRoundTripCompressedUint(0x01234567u); // unique bytes tests order
            TestRoundTripCompressedUint(0x3Fu);       // largest value packed in one byte
            TestRoundTripCompressedUint(0x3FFFu);     // largest value packed into two bytes
            TestRoundTripCompressedUint(0x3FFFFFu);   // no three byte option yet, but test anyway
            TestRoundTripCompressedUint(0x3FFFFFFFu); // largest unit allowed in four bytes

            Assert.Throws<ArgumentException>(() => TestRoundTripCompressedUint(uint.MaxValue)); // max uint not allowed
            Assert.Throws<ArgumentException>(() => TestRoundTripCompressedUint(0x80000000u)); // highest bit set not allowed
            Assert.Throws<ArgumentException>(() => TestRoundTripCompressedUint(0x40000000u)); // second highest bit set not allowed
            Assert.Throws<ArgumentException>(() => TestRoundTripCompressedUint(0xC0000000u)); // both high bits set not allowed
        }

        [Fact]
        public void TestArraySizes()
        {
            TestArrayValues<byte>(1, 2, 3, 4, 5);
            TestArrayValues<sbyte>(1, 2, 3, 4, 5);
            TestArrayValues<short>(1, 2, 3, 4, 5);
            TestArrayValues<ushort>(1, 2, 3, 4, 5);
            TestArrayValues<int>(1, 2, 3, 4, 5);
            TestArrayValues<uint>(1, 2, 3, 4, 5);
            TestArrayValues<long>(1, 2, 3, 4, 5);
            TestArrayValues<ulong>(1, 2, 3, 4, 5);
            TestArrayValues<decimal>(1m, 2m, 3m, 4m, 5m);
            TestArrayValues<float>(1.0f, 2.0f, 3.0f, 4.0f, 5.0f);
            TestArrayValues<double>(1.0, 2.0, 3.0, 4.0, 5.0);
            TestArrayValues<char>('1', '2', '3', '4', '5');
            TestArrayValues<string>("1", "2", "3", "4", "5");
            TestArrayValues(
                new TypeWithOneMember<int>(1),
                new TypeWithOneMember<int>(2),
                new TypeWithOneMember<int>(3),
                new TypeWithOneMember<int>(4),
                new TypeWithOneMember<int>(5));
        }

        private void TestArrayValues<T>(T v1, T v2, T v3, T v4, T v5)
        {
            TestRoundTripValue((T[])null);
            TestRoundTripValue(new T[] { });
            TestRoundTripValue(new T[] { v1 });
            TestRoundTripValue(new T[] { v1, v2 });
            TestRoundTripValue(new T[] { v1, v2, v3 });
            TestRoundTripValue(new T[] { v1, v2, v3, v4 });
            TestRoundTripValue(new T[] { v1, v2, v3, v4, v5 });
        }

        [Fact]
        public void TestPrimitiveArrayValues()
        {
            TestRoundTrip(w => TestWritingPrimitiveArrays(w), r => TestReadingPrimitiveArrays(r));
        }

        [Theory]
        [CombinatorialData]
        public void TestByteSpan([CombinatorialValues(0, 1, 2, 3, 1000, 1000000)] int size)
        {
            var data = new byte[size];
            for (int i = 0; i < data.Length; i++)
            {
                data[i] = (byte)i;
            }

            TestRoundTrip(w => TestWritingByteSpan(data, w), r => TestReadingByteSpan(data, r));
        }

        [Fact]
        public void TestPrimitiveArrayMembers()
        {
            TestRoundTrip(w => w.WriteValue(new PrimitiveArrayMemberTest()), r => r.ReadValue());
        }

        public class PrimitiveArrayMemberTest : IObjectWritable
        {
            public PrimitiveArrayMemberTest()
            {
            }

            private PrimitiveArrayMemberTest(ObjectReader reader)
            {
                TestReadingPrimitiveArrays(reader);
            }

            bool IObjectWritable.ShouldReuseInSerialization => true;

            void IObjectWritable.WriteTo(ObjectWriter writer)
            {
                TestWritingPrimitiveArrays(writer);
            }

            static PrimitiveArrayMemberTest()
            {
                ObjectBinder.RegisterTypeReader(typeof(PrimitiveArrayMemberTest), r => new PrimitiveArrayMemberTest(r));
            }
        }

        private static void TestWritingPrimitiveArrays(ObjectWriter writer)
        {
            var inputBool = new bool[] { true, false };
            var inputByte = new byte[] { 1, 2, 3, 4, 5 };
            var inputChar = new char[] { 'h', 'e', 'l', 'l', 'o' };
            var inputDecimal = new decimal[] { 1.0M, 2.0M, 3.0M, 4.0M, 5.0M };
            var inputDouble = new double[] { 1.0, 2.0, 3.0, 4.0, 5.0 };
            var inputFloat = new float[] { 1.0F, 2.0F, 3.0F, 4.0F, 5.0F };
            var inputInt = new int[] { -1, -2, -3, -4, -5 };
            var inputLong = new long[] { 1, 2, 3, 4, 5 };
            var inputSByte = new sbyte[] { -1, -2, -3, -4, -5 };
            var inputShort = new short[] { -1, -2, -3, -4, -5 };
            var inputUInt = new uint[] { 1, 2, 3, 4, 5 };
            var inputULong = new ulong[] { 1, 2, 3, 4, 5 };
            var inputUShort = new ushort[] { 1, 2, 3, 4, 5 };
            var inputString = new string[] { "h", "e", "l", "l", "o" };

            writer.WriteValue(inputBool);
            writer.WriteValue((object)inputByte);
            writer.WriteValue(inputChar);
            writer.WriteValue(inputDecimal);
            writer.WriteValue(inputDouble);
            writer.WriteValue(inputFloat);
            writer.WriteValue(inputInt);
            writer.WriteValue(inputLong);
            writer.WriteValue(inputSByte);
            writer.WriteValue(inputShort);
            writer.WriteValue(inputUInt);
            writer.WriteValue(inputULong);
            writer.WriteValue(inputUShort);
            writer.WriteValue(inputString);
        }

        private static void TestReadingPrimitiveArrays(ObjectReader reader)
        {
            var inputBool = new bool[] { true, false };
            var inputByte = new byte[] { 1, 2, 3, 4, 5 };
            var inputChar = new char[] { 'h', 'e', 'l', 'l', 'o' };
            var inputDecimal = new decimal[] { 1.0M, 2.0M, 3.0M, 4.0M, 5.0M };
            var inputDouble = new double[] { 1.0, 2.0, 3.0, 4.0, 5.0 };
            var inputFloat = new float[] { 1.0F, 2.0F, 3.0F, 4.0F, 5.0F };
            var inputInt = new int[] { -1, -2, -3, -4, -5 };
            var inputLong = new long[] { 1, 2, 3, 4, 5 };
            var inputSByte = new sbyte[] { -1, -2, -3, -4, -5 };
            var inputShort = new short[] { -1, -2, -3, -4, -5 };
            var inputUInt = new uint[] { 1, 2, 3, 4, 5 };
            var inputULong = new ulong[] { 1, 2, 3, 4, 5 };
            var inputUShort = new ushort[] { 1, 2, 3, 4, 5 };
            var inputString = new string[] { "h", "e", "l", "l", "o" };

            Assert.True(Enumerable.SequenceEqual(inputBool, (bool[])reader.ReadValue()));
            Assert.True(Enumerable.SequenceEqual(inputByte, (byte[])reader.ReadValue()));
            Assert.True(Enumerable.SequenceEqual(inputChar, (char[])reader.ReadValue()));
            Assert.True(Enumerable.SequenceEqual(inputDecimal, (decimal[])reader.ReadValue()));
            Assert.True(Enumerable.SequenceEqual(inputDouble, (double[])reader.ReadValue()));
            Assert.True(Enumerable.SequenceEqual(inputFloat, (float[])reader.ReadValue()));
            Assert.True(Enumerable.SequenceEqual(inputInt, (int[])reader.ReadValue()));
            Assert.True(Enumerable.SequenceEqual(inputLong, (long[])reader.ReadValue()));
            Assert.True(Enumerable.SequenceEqual(inputSByte, (sbyte[])reader.ReadValue()));
            Assert.True(Enumerable.SequenceEqual(inputShort, (short[])reader.ReadValue()));
            Assert.True(Enumerable.SequenceEqual(inputUInt, (uint[])reader.ReadValue()));
            Assert.True(Enumerable.SequenceEqual(inputULong, (ulong[])reader.ReadValue()));
            Assert.True(Enumerable.SequenceEqual(inputUShort, (ushort[])reader.ReadValue()));
            Assert.True(Enumerable.SequenceEqual(inputString, (string[])reader.ReadValue()));
        }

        private static void TestWritingByteSpan(byte[] data, ObjectWriter writer)
        {
            writer.WriteValue(data.AsSpan());
        }

        private static void TestReadingByteSpan(byte[] expected, ObjectReader reader)
        {
            Assert.True(Enumerable.SequenceEqual(expected, (byte[])reader.ReadValue()));
        }

        [Fact]
        public void TestBooleanArrays()
        {
            for (var i = 0; i < 1000; i++)
            {
                var inputBool = new bool[i];

                for (var j = 0; j < i; j++)
                {
                    inputBool[j] = j % 2 == 0;
                }

                TestRoundTripValue(inputBool);
                TestRoundTripMember(inputBool);
            }
        }

        [Fact]
        public void TestFalseBooleanArray()
        {
            var inputBool = Enumerable.Repeat<bool>(false, 1000).ToArray();
            TestRoundTripValue(inputBool);
            TestRoundTripMember(inputBool);
        }

        private static readonly DateTime _testNow = DateTime.Now;

        [Fact]
        public void TestPrimitiveValues()
        {
            TestRoundTripValue(true);
            TestRoundTripValue(false);
            TestRoundTripValue(Byte.MaxValue);
            TestRoundTripValue(SByte.MaxValue);
            TestRoundTripValue(Int16.MaxValue);
            TestRoundTripValue(Int32.MaxValue);
            TestRoundTripValue(Byte.MaxValue);
            TestRoundTripValue(Int16.MaxValue);
            TestRoundTripValue(Int64.MaxValue);
            TestRoundTripValue(UInt16.MaxValue);
            TestRoundTripValue(UInt32.MaxValue);
            TestRoundTripValue(UInt64.MaxValue);
            TestRoundTripValue(Decimal.MaxValue);
            TestRoundTripValue(Double.MaxValue);
            TestRoundTripValue(Single.MaxValue);
            TestRoundTripValue('X');
            TestRoundTripValue("YYY");
            TestRoundTripValue("\uD800\uDC00"); // valid surrogate pair
            TestRoundTripValue("\uDC00\uD800"); // invalid surrogate pair
            TestRoundTripValue("\uD800"); // incomplete surrogate pair
            TestRoundTripValue<object>(null);
            TestRoundTripValue(ConsoleColor.Cyan);
            TestRoundTripValue(EByte.Value);
            TestRoundTripValue(ESByte.Value);
            TestRoundTripValue(EShort.Value);
            TestRoundTripValue(EUShort.Value);
            TestRoundTripValue(EInt.Value);
            TestRoundTripValue(EUInt.Value);
            TestRoundTripValue(ELong.Value);
            TestRoundTripValue(EULong.Value);
            TestRoundTripValue(_testNow);
        }

        [Fact]
        public void TestInt32Values()
        {
            TestRoundTripValue<Int32>(0);
            TestRoundTripValue<Int32>(1);
            TestRoundTripValue<Int32>(2);
            TestRoundTripValue<Int32>(3);
            TestRoundTripValue<Int32>(4);
            TestRoundTripValue<Int32>(5);
            TestRoundTripValue<Int32>(6);
            TestRoundTripValue<Int32>(7);
            TestRoundTripValue<Int32>(8);
            TestRoundTripValue<Int32>(9);
            TestRoundTripValue<Int32>(10);
            TestRoundTripValue<Int32>(-1);
            TestRoundTripValue<Int32>(Int32.MinValue);
            TestRoundTripValue<Int32>(Byte.MaxValue);
            TestRoundTripValue<Int32>(UInt16.MaxValue);
            TestRoundTripValue<Int32>(Int32.MaxValue);
        }

        [Fact]
        public void TestUInt32Values()
        {
            TestRoundTripValue<UInt32>(0);
            TestRoundTripValue<UInt32>(1);
            TestRoundTripValue<UInt32>(2);
            TestRoundTripValue<UInt32>(3);
            TestRoundTripValue<UInt32>(4);
            TestRoundTripValue<UInt32>(5);
            TestRoundTripValue<UInt32>(6);
            TestRoundTripValue<UInt32>(7);
            TestRoundTripValue<UInt32>(8);
            TestRoundTripValue<UInt32>(9);
            TestRoundTripValue<UInt32>(10);
            TestRoundTripValue<Int32>(Byte.MaxValue);
            TestRoundTripValue<Int32>(UInt16.MaxValue);
            TestRoundTripValue<Int32>(Int32.MaxValue);
        }

        [Fact]
        public void TestInt64Values()
        {
            TestRoundTripValue<Int64>(0);
            TestRoundTripValue<Int64>(1);
            TestRoundTripValue<Int64>(2);
            TestRoundTripValue<Int64>(3);
            TestRoundTripValue<Int64>(4);
            TestRoundTripValue<Int64>(5);
            TestRoundTripValue<Int64>(6);
            TestRoundTripValue<Int64>(7);
            TestRoundTripValue<Int64>(8);
            TestRoundTripValue<Int64>(9);
            TestRoundTripValue<Int64>(10);
            TestRoundTripValue<Int64>(-1);
            TestRoundTripValue<Int64>(Byte.MinValue);
            TestRoundTripValue<Int64>(Byte.MaxValue);
            TestRoundTripValue<Int64>(Int16.MinValue);
            TestRoundTripValue<Int64>(Int16.MaxValue);
            TestRoundTripValue<Int64>(UInt16.MinValue);
            TestRoundTripValue<Int64>(UInt16.MaxValue);
            TestRoundTripValue<Int64>(Int32.MinValue);
            TestRoundTripValue<Int64>(Int32.MaxValue);
            TestRoundTripValue<Int64>(UInt32.MinValue);
            TestRoundTripValue<Int64>(UInt32.MaxValue);
            TestRoundTripValue<Int64>(Int64.MinValue);
            TestRoundTripValue<Int64>(Int64.MaxValue);
        }

        [Fact]
        public void TestUInt64Values()
        {
            TestRoundTripValue<UInt64>(0);
            TestRoundTripValue<UInt64>(1);
            TestRoundTripValue<UInt64>(2);
            TestRoundTripValue<UInt64>(3);
            TestRoundTripValue<UInt64>(4);
            TestRoundTripValue<UInt64>(5);
            TestRoundTripValue<UInt64>(6);
            TestRoundTripValue<UInt64>(7);
            TestRoundTripValue<UInt64>(8);
            TestRoundTripValue<UInt64>(9);
            TestRoundTripValue<UInt64>(10);
            TestRoundTripValue<UInt64>(Byte.MinValue);
            TestRoundTripValue<UInt64>(Byte.MaxValue);
            TestRoundTripValue<UInt64>(UInt16.MinValue);
            TestRoundTripValue<UInt64>(UInt16.MaxValue);
            TestRoundTripValue<UInt64>(Int32.MaxValue);
            TestRoundTripValue<UInt64>(UInt32.MinValue);
            TestRoundTripValue<UInt64>(UInt32.MaxValue);
            TestRoundTripValue<UInt64>(UInt64.MinValue);
            TestRoundTripValue<UInt64>(UInt64.MaxValue);
        }

        [Fact]
        public void TestPrimitiveMemberValues()
        {
            TestRoundTripMember(true);
            TestRoundTripMember(false);
            TestRoundTripMember(Byte.MaxValue);
            TestRoundTripMember(SByte.MaxValue);
            TestRoundTripMember(Int16.MaxValue);
            TestRoundTripMember(Int32.MaxValue);
            TestRoundTripMember(Byte.MaxValue);
            TestRoundTripMember(Int16.MaxValue);
            TestRoundTripMember(Int64.MaxValue);
            TestRoundTripMember(UInt16.MaxValue);
            TestRoundTripMember(UInt32.MaxValue);
            TestRoundTripMember(UInt64.MaxValue);
            TestRoundTripMember(Decimal.MaxValue);
            TestRoundTripMember(Double.MaxValue);
            TestRoundTripMember(Single.MaxValue);
            TestRoundTripMember('X');
            TestRoundTripMember("YYY");
            TestRoundTripMember("\uD800\uDC00"); // valid surrogate pair
            TestRoundTripMember("\uDC00\uD800"); // invalid surrogate pair
            TestRoundTripMember("\uD800"); // incomplete surrogate pair
            TestRoundTripMember<object>(null);
            TestRoundTripMember(ConsoleColor.Cyan);
            TestRoundTripMember(EByte.Value);
            TestRoundTripMember(ESByte.Value);
            TestRoundTripMember(EShort.Value);
            TestRoundTripMember(EUShort.Value);
            TestRoundTripMember(EInt.Value);
            TestRoundTripMember(EUInt.Value);
            TestRoundTripMember(ELong.Value);
            TestRoundTripMember(EULong.Value);
            TestRoundTripMember(_testNow);
        }

        [Fact]
        public void TestPrimitiveAPIs()
        {
            TestRoundTrip(w => TestWritingPrimitiveAPIs(w), r => TestReadingPrimitiveAPIs(r));
        }

        [Fact]
        public void TestPrimitiveMemberAPIs()
        {
            TestRoundTrip(w => w.WriteValue(new PrimitiveMemberTest()), r => r.ReadValue());
        }

        public class PrimitiveMemberTest : IObjectWritable
        {
            public PrimitiveMemberTest()
            {
            }

            private PrimitiveMemberTest(ObjectReader reader)
            {
                TestReadingPrimitiveAPIs(reader);
            }

            bool IObjectWritable.ShouldReuseInSerialization => true;

            void IObjectWritable.WriteTo(ObjectWriter writer)
            {
                TestWritingPrimitiveAPIs(writer);
            }

            static PrimitiveMemberTest()
            {
                ObjectBinder.RegisterTypeReader(typeof(PrimitiveMemberTest), r => new PrimitiveMemberTest(r));
            }
        }

        private static void TestWritingPrimitiveAPIs(ObjectWriter writer)
        {
            writer.WriteBoolean(true);
            writer.WriteBoolean(false);
            writer.WriteByte(Byte.MaxValue);
            writer.WriteSByte(SByte.MaxValue);
            writer.WriteInt16(Int16.MaxValue);
            writer.WriteInt32(Int32.MaxValue);
            writer.WriteInt32(Byte.MaxValue);
            writer.WriteInt32(Int16.MaxValue);
            writer.WriteInt64(Int64.MaxValue);
            writer.WriteUInt16(UInt16.MaxValue);
            writer.WriteUInt32(UInt32.MaxValue);
            writer.WriteUInt64(UInt64.MaxValue);
            writer.WriteDecimal(Decimal.MaxValue);
            writer.WriteDouble(Double.MaxValue);
            writer.WriteSingle(Single.MaxValue);
            writer.WriteChar('X');
            writer.WriteString("YYY");
            writer.WriteString("\uD800\uDC00"); // valid surrogate pair
            writer.WriteString("\uDC00\uD800"); // invalid surrogate pair
            writer.WriteString("\uD800"); // incomplete surrogate pair
        }

        private static void TestReadingPrimitiveAPIs(ObjectReader reader)
        {
            Assert.True(reader.ReadBoolean());
            Assert.False(reader.ReadBoolean());
            Assert.Equal(Byte.MaxValue, reader.ReadByte());
            Assert.Equal(SByte.MaxValue, reader.ReadSByte());
            Assert.Equal(Int16.MaxValue, reader.ReadInt16());
            Assert.Equal(Int32.MaxValue, reader.ReadInt32());
            Assert.Equal(Byte.MaxValue, reader.ReadInt32());
            Assert.Equal(Int16.MaxValue, reader.ReadInt32());
            Assert.Equal(Int64.MaxValue, reader.ReadInt64());
            Assert.Equal(UInt16.MaxValue, reader.ReadUInt16());
            Assert.Equal(UInt32.MaxValue, reader.ReadUInt32());
            Assert.Equal(UInt64.MaxValue, reader.ReadUInt64());
            Assert.Equal(Decimal.MaxValue, reader.ReadDecimal());
            Assert.Equal(Double.MaxValue, reader.ReadDouble());
            Assert.Equal(Single.MaxValue, reader.ReadSingle());
            Assert.Equal('X', reader.ReadChar());
            Assert.Equal("YYY", reader.ReadString());
            Assert.Equal("\uD800\uDC00", reader.ReadString()); // valid surrogate pair
            Assert.Equal("\uDC00\uD800", reader.ReadString()); // invalid surrogate pair
            Assert.Equal("\uD800", reader.ReadString()); // incomplete surrogate pair
        }

        [Fact]
        public void TestPrimitivesValue()
        {
            TestRoundTrip(w => TestWritingPrimitiveValues(w), r => TestReadingPrimitiveValues(r));
        }

        [Fact]
        public void TestPrimitiveValueAPIs()
        {
            TestRoundTrip(w => w.WriteValue(new PrimitiveValueTest()), r => r.ReadValue());
        }

        public class PrimitiveValueTest : IObjectWritable
        {
            public PrimitiveValueTest()
            {
            }

            private PrimitiveValueTest(ObjectReader reader)
            {
                TestReadingPrimitiveValues(reader);
            }

            bool IObjectWritable.ShouldReuseInSerialization => true;

            void IObjectWritable.WriteTo(ObjectWriter writer)
            {
                TestWritingPrimitiveValues(writer);
            }

            static PrimitiveValueTest()
            {
                ObjectBinder.RegisterTypeReader(typeof(PrimitiveValueTest), r => new PrimitiveValueTest(r));
            }
        }

        private static void TestWritingPrimitiveValues(ObjectWriter writer)
        {
            writer.WriteValue(true);
            writer.WriteValue(false);
            writer.WriteValue(Byte.MaxValue);
            writer.WriteValue(SByte.MaxValue);
            writer.WriteValue(Int16.MaxValue);
            writer.WriteValue(Int32.MaxValue);
            writer.WriteValue((Int32)Byte.MaxValue);
            writer.WriteValue((Int32)Int16.MaxValue);
            writer.WriteValue(Int64.MaxValue);
            writer.WriteValue(UInt16.MaxValue);
            writer.WriteValue(UInt32.MaxValue);
            writer.WriteValue(UInt64.MaxValue);
            writer.WriteValue(Decimal.MaxValue);
            writer.WriteValue(Double.MaxValue);
            writer.WriteValue(Single.MaxValue);
            writer.WriteValue('X');

            writer.WriteValue((object)"YYY");

            writer.WriteValue((object)"\uD800\uDC00"); // valid surrogate pair

            writer.WriteValue((object)"\uDC00\uD800"); // invalid surrogate pair

            writer.WriteValue((object)"\uD800"); // incomplete surrogate pair

            writer.WriteValue((object)null);
            writer.WriteValue((IObjectWritable)null);
            unchecked
            {
                writer.WriteInt64((long)ConsoleColor.Cyan);
                writer.WriteInt64((long)EByte.Value);
                writer.WriteInt64((long)ESByte.Value);
                writer.WriteInt64((long)EShort.Value);
                writer.WriteInt64((long)EUShort.Value);
                writer.WriteInt64((long)EInt.Value);
                writer.WriteInt64((long)EUInt.Value);
                writer.WriteInt64((long)ELong.Value);
                writer.WriteInt64((long)EULong.Value);
            }
            writer.WriteValue(_testNow);
        }

        private static void TestReadingPrimitiveValues(ObjectReader reader)
        {
            Assert.True((bool)reader.ReadValue());
            Assert.False((bool)reader.ReadValue());
            Assert.Equal(Byte.MaxValue, (Byte)reader.ReadValue());
            Assert.Equal(SByte.MaxValue, (SByte)reader.ReadValue());
            Assert.Equal(Int16.MaxValue, (Int16)reader.ReadValue());
            Assert.Equal(Int32.MaxValue, (Int32)reader.ReadValue());
            Assert.Equal(Byte.MaxValue, (Int32)reader.ReadValue());
            Assert.Equal(Int16.MaxValue, (Int32)reader.ReadValue());
            Assert.Equal(Int64.MaxValue, (Int64)reader.ReadValue());
            Assert.Equal(UInt16.MaxValue, (UInt16)reader.ReadValue());
            Assert.Equal(UInt32.MaxValue, (UInt32)reader.ReadValue());
            Assert.Equal(UInt64.MaxValue, (UInt64)reader.ReadValue());
            Assert.Equal(Decimal.MaxValue, (Decimal)reader.ReadValue());
            Assert.Equal(Double.MaxValue, (Double)reader.ReadValue());
            Assert.Equal(Single.MaxValue, (Single)reader.ReadValue());
            Assert.Equal('X', (Char)reader.ReadValue());
            Assert.Equal("YYY", (String)reader.ReadValue());
            Assert.Equal("\uD800\uDC00", (String)reader.ReadValue()); // valid surrogate pair
            Assert.Equal("\uDC00\uD800", (String)reader.ReadValue()); // invalid surrogate pair
            Assert.Equal("\uD800", (String)reader.ReadValue()); // incomplete surrogate pair
            Assert.Null(reader.ReadValue());
            Assert.Null(reader.ReadValue());

            unchecked
            {
                Assert.Equal((long)ConsoleColor.Cyan, reader.ReadInt64());
                Assert.Equal((long)EByte.Value, reader.ReadInt64());
                Assert.Equal((long)ESByte.Value, reader.ReadInt64());
                Assert.Equal((long)EShort.Value, reader.ReadInt64());
                Assert.Equal((long)EUShort.Value, reader.ReadInt64());
                Assert.Equal((long)EInt.Value, reader.ReadInt64());
                Assert.Equal((long)EUInt.Value, reader.ReadInt64());
                Assert.Equal((long)ELong.Value, reader.ReadInt64());
                Assert.Equal((long)EULong.Value, reader.ReadInt64());
            }

            Assert.Equal(_testNow, (DateTime)reader.ReadValue());
        }

        public enum EByte : byte
        {
            Value = 1
        }

        public enum ESByte : sbyte
        {
            Value = 2
        }

        public enum EShort : short
        {
            Value = 3
        }

        public enum EUShort : ushort
        {
            Value = 4
        }

        public enum EInt : int
        {
            Value = 5
        }

        public enum EUInt : uint
        {
            Value = 6
        }

        public enum ELong : long
        {
            Value = 7
        }

        public enum EULong : ulong
        {
            Value = 8
        }

        [Fact]
        public void TestRoundTripCharacters()
        {
            // round trip all possible characters as a string
            for (int i = ushort.MinValue; i <= ushort.MaxValue; i++)
            {
                TestRoundTripChar((char)i);
            }
        }

        private void TestRoundTripChar(Char ch)
        {
            TestRoundTrip(ch, (w, v) => w.WriteChar(v), r => r.ReadChar());
        }

        [Fact]
        public void TestRoundTripGuid()
        {
            void test(Guid guid)
            {
                TestRoundTrip(guid, (w, v) => w.WriteGuid(v), r => r.ReadGuid());
            }

            test(Guid.Empty);
            test(new Guid(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1));
            test(new Guid(0b10000000000000000000000000000000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1));
            test(new Guid(0b10000000000000000000000000000000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0));
            for (int i = 0; i < 10; i++)
            {
                test(Guid.NewGuid());
            }
        }

        [Fact]
        public void TestRoundTripStringCharacters()
        {
            // round trip all possible characters as a string
            for (int i = ushort.MinValue; i <= ushort.MaxValue; i++)
            {
                TestRoundTripStringCharacter((ushort)i);
            }

            // round trip single string with all possible characters
            var sb = new StringBuilder();
            for (int i = ushort.MinValue; i <= ushort.MaxValue; i++)
            {
                sb.Append((char)i);
            }

            TestRoundTripString(sb.ToString());
        }

        private void TestRoundTripString(string text)
        {
            TestRoundTrip(text, (w, v) => w.WriteString(v), r => r.ReadString());
        }

        private void TestRoundTripStringCharacter(ushort code)
        {
            TestRoundTripString(new String((char)code, 1));
        }

        [Fact]
        public void TestRoundTripArrays()
        {
            //TestRoundTripArray(new object[] { });
            //TestRoundTripArray(new object[] { "hello" });
            //TestRoundTripArray(new object[] { "hello", "world" });
            //TestRoundTripArray(new object[] { "hello", "world", "good" });
            //TestRoundTripArray(new object[] { "hello", "world", "good", "bye" });
            //TestRoundTripArray(new object[] { "hello", 123, 45m, 99.9, 'c' });
            TestRoundTripArray(new string[] { "hello", null, "world" });
        }

        private void TestRoundTripArray<T>(T[] values)
        {
            TestRoundTripValue(values);
        }

        public static IEnumerable<object[]> GetEncodingTestCases()
            => EncodingTestHelpers.GetEncodingTestCases();

        [Theory]
        [MemberData(nameof(GetEncodingTestCases))]
        public void Encodings(Encoding original)
        {
            using var stream = new MemoryStream();

            using (var writer = new ObjectWriter(stream, leaveOpen: true))
            {
                writer.WriteEncoding(original);
            }

            stream.Position = 0;

            using var reader = ObjectReader.TryGetReader(stream);
            Assert.NotNull(reader);
            var deserialized = (Encoding)reader.ReadValue();
            EncodingTestHelpers.AssertEncodingsEqual(original, deserialized);
        }

        [Fact]
        public void TestObjectMapLimits()
        {
            using var stream = new MemoryStream();
            var instances = new List<TypeWithTwoMembers<int, string>>();

            // We need enough items to exercise all sizes of ObjectRef
            for (int i = 0; i < ushort.MaxValue + 1; i++)
            {
                instances.Add(new TypeWithTwoMembers<int, string>(i, i.ToString()));
            }

            using (var writer = new ObjectWriter(stream, leaveOpen: true))
            {
                // Write each instance twice. The second time around, they'll become ObjectRefs
                for (int pass = 0; pass < 2; pass++)
                {
                    foreach (var instance in instances)
                    {
                        writer.WriteValue(instance);
                    }
                }
            }

            stream.Position = 0;
            using (var reader = ObjectReader.TryGetReader(stream, leaveOpen: true))
            {
                for (int pass = 0; pass < 2; pass++)
                {
                    foreach (var instance in instances)
                    {
                        var obj = reader.ReadValue();
                        Assert.NotNull(obj);
                        Assert.True(Equalish(obj, instance));
                    }
                }
            }
        }

        [Fact]
        public void TestObjectGraph()
        {
            var oneNode = new Node("one");
            TestRoundTripValue(oneNode);
            TestRoundTripValue(new Node("a", new Node("b"), new Node("c")));
            TestRoundTripValue(new Node("x", oneNode, oneNode, oneNode, oneNode));
        }

        [Fact]
        public void TestReuse()
        {
            var oneNode = new Node("one");
            var n1 = new Node("x", oneNode, oneNode, oneNode, oneNode);
            var n2 = RoundTripValue(n1, recursive: true);

            Assert.Same(n2.Children[0], n2.Children[1]);
            Assert.Same(n2.Children[1], n2.Children[2]);
            Assert.Same(n2.Children[2], n2.Children[3]);
        }

        [Fact]
        public void TestReuseNegative()
        {
            var oneNode = new Node("one", isReusable: false);
            var n1 = new Node("x", oneNode, oneNode, oneNode, oneNode);
            var n2 = RoundTripValue(n1, recursive: true);

            Assert.NotSame(n2.Children[0], n2.Children[1]);
            Assert.NotSame(n2.Children[1], n2.Children[2]);
            Assert.NotSame(n2.Children[2], n2.Children[3]);
        }

        [Fact]
        public void TestWideObjectGraph()
        {
            int id = 0;
            var graph = ConstructGraph(ref id, 5, 3);
            TestRoundTripValue(graph);
        }

        [Fact]
        public void TestDeepObjectGraph_RecursiveSucceeds()
        {
            int id = 0;
            var graph = ConstructGraph(ref id, 1, 1000);
            TestRoundTripValue(graph);
        }

        [Fact]
        public void TestDeepObjectGraph_NonRecursiveSucceeds()
        {
            int id = 0;
            var graph = ConstructGraph(ref id, 1, 1000);
            TestRoundTripValue(graph, recursive: false);
        }

        private Node ConstructGraph(ref int id, int width, int depth)
        {
            var name = "node" + (id++);

            Node[] children;

            if (depth > 0)
            {
                children = new Node[width];

                for (int i = 0; i < width; i++)
                {
                    children[i] = ConstructGraph(ref id, width, depth - 1);
                }
            }
            else
            {
                children = Array.Empty<Node>();
            }

            return new Node(name, children);
        }

        private class Node : IObjectWritable, IEquatable<Node>
        {
            internal readonly string Name;
            internal readonly Node[] Children;
            private readonly bool _isReusable = true;

            public Node(string name, params Node[] children)
            {
                this.Name = name;
                this.Children = children;
            }

            public Node(string name, bool isReusable)
                : this(name)
            {
                this._isReusable = isReusable;
            }

            private Node(ObjectReader reader)
            {
                this.Name = reader.ReadString();
                this.Children = (Node[])reader.ReadValue();
            }

            private static readonly Func<ObjectReader, object> s_createInstance = r => new Node(r);

            bool IObjectWritable.ShouldReuseInSerialization => _isReusable;

            public void WriteTo(ObjectWriter writer)
            {
                writer.WriteString(this.Name);
                writer.WriteValue(this.Children);
            }

            static Node()
            {
                ObjectBinder.RegisterTypeReader(typeof(Node), r => new Node(r));
            }

            public override Int32 GetHashCode()
            {
                return this.Name != null ? this.Name.GetHashCode() : 0;
            }

            public override Boolean Equals(Object obj)
            {
                return Equals(obj as Node);
            }

            public bool Equals(Node node)
            {
                if (node == null || this.Name != node.Name)
                {
                    return false;
                }

                if (this.Children.Length != node.Children.Length)
                {
                    return false;
                }

                for (int i = 0; i < this.Children.Length; i++)
                {
                    if (!this.Children[i].Equals(node.Children[i]))
                    {
                        return false;
                    }
                }

                return true;
            }
        }

        // keep these around for analyzing perf issues
#if false
        [Fact]
        public void TestReaderPerf()
        {
            var iterations = 10000;

            var recTime = TestReaderPerf(iterations, recursive: true);
            var nonTime = TestReaderPerf(iterations, recursive: false);

            Console.WriteLine($"Recursive Time    : {recTime.TotalMilliseconds}");
            Console.WriteLine($"Non Recursive Time: {nonTime.TotalMilliseconds}");
        }

        [Fact]
        public void TestNonRecursiveReaderPerf()
        {
            var iterations = 10000;
            var nonTime = TestReaderPerf(iterations, recursive: false);
        }

        private TimeSpan TestReaderPerf(int iterations, bool recursive)
        {
            int id = 0;
            var graph = ConstructGraph(ref id, 5, 3);

            var stream = new MemoryStream();
            var binder = new RecordingObjectBinder();
            var writer = new StreamObjectWriter(stream, binder: binder, recursive: recursive);

            writer.WriteValue(graph);
            writer.Dispose();

            var start = DateTime.Now;
            for (int i = 0; i < iterations; i++)
            {
                stream.Position = 0;
                var reader = new StreamObjectReader(stream, binder: binder);
                var item = reader.ReadValue();
            }

            return DateTime.Now - start;
        }
#endif
    }
}
