//
// Copyright (c) 2012-2021 Antmicro
//
// This file is licensed under the MIT License.
// Full license text is available in the LICENSE file.

using System;
using System.Threading;
using NUnit.Framework;
using System.Collections.Generic;
using System.Collections;
using System.Collections.Concurrent;
using System.Linq;
using System.IO;
using System.Collections.ObjectModel;
using Antmicro.Migrant.Hooks;

namespace Antmicro.Migrant.Tests
{
    [TestFixture(false, false, false, false, true)]
    [TestFixture(true, false, false, false, true)]
    [TestFixture(false, true, false, false, true)]
    [TestFixture(true, true, false, false, true)]
    [TestFixture(false, false, true, false, true)]
    [TestFixture(true, false, true, false, true)]
    [TestFixture(false, true, true, false, true)]
    [TestFixture(true, true, true, false, true)]
    [TestFixture(false, false, false, true, true)]
    [TestFixture(true, false, false, true, true)]
    [TestFixture(false, true, false, true, true)]
    [TestFixture(true, true, false, true, true)]
    [TestFixture(false, false, true, true, true)]
    [TestFixture(true, false, true, true, true)]
    [TestFixture(false, true, true, true, true)]
    [TestFixture(true, true, true, true, true)]
    [TestFixture(false, false, false, false, false)]
    [TestFixture(true, false, false, false, false)]
    [TestFixture(false, true, false, false, false)]
    [TestFixture(true, true, false, false, false)]
    [TestFixture(false, false, true, false, false)]
    [TestFixture(true, false, true, false, false)]
    [TestFixture(false, true, true, false, false)]
    [TestFixture(true, true, true, false, false)]
    [TestFixture(false, false, false, true, false)]
    [TestFixture(true, false, false, true, false)]
    [TestFixture(false, true, false, true, false)]
    [TestFixture(true, true, false, true, false)]
    [TestFixture(false, false, true, true, false)]
    [TestFixture(true, false, true, true, false)]
    [TestFixture(false, true, true, true, false)]
    [TestFixture(true, true, true, true, false)]
    public class SerializationTests : BaseTestWithSettings
    {
        public SerializationTests(bool useGeneratedSerializer, bool useGeneratedDeserializer, bool treatCollectionsAsUserObjects, bool supportForISerializable, bool useTypeStamping) : base(useGeneratedSerializer, useGeneratedDeserializer, treatCollectionsAsUserObjects, supportForISerializable, false, useTypeStamping, true)
        {
            useGeneratedSerialization = useGeneratedSerializer;
        }

        [Test]
        public void ShouldSerializeObject()
        {
            var obj = new object();
            var copy = SerializerClone(obj);
            Assert.AreEqual(copy.GetType(), typeof(object));
        }

        [Test]
        public void ShouldSerializeSimpleClass()
        {
            var simpleClass = new SimpleClass { Value = 0x69, Str = "Magic" };
            var copy = SerializerClone(simpleClass);
            Assert.AreEqual(simpleClass, copy);
        }

        [Test]
        public void ShouldSerializeCyclicReference()
        {
            var one = new CyclicRef();
            var another = new CyclicRef { Another = one, Val = 1 };
            one.Another = another;
            var oneCopy = SerializerClone(one);
            Assert.AreEqual(oneCopy, oneCopy.Another.Another, "Cyclic reference was not properly serialized.");
        }

        [Test]
        public void ShouldSerializeReferenceToSelf()
        {
            var one = new CyclicRef();
            one.Another = one;
            var copy = SerializerClone(one);
            Assert.AreSame(copy, copy.Another);
        }

        [Test]
        public void ShouldSerializeArrayReferenceToSelf()
        {
            var array = new object[2];
            array[0] = array;
            array[1] = array;
            var copy = SerializerClone(array);
            Assert.AreSame(copy, copy[0]);
            Assert.AreSame(copy[0], copy[1]);
        }

        [Test]
        public void ShouldSerializeNullReference()
        {
            var simpleClass = new SimpleClass();
            var copy = SerializerClone(simpleClass);
            Assert.AreEqual(simpleClass, copy);
        }

        [Test]
        public void ShouldPreserveIdentity()
        {
            var str = "This is a string";
            var simple1 = new SimpleClass { Str = str };
            var simple2 = new SimpleClass { Str = str };
            var container = new SimpleContainer { First = simple1, Second = simple2 };
            var copy = SerializerClone(container);
            Assert.AreSame(copy.First.Str, copy.Second.Str);
        }

        [Test]
        public void ShouldSerializeReadonlyField()
        {
            const int testValue = 0xABCD;
            var readonlyClass = new ClassWithReadonly(testValue);
            var copy = SerializerClone(readonlyClass);
            Assert.AreEqual(readonlyClass.ReadonlyValue, copy.ReadonlyValue);
        }

        [Test]
        public void ShouldNotSerializeIntPtr()
        {
            var intPtrClass = new ClassWithIntPtr{ Ptr = new IntPtr(0x666) };
            try
            {
                SerializerClone(intPtrClass);
                Assert.Fail("Class with IntPtr was serialized without exception.");
            }
            catch(InvalidOperationException)
            {

            }
        }

        [Test]
        public void ShouldNotSerializeSpinLock()
        {
            var spinLockClass = new ClassWithSpinLock{ SLock = new SpinLock() };
            try
            {
                SerializerClone(spinLockClass);
                Assert.Fail("Class with SpinLock was serialized without exception.");
            }
            catch(InvalidOperationException)
            {

            }
        }

        [Test]
        public void ShouldSerializeWithInheritance()
        {
            SimpleBaseClass derived = new SimpleDerivedClass { BaseField = 1, DerivedField = 2 };
            var copy = (SimpleDerivedClass)SerializerClone(derived);
            Assert.AreEqual(copy.DerivedField, 2);
            Assert.AreEqual(copy.BaseField, 1);
        }

        [Test]
        public void ShouldSerializeBoxWithLazyScan()
        {
            var str = "Something";
            var box = new Box { Element = str };
            var copy = SerializerClone(box);
            Assert.AreEqual(str, copy.Element);
        }

        [Test]
        public void ShouldSerializeFieldWithDifferentFormalAndActualType()
        {
            var box = new Box();
            var anotherBox = new Box { Element = box };
            var copy = SerializerClone(anotherBox);
            Assert.AreEqual(typeof(Box), copy.Element.GetType());
        }

        [Test]
        public void ShouldSerializeListOfPrimitives()
        {
            var list = new List<int> { 1, 2, 3 };
            var copy = SerializerClone(list);
            CollectionAssert.AreEqual(list, copy);
        }

        [Test]
        public void ShouldSerializeLongListOfPrimitives()
        {
            var list = new List<int>();
            for(var i = 0; i < 100000; i++)
            {
                list.Add(i);
            }
            var copy = SerializerClone(list);
            CollectionAssert.AreEqual(list, copy);
        }

        [Test]
        public void ShouldSerializeListOfLists()
        {
            var first = new List<int> { 1, 2, 3 };
            var second = new List<int> { 97, 98, 99 };
            var list = new List<List<int>> { first, second };
            var copy = SerializerClone(list);
            for(var i = 0; i < 2; i++)
            {
                CollectionAssert.AreEqual(list[i], copy[i]);
            }
        }

        [Test]
        public void ShouldPreserveIdentityOnLists()
        {
            var list = new List<Box>();
            var box1 = new Box { Element = list };
            var box2 = new Box { Element = list };
            list.Add(box1);
            list.Add(box2);

            var copy = SerializerClone(list);
            Assert.AreSame(copy, copy[0].Element);
            Assert.AreSame(copy[0].Element, copy[1].Element);
        }

        [Test]
        public void ShouldSerializeArrayListWithStrings()
        {
            var list = new ArrayList { "Word 1", "Word 2", new SimpleClass { Value = 6, Str = "Word 4" }, "Word 3" };
            var copy = SerializerClone(list);
            CollectionAssert.AreEqual(list, copy);
        }

        [Test]
        public void ShouldSerializeArrayListWithPrimitives()
        {
            var list = new ArrayList { 1, 2, 3 };
            var copy = SerializerClone(list);
            CollectionAssert.AreEqual(list, copy);
        }

        [Test]
        public void ShouldSerializeHashSetWithPrimitives()
        {
            var collection = new HashSet<int> { 1, 2, 3 };
            var copy = SerializerClone(collection);
            CollectionAssert.AreEquivalent(collection, copy);
        }

        [Test]
        public void ShouldSerializeReadOnlyCollection()
        {
            var list = new List<object> { "asdasd", 1, "cvzxcv" };
            var roList = new ReadOnlyCollection<object>(list);
            var copy = SerializerClone(roList);
            CollectionAssert.AreEqual(roList, copy);
        }

        [Test]
        public void ShouldSerializeBoxedPrimitve()
        {
            var box = new Box { Element = 3 };
            var copy = SerializerClone(box);
            Assert.AreEqual(3, copy.Element);
        }

        [Test]
        public void ShouldPreserveIdentityWithBoxedPrimitives()
        {
            object primitive = 1234;
            var box1 = new Box { Element = primitive };
            var box2 = new Box { Element = primitive };
            var list = new List<Box> { box1, box2 };
            var copy = SerializerClone(list);
            Assert.AreSame(copy[0].Element, copy[1].Element);
        }

        [Test]
        public void ShouldSerializeListWithNull()
        {
            var list = new List<object> { "One", null, "Two" };
            var copy = SerializerClone(list);
            CollectionAssert.AreEqual(list, copy);
        }

        [Test]
        public void ShouldSerializeArrayWithPrimitives()
        {
            var array = new [] { 1, 2, 3, 4, 5, 6 };
            var copy = SerializerClone(array);
            CollectionAssert.AreEqual(array, copy);
        }

        [Test]
        public void ShouldSerializeTwodimensionalArrayWithPrimitives()
        {
            var array = new [,] { { 1, 2 }, { 3, 4 }, { 5, 6 } };
            var copy = SerializerClone(array);
            CollectionAssert.AreEqual(array, copy);
        }

        [Test]
        public void ShouldSerializeArrayWithStrings()
        {
            var array = new [] { "One", "Two", "Three" };
            var copy = SerializerClone(array);
            CollectionAssert.AreEqual(array, copy);
        }

        [Test]
        public void ShouldSerializeTwodimensionalArrayWithStrings()
        {
            var array = new [,] { { "One", "Two" }, { "Three", "Four" } };
            var copy = SerializerClone(array);
            CollectionAssert.AreEqual(array, copy);
        }

        [Test]
        public void ShouldSerialize4DArrayWithPrimitives()
        {
            var array = new int[2, 2, 3, 4];
            array[0, 0, 0, 0] = 1;
            array[1, 0, 1, 0] = 2;
            array[0, 1, 0, 1] = 3;
            array[0, 1, 2, 2] = 4;
            array[1, 0, 2, 3] = 5;
            var copy = SerializerClone(array);
            Assert.AreEqual(array, copy);
        }

        [Test]
        public void ShouldSerializeMixedArray()
        {
            var obj = new object();
            var array = new [] { "One", obj, "Two", obj, new object() };
            var copy = SerializerClone(array);
            CollectionAssert.AreEqual(array.Where(x => x is String), copy.Where(x => x is String));
            Assert.AreSame(copy[1], copy[3]);
            Assert.AreNotSame(copy[3], copy[4]);
        }

        [Test]
        public void ShouldSerializeDictionaryWithPrimitives()
        {
            var dictionary = new Dictionary<int, int>();
            for(var i = 0; i < 100; i++)
            {
                dictionary.Add(i, i + 1);
            }
            var copy = SerializerClone(dictionary);
            CollectionAssert.AreEquivalent(dictionary, copy);
        }

        [Test]
        public void ShouldSerializeHashtable()
        {
            var hashtable = new Hashtable();
            for(var i = 0; i < 100; i++)
            {
                hashtable.Add(i, i + 1);
            }
            var copy = SerializerClone(hashtable);
            CollectionAssert.AreEquivalent(hashtable, copy);
        }

        [Test]
        public void ShouldSerializeDictionaryWithStrings()
        {
            var dictionary = new Dictionary<string, string> {
                { "Ball", "Piłka" },
                { "Cat", "Kot" },
                { "Line", "Linia" }
            };
            var copy = SerializerClone(dictionary);
            CollectionAssert.AreEquivalent(dictionary, copy);
        }

        [Test]
        public void ShouldPreserveIdentityWithDictionary()
        {
            var str = "Word";
            var dictionary = new Dictionary<int, string> {
                { 10, str },
                { 20, "One" },
                { 30, str }
            };
            var copy = SerializerClone(dictionary);
            CollectionAssert.AreEquivalent(dictionary, copy);
            Assert.AreSame(copy[10], copy[30]);
            Assert.AreNotSame(copy[10], copy[20]);
        }

        [Test]
        public void ShouldSerializeSimpleEnum()
        {
            var e = SimpleEnum.Two;
            var copy = SerializerClone(e);
            Assert.AreEqual(e, copy);
        }

        [Test]
        public void ShouldSerializeEnums()
        {
            var enumLongValues = Enum.GetValues(typeof(EnumLong)).Cast<EnumLong>().ToArray();
            var enumShortValues = Enum.GetValues(typeof(EnumShort)).Cast<EnumShort>().ToArray();
            var enumFlagsValues = Enum.GetValues(typeof(TestEnumFlags)).Cast<TestEnumFlags>()
            .Union(new [] {
                TestEnumFlags.First | TestEnumFlags.Second
                , TestEnumFlags.First | TestEnumFlags.Second | TestEnumFlags.Third
                , TestEnumFlags.Second | TestEnumFlags.Third
            }).ToArray();

            foreach(var item in enumLongValues)
            {
                Assert.AreEqual(item, SerializerClone(item));
            }
            foreach(var item in enumShortValues)
            {
                Assert.AreEqual(item, SerializerClone(item));
            }
            foreach(var item in enumFlagsValues)
            {
                Assert.AreEqual(item, SerializerClone(item));
            }
        }

        [Test]
        public void ShouldSerializeSimplerStruct()
        {
            var str = new SimplerStruct { A = 1234567, B = 543 };
            var copy = SerializerClone(str);
            Assert.AreEqual(str, copy);
        }

        [Test]
        public void ShouldSerializeSimpleStruct()
        {
            var str = new SimpleStruct {
                A = 5,
                B = "allman"
            };
            var copy = SerializerClone(str);
            Assert.AreEqual(str, copy);
        }

        [Test]
        public void ShouldSerializeComplexStruct()
        {
            var str = new SimpleStruct {
                A = 5,
                B = "allman"
            };

            var compstr = new ComplexStruct {
                A = 6,
                B = str
            };
            var newStr = SerializerClone(compstr);

            Assert.AreEqual(compstr, newStr);
        }

        [Test]
        public void ShouldSerializeStructInClass()
        {
            var str = new SimpleStruct { A = 1, B = "allman" };
            var box = new GenericBox<SimpleStruct> { Element = str };
            var copy = SerializerClone(box);
            Assert.AreEqual(str, copy.Element);
        }

        [Test]
        public void ShouldSerializeSpeciallySerializable()
        {
            var special = new SpeciallySerializable();
            special.Fill(100);
            var copy = SerializerClone(special);
            CollectionAssert.AreEqual(special.Data, copy.Data);
        }

        [Test]
        public void ShouldSerializeSpeciallySerializableInArrayAndPreserveIdentity()
        {
            var special = new SpeciallySerializable();
            special.Fill(100);
            var str = "Some string";
            var box = new Box { Element = special };
            var anotherSpecial = new SpeciallySerializable();
            anotherSpecial.Fill(100);
            var array = new object[] { special, str, box, anotherSpecial };
            var copy = SerializerClone(array);
            Assert.AreSame(copy[0], ((Box)copy[2]).Element);
            Assert.AreNotSame(copy[0], copy[3]);
            CollectionAssert.AreEqual(((SpeciallySerializable)copy[0]).Data, special.Data);
            CollectionAssert.AreEqual(((SpeciallySerializable)copy[3]).Data, anotherSpecial.Data);
        }

        [Test]
        public void ShouldSerializeBoxWithLong()
        {
            var box = new GenericBox<long> { Element = 1234 };
            var copy = SerializerClone(box);
            Assert.AreEqual(box.Element, copy.Element);
        }

        [Test]
        public void ShouldSerializeStructWithSpeciallySerializable()
        {
            var special = new SpeciallySerializable();
            special.Fill(100);
            var box = new StructGenericBox<SpeciallySerializable> { Element = special };
            var copy = SerializerClone(box);
            Assert.AreEqual(box.Element.Data, copy.Element.Data);
        }

        [Test]
        public void ShouldSerializeNonNullNullable()
        {
            var box = new GenericBox<int?> { Element = 3 };
            var copy = SerializerClone(box);
            Assert.AreEqual(box.Element, copy.Element);
        }

        [Test]
        public void ShouldSerializeNullNullable()
        {
            var box = new GenericBox<int?> { Element = null };
            var copy = SerializerClone(box);
            Assert.AreEqual(box.Element, copy.Element);
        }

        [Test]
        public void ShouldSerializeBoxedNullNullable()
        {
            int? value = 66;
            var box = new Box { Element = value };
            var copy = SerializerClone(box);
            Assert.AreEqual(box.Element, copy.Element);
        }

        [Test]
        public void ShouldSerializeBoxedNonNullNullable()
        {
            int? value = null;
            var box = new Box { Element = value };
            var copy = SerializerClone(box);
            Assert.AreEqual(box.Element, copy.Element);
        }

        [Test]
        public void ShouldSerializeConcurrentDictionary()
        {
            var dictionary = new ConcurrentDictionary<int, int>();
            for(var i = 0; i < 10; i++)
            {
                dictionary.TryAdd(i, 2 * i);
            }
            var copy = SerializerClone(dictionary);
            CollectionAssert.AreEquivalent(dictionary, copy);
        }

        [Test]
        public void ShouldHandleTwoSerializations()
        {
            var someObjects = new object[] { "One", 2, null, "Four" };
            var stream = new MemoryStream();
            var serializer = new Serializer(GetSettings());
            serializer.Serialize(someObjects, stream);
            serializer.Serialize(someObjects, stream);
            stream.Seek(0, SeekOrigin.Begin);
            var copy = serializer.Deserialize<object[]>(stream);
            CollectionAssert.AreEqual(someObjects, copy);
            copy = serializer.Deserialize<object[]>(stream);
            CollectionAssert.AreEqual(someObjects, copy);
        }

        [Test]
        public void ShouldDetectStreamShift()
        {
            var errorneousObject = new BadlySerializable();
            try
            {
                SerializerClone(errorneousObject);
                Assert.Fail("Exception was not thrown despite stream corruption.");
            }
            catch(InvalidOperationException)
            {
            }
        }

        [Test]
        public void ShouldSerializeTuple()
        {
            var tuple = Tuple.Create("One", "Two", "Three");
            var copy = SerializerClone(tuple);
            Assert.AreEqual(tuple, copy);
        }

        [Test]
        public void ShouldInvokeConstructorForFieldInStruct()
        {
            var withCtor = new StructWithConstructorAttribute();
            var copy = SerializerClone(withCtor);
            Assert.IsTrue(copy.IsFieldConstructed, "[Constructor] marked field was not initialized.");
        }

        [Test]
        public void ShouldInvokeConstructorForFieldInStructComplex()
        {
            var withCtor = new StructWithConstructorAttributeOnNestedStruct();
            var copy = SerializerClone(withCtor);
            Assert.IsTrue(copy.IsFieldConstructed, "[Constructor] marked field was not initialized.");
        }

        [Test]
        public void ShouldInvokeConstructorForField()
        {
            var withCtor = new WithConstructorAttribute();
            var copy = SerializerClone(withCtor);
            Assert.IsTrue(copy.IsFieldConstructed, "[Constructor] marked field was not initialized.");
        }

        [Test]
        public void ShouldOmitTransientClass()
        {
            SerializerClone(new Box { Element = new TransientClass() });
        }

        [Test]
        public void ShouldOmitTransientField()
        {
            var trans = new ClassWithTransientField() { a = 147, b = 256, c = 850 };
            var scd = SerializerClone(trans);

            Assert.AreEqual(trans.a, scd.a);
            Assert.AreEqual(default(int), scd.b);
            Assert.AreEqual(trans.c, scd.c);
        }

        [Test]
        public void ShouldSerializeQueue()
        {
            var queue = new Queue<int>();
            queue.Enqueue(1);
            queue.Enqueue(3);
            queue.Enqueue(5);
            var copy = SerializerClone(queue);
            CollectionAssert.AreEqual(queue, copy);
        }

        [Test]
        public void ShouldSerializeByteArray()
        {
            var array = new byte[] { 2, 4, 6, 8, 10 };
            var copy = SerializerClone(array);
            CollectionAssert.AreEqual(array, copy);
        }

        [Test]
        public void ShouldSerializeEvent()
        {
            var withEvent = new ClassWithEvent();
            var companion = new CompanionToClassWithEvent();
            withEvent.Event += companion.Method;
            var pair = Tuple.Create(withEvent, companion);

            var copy = SerializerClone(pair);
            copy.Item1.Invoke();
            Assert.AreEqual(1, copy.Item2.Counter);
        }

        [Test]
        public void ShouldSerializeMulticastEvent()
        {
            var withEvent = new ClassWithEvent();
            var companion1 = new CompanionToClassWithEvent();
            withEvent.Event += companion1.Method;
            var companion2 = new CompanionToClassWithEvent();
            withEvent.Event += companion2.Method;
            var triple = Tuple.Create(withEvent, companion1, companion2);

            var copy = SerializerClone(triple);
            copy.Item1.Invoke();
            Assert.AreEqual(1, copy.Item2.Counter);
            Assert.AreEqual(1, copy.Item3.Counter);
        }

        [Test]
        public void ShouldSerializeDelegateWithTargetFromDifferentModule()
        {
            var withEvent = new ClassWithEvent();
            var companion = new CompanionSecondModule();
            withEvent.Event += companion.MethodAsExtension;
            var pair = Tuple.Create(withEvent, companion);

            var copy = SerializerClone(pair);
            copy.Item1.Invoke();
            Assert.AreEqual(1, copy.Item2.Counter);
        }

        [Test]
        public void ShouldSerializeDelegateWithInterfaceMethod()
        {
            var withEvent = new ClassWithEvent<IInterfaceForDelegate>();
            var companion = new ClassImplementingInterfaceForDelegate();
            withEvent.Event += companion.Method;
            var pair = Tuple.Create(withEvent, companion);

            var copy = SerializerClone(pair);
            copy.Item1.Invoke(null);
            Assert.AreEqual(true, copy.Item2.Invoked);
        }

        [Test]
        public void ShouldSerializeDelegateWithLambdaAttached()
        {
            var withEvent = new ClassWithEvent();
            var companion = new ClassImplementingInterfaceForDelegate();
            withEvent.Event += () => companion.Method(null);
            var pair = Tuple.Create(withEvent, companion);

            var copy = SerializerClone(pair);
            copy.Item1.Invoke();
            Assert.AreEqual(true, copy.Item2.Invoked);
        }

        [Test]
        public void ShouldSerializeEmptyArray()
        {
            var emptyArray = new int[0];
            var copy = SerializerClone(emptyArray);
            CollectionAssert.AreEqual(emptyArray, copy);
        }

        [Test]
        public void ShouldSerializeEmptyList()
        {
            var emptyList = new List<int>();
            var copy = SerializerClone(emptyList);
            CollectionAssert.AreEqual(emptyList, copy);
        }

        [Test]
        public void ShouldSerializeByteEnum()
        {
            var byteEnum = EnumByte.Two;
            var copy = SerializerClone(byteEnum);
            Assert.AreEqual(byteEnum, copy);
        }

        [Test]
        public void ShouldThrowOnThreadLocalSerialization()
        {
            var classWithThreadLocal = new ClassWithThreadLocal();
            Assert.Throws(typeof(InvalidOperationException), () => SerializerClone(classWithThreadLocal));
        }

        [Test]
        public void ShouldOmitDelegateWithTransientTarget()
        {
            var withEvent = new ClassWithEvent();
            var target1 = new TransientClassWithMethod();
            var target2 = new CompanionToClassWithEvent();
            withEvent.Event += target2.Method;
            withEvent.Event += target1.Method;
            var copy = SerializerClone(withEvent);
            copy.Invoke();
        }

        [Test]
        public void ShouldSerializeEmptyDelegateList()
        {
            var withEvent = new ClassWithEvent();
            var target = new TransientClassWithMethod();
            withEvent.Event += target.Method;
            SerializerClone(withEvent);
        }

        [Test]
        public void TransientDerivedShouldAlsoBeTransient()
        {
            var transientDerived = new TransientDerived();
            var values = new object[] { 0, transientDerived };
            var copy = SerializerClone(values);
            CollectionAssert.AreEqual(new object[] { 0, null }, copy);
        }

        [Test]
        public void ShouldSerializeNongenericStack()
        {
            var stack = new Stack();

            var test = "It works!";
            foreach(var chr in test)
            {
                stack.Push(chr);
            }
            var copy = SerializerClone(stack);
            CollectionAssert.AreEqual(stack, copy);


        }

        [Test]
        public void ShouldSerializeGenericStack()
        {
            var stack = new Stack<char>();

            var test = "It works!";
            foreach(var chr in test)
            {
                stack.Push(chr);
            }
            var copy = SerializerClone(stack);
            CollectionAssert.AreEqual(stack, copy);
        }

        [Test]
        public void ShouldSerializeBoxWithGuid()
        {
            var box = new GenericBox<Guid>();
            box.Element = Guid.NewGuid();
            var copy = SerializerClone(box);
            Assert.AreEqual(box.Element, copy.Element);
        }

        [Test]
        public void ShouldSerializeClassWithGuid()
        {
            var withGuid = new ClassWithGuid();
            var copy = SerializerClone(withGuid);
            Assert.AreEqual(withGuid, copy);
        }

        [Test]
        public void ShouldFindPathToPointerContainingObject()
        {
            Exception exception = null;
            var toClone = new GenericBox<AnotherContainingType>();
            toClone.Element = new AnotherContainingType();
            try
            {
                SerializerClone(toClone);
                Assert.Fail("Exception was not thrown.");
            }
            catch(Exception e)
            {
                exception = e;
            }
            if(!useGeneratedSerialization)
            {
                Assert.IsTrue(exception.Message.Contains(toClone.GetType().Name));
                Assert.IsTrue(exception.Message.Contains(toClone.Element.GetType().Name));
                Assert.IsTrue(exception.Message.Contains(toClone.Element.WithIntPtr.GetType().Name));
            }
        }

        [Test]
        public void GithubIssue60()
        {
            object instance = new Issue60OuterClass() {
                Field1 = 0,
                Inner = new Issue60InnerClass() {
                    Field2 = 1,
                    Field3 = 2,
                }
            };

            SerializerClone(instance);
        }

        [Test]
        public void ShouldSerializeKeyValuePair()
        {
            var src = new KeyValuePair<int, int>(12, 34);
            var dst = SerializerClone(src);

            Assert.AreEqual(src.Key, dst.Key);
            Assert.AreEqual(src.Value, dst.Value);
        }

        [Test]
        public void ShouldSerializeNullableEnum()
        {
            var src = new ClassWithNullableEnum() { e = SimpleEnum.Two };
            var dst = SerializerClone(src);

            Assert.AreEqual(SimpleEnum.Two, dst.e);
        }

        [Test]
        public void ShouldSerializeGenericClassWithLatePostDeserializationHook()
        {
            var src = new GenericClassWithLatePostDeserializationHook { x = 150 };
            var dst = SerializerClone(src);

            Assert.AreEqual(147, dst.x);
        }

        [Test]
        public void ShouldSerialize2DArrayWithAllDimensionZero()
        {
            var array = new int[0, 0];
            var copy = SerializerClone(array);
            CollectionAssert.AreEqual(array, copy);
        }

        [Test]
        public void ShouldSerialize3DArrayWithOneDimensionZero()
        {
            var array = new int[2, 0, 2];
            var copy = SerializerClone(array);
            CollectionAssert.AreEqual(array, copy);
        }

        [Test]
        public void ShouldNotSerializePointer()
        {
            if(Environment.OSVersion.Platform == PlatformID.Unix && Environment.OSVersion.Version < Version.Parse("4.0"))
            {
                Assert.Inconclusive("Due to a bug in mono implementation this test is ignored on Linux");
            }

            var classWithPtr = new ClassWithPointer();
            try
            {
                SerializerClone(classWithPtr);
                Assert.Fail("A class with pointer was serialized, but it should have been not.");
            }
            catch(InvalidOperationException)
            {

            }
        }

        [Test]
        public void ShouldNotSerializeNull()
        {
            try
            {
                SerializerClone<object>(null);
                Assert.Fail("Serialization of null succeeded while it should not.");
            }
            catch(ArgumentException)
            {
            }
        }

        [Test]
        public void ShouldNotSerializeTransient()
        {
            try
            {
                SerializerClone(new TransientClass());
                Assert.Fail("Serialization of a transient object succeeded while it should not.");
            }
            catch(ArgumentException)
            {
            }
        }

        [Test]
        public void ShouldSerializeTwoTimesDifferentObjects()
        {
            var serializer = new Serializer(GetSettings());

            using(var stream = new MemoryStream())
            {
                var obj = new SimpleContainer();
                obj.First = new SimpleClass();
                obj.Second = new SimpleClass();
                serializer.Serialize(obj, stream);
                stream.Seek(0, SeekOrigin.Begin);
                var copy = serializer.Deserialize<SimpleContainer>(stream);
                Assert.IsNotNull(copy);
            }

            using(var stream = new MemoryStream())
            {
                var obj = new object[] { new SimpleClass(), new SimpleContainer(), new SimpleContainer() };
                serializer.Serialize(obj, stream);
                stream.Seek(0, SeekOrigin.Begin);
                var copy = serializer.Deserialize<object[]>(stream);
                Assert.IsNotNull(copy);
                Assert.IsInstanceOf<SimpleClass>(copy[0]);
                Assert.IsInstanceOf<SimpleContainer>(copy[1]);
                Assert.IsInstanceOf<SimpleContainer>(copy[2]);
            }
        }

        [Test]
        public void ShouldSerializeClassWithGenericMethodDelegate()
        {
            var serializer = new Serializer(GetSettings());

            var obj = new GenericClassWithGenericDelegate<List<bool>>();
            using(var stream = new MemoryStream())
            {
                serializer.Serialize(obj, stream);
                stream.Seek(0, SeekOrigin.Begin);
                var copy = serializer.Deserialize<GenericClassWithGenericDelegate<List<bool>>>(stream);
                Assert.IsNotNull(copy);
            }
        }

        [Test]
        public void ShouldSerializeClassWithArrayOfNestedGenericArguments()
        {
            var serializer = new Serializer(GetSettings());

            var obj = new GenericClassWithNestedGenericArgument<string>("test");
            using(var stream = new MemoryStream())
            {
                serializer.Serialize(obj, stream);
                stream.Seek(0, SeekOrigin.Begin);
                var copy = serializer.Deserialize<GenericClassWithNestedGenericArgument<string>>(stream);
                Assert.IsNotNull(copy);
                Assert.AreEqual("test", copy.field[0].field);
            }
        }

        [Test]
        public void ShouldSerializeDeepGraph()
        {
            var obj = new ClassWithReferenceToTheObjectOfTheSameType();
            var current = obj;
            for(int i = 0; i < 45000; i++)
            {
                current.reference = new ClassWithReferenceToTheObjectOfTheSameType();
                current = current.reference;
            }

            var serializer = new Serializer(GetSettings());

            ClassWithReferenceToTheObjectOfTheSameType copy;
            using(var stream = new MemoryStream())
            {
                serializer.Serialize(obj, stream);
                stream.Seek(0, SeekOrigin.Begin);
                copy = serializer.Deserialize<ClassWithReferenceToTheObjectOfTheSameType>(stream);
            }

            Assert.IsNotNull(copy);
            var depth = 0;
            current = copy;
            while(current.reference != null)
            {
                depth++;
                current = current.reference;
            }

             Assert.AreEqual(45000, depth);
        }

        [Test]
        public void ShouldSerializeListOfInterfaces()
        {
            var serializer = new Serializer(GetSettings());

            var obj = new ClassWithTheListOfInterfaces();
            obj.list = new List<IInterface>();
            for(int i = 0; i < 100; i++)
            {
                obj.list.Add(i % 2 == 0 ? (IInterface)new ImplementingInterfaceOne() : (IInterface)new ImplementingInterfaceTwo());
            }
            using(var stream = new MemoryStream())
            {
                serializer.Serialize(obj, stream);
                stream.Seek(0, SeekOrigin.Begin);
                var copy = serializer.Deserialize<ClassWithTheListOfInterfaces>(stream);
                Assert.IsNotNull(copy);
                Assert.AreEqual(100, copy.list.Count);
            }
        }

        [Test]
        public void ShouldHandleEventWithBackreference()
        {
            var machine = new BoxingClass();
            var copy = SerializerClone(machine);

            Assert.IsNotNull(copy);
            for(var i = 0; i < copy.objectWithItems.items.Length; i++)
            {
                Assert.IsFalse(copy.objectWithItems.items [i].ActionIsNull, string.Format("Array element: {0}", i));
            }
        }

        [Test]
        public void ShouldSerializeDictionaryWithCustomObject()
        {
            var dic = new Dictionary<CustomObject, string>();
            dic.Add(new CustomObject(new object()), "test1");
            dic.Add(new CustomObject(new object()), "test2");
            dic.Add(new CustomObject(new object()), "test3");

            var copy = SerializerClone(dic);

            Assert.AreEqual(3, copy.Count);
        }

        [Test]
        public void ShouldSerializeHashsetWithCustomObject()
        {
            var hset = new HashSet<CustomObject>();
            hset.Add(new CustomObject(new object()));
            hset.Add(new CustomObject(new object()));
            hset.Add(new CustomObject(new object()));

            var copy = SerializerClone(hset);
            Assert.AreEqual(3, copy.Count);
        }

        private class CustomObject
        {
            public CustomObject(object field)
            {
                this.field = field;
            }

            public override int GetHashCode()
            {
                return field.GetHashCode();
            }

            private object field;
        }

        private class BoxingClass
        {
            // it is necessary for fields to be serialized in proper order
            // to detect a problem;
            // as fileds are ordered alphabetically, please be
            // carefull while renaming them
            public ClassWithAction objectWithAction;
            public ClassWithItems objectWithItems;

            public BoxingClass()
            {
                objectWithItems = new ClassWithItems(this);
            }
        }

        private class ClassWithItems
        {
            public ClassWithItems(BoxingClass mach)
            {
                items = new ClassWithAction[2];
                for(var i = 0; i < items.Length; i++)
                {
                    items[i] = new ClassWithAction(mach);
                    // passing 'i' variable to the method is crucial here
                    items[i].Action += () => Method(i);
                }
            }

            private void Method(int i) { }

            public ClassWithAction[] items;
        }

        private class ClassWithAction
        {
            public ClassWithAction(BoxingClass box)
            {
                box.objectWithAction = this;
            }

            public event Action Action;

            public bool ActionIsNull
            {
                get { return Action == null; }
            }
        }

        public class ClassWithTheListOfInterfaces
        {
            public List<IInterface> list;
        }

        public interface IInterface
        {
            void Method();
        }

        public class ImplementingInterfaceOne : IInterface
        {
            public void Method() {}
        }

        public class ImplementingInterfaceTwo : IInterface
        {
            public void Method() {}
        }

        public class ClassWithReferenceToTheObjectOfTheSameType
        {
            public ClassWithReferenceToTheObjectOfTheSameType reference;
        }

        public class GenericClassWithNestedGenericArgument<T>
        {
            public GenericClassWithNestedGenericArgument(T t)
            {
                field = new [] { new GenericClass<T>() } ;
                field[0].field = t;
            }

            public GenericClass<T>[] field;
        }

        public class GenericClass<T>
        {
            public T field;
        }

        public class GenericClassWithGenericDelegate<T>
        {
            public GenericClassWithGenericDelegate()
            {
                del = Action;
            }

            public void CallDelegate(T c)
            {
                del(c, "string");
            }

            private void Action<TInner>(TInner x, string y)
            {
            }

            private readonly Action<T, string> del;
        }

        public class GenericClassWithTransient<T>
        {
            [Transient]
            public T x;
        }

        public class GenericClassWithLatePostDeserializationHook : GenericClassWithTransient<int>
        {
            [LatePostDeserialization]
            public void LatePostDeserialization()
            {
                if(x == 0)
                {
                    x = 147;
                }
            }
        }

        public class ClassWithNullableEnum
        {
            public SimpleEnum? e { get; set; }
        }

        public class SimpleClass
        {
            public int Value { get; set; }

            public string Str { get; set; }

            public override bool Equals(object obj)
            {
                if(obj == null)
                {
                    return false;
                }
                if(ReferenceEquals(this, obj))
                {
                    return true;
                }
                if(obj.GetType() != typeof(SimpleClass))
                {
                    return false;
                }
                var other = (SimpleClass)obj;
                return Value == other.Value && Str == other.Str;
            }

            public override int GetHashCode()
            {
                unchecked
                {
                    return Value.GetHashCode() ^ 33 * (Str != null ? Str.GetHashCode() : 0);
                }
            }

            public override string ToString()
            {
                return string.Format("[SimpleClass: Value={0}, Str={1}]", Value, Str);
            }
        }

        public class SimpleContainer
        {
            public SimpleClass First { get; set; }

            public SimpleClass Second { get; set; }
        }

        public class CyclicRef
        {
            public CyclicRef Another { get; set; }

            public int Val { get; set; }
        }

        public class ClassWithReadonly
        {
            public ClassWithReadonly(int readonlyValue)
            {
                privateReadonlyField = readonlyValue;
            }

            public int ReadonlyValue
            {
                get
                {
                    return privateReadonlyField;
                }
            }

            private readonly int privateReadonlyField;
        }

        public class ClassWithIntPtr
        {
            public IntPtr Ptr { get; set; }
        }

        public class ClassWithSpinLock
        {
            public SpinLock SLock { get; set; }
        }

        public class SimpleBaseClass
        {
            public int BaseField { get; set; }
        }

        public class SimpleDerivedClass : SimpleBaseClass
        {
            public int DerivedField { get; set; }
        }

        public class Box
        {
            public object Element { get; set; }
        }

        public sealed class GenericBox<T>
        {
            public T Element { get; set; }

            public override bool Equals(object obj)
            {
                if(obj == null)
                {
                    return false;
                }
                if(ReferenceEquals(this, obj))
                {
                    return true;
                }
                if(obj.GetType() != typeof(GenericBox<T>))
                {
                    return false;
                }
                var other = (GenericBox<T>)obj;
                if(Element != null)
                {
                    return Element.Equals(other.Element);
                }
                return other.Element == null;
            }

            public override int GetHashCode()
            {
                unchecked
                {
                    return (Element != null ? Element.GetHashCode() : 0);
                }
            }
        }

        public struct StructGenericBox<T>
        {
            public T Element;
        }

        public enum SimpleEnum
        {
            One,
            Two,
            Three
        }

        private enum EnumLong : long
        {
            First = -1,
            Second = long.MaxValue - 12345,
            Third
        }

        private enum EnumShort : short
        {
            First = short.MinValue + 2,
            Second = 6,
            Third
        }

        private enum EnumByte : byte
        {
            One = 1,
            Two = 200
        }

        [Flags]
        private enum TestEnumFlags
        {
            First = 1,
            Second = 2,
            Third = 4
        }

        private struct SimplerStruct
        {
            public int A;
            public long B;

            public override string ToString()
            {
                return string.Format("[SimplerStruct: A={0}, B={1}]", A, B);
            }
        }

        private struct SimpleStruct
        {
            public int A;
            public String B;

            public override string ToString()
            {
                return string.Format("[SimpleStruct: A={0}, B={1}]", A, B);
            }
        }

        private struct ComplexStruct
        {
            public int A;
            public SimpleStruct B;

            public override string ToString()
            {
                return string.Format("[ComplexStruct: A={0}, B={1}]", A, B);
            }
        }

        private class SpeciallySerializable : ISpeciallySerializable
        {
            public void Fill(int length)
            {
                var bytes = new byte[length];
                Helpers.Random.NextBytes(bytes);
                data = bytes.Select(x => new IntPtr(x)).ToArray();
            }

            public byte[] Data
            {
                get
                {
                    return data.Select(x => (byte)x).ToArray();
                }
            }

            public void Load(PrimitiveReader reader)
            {
                var length = reader.ReadInt32();
                data = new IntPtr[length];
                for(var i = 0; i < length; i++)
                {
                    data[i] = new IntPtr(reader.ReadByte());
                }
            }

            public void Save(PrimitiveWriter writer)
            {
                writer.Write(data.Length);
                for(var i = 0; i < data.Length; i++)
                {
                    writer.Write((byte)data[i]);
                }
            }

            private IntPtr[] data;
        }

        private class BadlySerializable : ISpeciallySerializable
        {
            public void Load(PrimitiveReader reader)
            {
                reader.ReadInt32();
                reader.ReadInt32();
            }

            public void Save(PrimitiveWriter writer)
            {
                for(var i = 0; i < 3; i++)
                {
                    writer.Write(666 + i);
                }
            }
        }

        private struct StructWithConstructorAttribute
        {
            public bool IsFieldConstructed
            {
                get
                {
                    return resetEvent != null;
                }
            }

            #pragma warning disable 649
            [Constructor(false)]
            private ManualResetEvent resetEvent;
            #pragma warning restore 649
        }

        private struct StructWithConstructorAttributeOnNestedStruct
        {
            public bool IsFieldConstructed
            {
                get
                {
                    return s.field == 5;
                }
            }

            #pragma warning disable 649
            [ConstructorAttribute(true)]
            private NestedStruct s;
            #pragma warning restore 649
        }

        private struct NestedStruct
        {
            public NestedStruct(bool condition)
            {
                field = (condition ? 5 : 0);
            }

            #pragma warning disable 649
            public int field;
            #pragma warning restore 649
        }

        private class WithConstructorAttribute
        {
            public bool IsFieldConstructed
            {
                get
                {
                    return resetEvent != null;
                }
            }

            #pragma warning disable 649
            [Constructor(false)]
            private ManualResetEvent resetEvent;
            #pragma warning restore 649
        }

        [Transient]
        private class TransientClass
        {
            public IntPtr Pointer { get; set; }
        }

        private class TransientDerived : TransientClass
        {
            public int Integer { get; set; }
        }

        private class ClassWithTransientField
        {
            public int a;
            [Transient]
            public int
                b;
            public int c;
        }

        private class ClassWithEvent
        {
            public event Action Event;

            public void Invoke()
            {
                var toInvoke = Event;
                if(toInvoke != null)
                {
                    toInvoke();
                }
            }
        }

        private class CompanionToClassWithEvent
        {
            public int Counter { get; set; }

            public void Method()
            {
                Counter++;
            }
        }

        private class ClassWithThreadLocal
        {
            public ClassWithThreadLocal()
            {
                ThreadLocal = new ThreadLocal<int>();
            }

            public ThreadLocal<int> ThreadLocal { get; set; }
        }

        [Transient]
        private class TransientClassWithMethod
        {
            public TransientClassWithMethod()
            {
                a = 1;
            }

            public void Method()
            {
                a++;
            }

            private int a;
        }

        public class Issue60OuterClass
        {
            public decimal Field1;

            public Issue60InnerClass Inner { get; set; }
        }

        public class Issue60InnerClass
        {
            public decimal? Field2;
            public decimal? Field3;
        }

        public class ClassWithGuid
        {
            public ClassWithGuid()
            {
                Id = Guid.NewGuid();
                Number = Id.ToByteArray()[1] * Id.ToByteArray()[0];
                Str = Helpers.GetRandomString(Number / 256);
            }

            public override bool Equals(object obj)
            {
                if(obj == null)
                {
                    return false;
                }
                if(ReferenceEquals(this, obj))
                {
                    return true;
                }
                if(obj.GetType() != typeof(ClassWithGuid))
                {
                    return false;
                }
                ClassWithGuid other = (ClassWithGuid)obj;
                return Id == other.Id && Number == other.Number && Str == other.Str;
            }

            public override int GetHashCode()
            {
                unchecked
                {
                    return Id.GetHashCode() ^ Number.GetHashCode() ^ (Str != null ? Str.GetHashCode() : 0);
                }
            }

            public override string ToString()
            {
                return string.Format("[ClassWithGuid: Id={0}, Number={1}, Str={2}]", Id, Number, Str);
            }

            public Guid Id { get; private set; }

            public int Number { get; private set; }

            public string Str { get; private set; }
        }

        public class AnotherContainingType
        {
            public AnotherContainingType()
            {
                WithIntPtr = new ClassWithIntPtr();
            }

            public ClassWithIntPtr WithIntPtr { get; private set; }
        }

        public unsafe class ClassWithPointer
        {
            protected int* Pointer;
        }

        private class ClassWithEvent<T>
        {
            public event Action<T> Event;

            public void Invoke(T arg)
            {
                var toInvoke = Event;
                if(toInvoke != null)
                {
                    toInvoke(arg);
                }
            }
        }

        private interface IInterfaceForDelegate
        {
            void Method(IInterfaceForDelegate arg);
        }

        private class ClassImplementingInterfaceForDelegate : IInterfaceForDelegate
        {
            public bool Invoked { get; private set; }

            public void Method(IInterfaceForDelegate arg)
            {
                Invoked = true;
            }
        }

        private readonly bool useGeneratedSerialization;
    }

    public static class CompanionExtensions
    {
        public static void MethodAsExtension(this CompanionSecondModule companion)
        {
            companion.Counter++;
        }
    }
}
