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

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

namespace Antmicro.Migrant.Tests
{
    [TestFixture(false, false, true)]
    [TestFixture(true, false, true)]
    [TestFixture(false, true, true)]
    [TestFixture(true, true, true)]

    [TestFixture(false, false, false)]
    [TestFixture(true, false, false)]
    [TestFixture(false, true, false)]
    [TestFixture(true, true, false)]
    public class HooksTests : BaseTestWithSettings
    {
        public HooksTests(bool useGeneratedSerializer, bool useGeneratedDeserializer, bool useTypeStamping) : base(useGeneratedSerializer, useGeneratedDeserializer, false, false, false, useTypeStamping, true)
        {
        }

        [Test]
        public void ShouldInvokeChainOfHooksInCorrectOrder()
        {
            ClassWithTheReference.Reset();
            var array = new ClassWithTheReference[6];
            for(var i = 0; i < array.Length; i++)
            {
                array[i] = new ClassWithTheReference(i);
            }

            array[0].refOne = array[1];
            array[0].refTwo = array[2];
            array[1].refOne = array[3];
            array[1].refTwo = array[4];
            array[2].refOne = array[1];
            array[2].refTwo = array[5];

            var copy = SerializerClone(array);

            Assert.AreEqual(1, copy[5].value);
            Assert.AreEqual(2, copy[4].value);
            Assert.AreEqual(3, copy[3].value);
            Assert.AreEqual(4, copy[2].value);
            Assert.AreEqual(5, copy[1].value);
            Assert.AreEqual(6, copy[0].value);
        }

        private class ClassWithTheReference
        {
            public ClassWithTheReference(int id)
            {
                this.id = id;
            }

            // we don't use array/list here to avoid additional objects
            public ClassWithTheReference refOne;
            public ClassWithTheReference refTwo;

            public int value;

            public int id;

            [PostDeserialization]
            private void PostDeserializationMethod()
            {
                value = ++counter;
            }

            public static void Reset()
            {
                counter = 0;
            }

            private static int counter = 0;
        }

        [Test]
        public void ShouldInvokePreSerialization()
        {
            var mock = new PreSerializationMock();
            var copy = SerializerClone(mock);
            Assert.IsTrue(mock.Invoked);
            Assert.IsTrue(copy.Invoked);
        }

        [Test]
        public void ShouldInvokeDerivedPreSerialization()
        {
            var mock = new PreSerializationMockDerived();
            var copy = SerializerClone(mock);
            Assert.IsTrue(mock.Invoked);
            Assert.IsTrue(mock.DerivedInvoked);
            Assert.IsTrue(copy.Invoked);
            Assert.IsTrue(copy.DerivedInvoked);
        }

        [Test]
        public void ShouldInvokePostSerialization()
        {
            var mock = new ImmediatePostSerializationMock();
            var copy = SerializerClone(mock);
            Assert.IsTrue(mock.Invoked);
            Assert.IsFalse(copy.Invoked);
        }

        [Test]
        public void ShouldInvokeDerivedPostSerialization()
        {
            var mock = new ImmediatePostSerializationMockDerived();
            var copy = SerializerClone(mock);
            Assert.IsTrue(mock.Invoked);
            Assert.IsTrue(mock.DerivedInvoked);
            Assert.IsFalse(copy.Invoked);
            Assert.IsFalse(copy.DerivedInvoked);
        }

        [Test]
        public void ShouldInvokeStaticPostSerialization()
        {
            var mock = new StaticImmediatePostSerializationMock();
            SerializerClone(mock);
            Assert.IsTrue(StaticImmediatePostSerializationMock.Invoked);
        }

        [Test]
        public void ShouldInvokePostDeserialization()
        {
            var mock = new PostDeserializationMock();
            var copy = SerializerClone(mock);
            Assert.IsFalse(mock.Invoked);
            Assert.IsTrue(copy.Invoked);
        }

        [Test]
        public void ShouldInvokeDerivedPostDeserialization()
        {
            var mock = new PostDeserializationMockDerived();
            var copy = SerializerClone(mock);
            Assert.IsFalse(mock.Invoked);
            Assert.IsFalse(mock.DerivedInvoked);
            Assert.IsTrue(copy.Invoked);
            Assert.IsTrue(copy.DerivedInvoked);
        }

        [Test]
        public void ShouldInvokeGlobalHooks()
        {
            var memoryStream = new MemoryStream();
            var serializer = new Serializer(GetSettings());
            var preSerializationCounter = 0;
            var postSerializationCounter = 0;
            var postDeserializationCounter = 0;
            serializer.OnPreSerialization += x => preSerializationCounter++;
            serializer.OnPostSerialization += x => postSerializationCounter++;
            serializer.OnPostDeserialization += x => postDeserializationCounter++;

            serializer.Serialize(new [] { "One", "Two" }, memoryStream);
            Assert.AreEqual(3, preSerializationCounter);
            Assert.AreEqual(3, postSerializationCounter);
            Assert.AreEqual(0, postDeserializationCounter);

            memoryStream.Seek(0, SeekOrigin.Begin);
            serializer.Deserialize<string[]>(memoryStream);
            Assert.AreEqual(3, postDeserializationCounter);
        }

        [Test]
        public void ShouldHaveDeserializedReferencedObjectsWhenHookIsInvoked()
        {
            var referencing = new ReferencingPostDeserializationMock();
            SerializerClone(referencing);
        }

        [Test]
        public void ShouldInvokePostHookAfterBothObjectsInCyclicReferenceAreDeserialized()
        {
            var a = new CyclicReferenceMockA();
            a.B = new CyclicReferenceMockB();
            a.B.A = a;
            SerializerClone(a);
        }

        [Test]
        public void ShouldInvokeLatePostSerializationHookAfterImmediate()
        {
            var late = new LatePostSerializationMockA();
            SerializerClone(late);
        }

        [Test]
        public void ShouldInvokeLatePostDeserializationHookAfterImmediate()
        {
            var late = new LatePostSerializationMockA();
            SerializerClone(late);
        }

        [Test]
        public void ShouldInvokeImmediateHooksInCorrectOrder()
        {
            var forOrderTest = new ForOrderTestA { B = new ForOrderTestB() };
            var copy = SerializerClone(forOrderTest);
            Assert.Less(forOrderTest.B.HookInvokedOn, forOrderTest.HookInvokedOn);
            Assert.Less(copy.B.HookInvokedOn, copy.HookInvokedOn);
        }

        [Test]
        public void ShouldInvokeLateHooksInCorrectOrder()
        {
            var forLateOrderTest = new ForLateOrderTestA { B = new ForLateOrderTestB() };
            var copy = SerializerClone(forLateOrderTest);
            Assert.Less(forLateOrderTest.B.HookInvokedOn, forLateOrderTest.HookInvokedOn);
            Assert.Less(copy.B.HookInvokedOn, copy.HookInvokedOn);
        }

        [Test]
        public void ShouldInvokePostSerializationEvenIfExceptionWasThrownDuringSerializationEarly()
        {
            ShouldInvokePostDeserializationEvenIfExceptionWasThrownDuringSerialization(new PrePostSerializationMock());
        }

        [Test]
        public void ShouldInvokePostSerializationEvenIfExceptionWasThrownDuringSerializationLate()
        {
            ShouldInvokePostDeserializationEvenIfExceptionWasThrownDuringSerialization(new LatePrePostSerializationMock());
        }

        [Test]
        public void ShouldInvokeHooksOnDerivedClassesInCorrectOrder()
        {
            var obj = new DerivedFromClassB();
            var copy = SerializerClone(obj);
            Assert.IsTrue(copy.FlagC);
        }

        [Test]
        public void ShouldProperlyExecuteHooksOnVirtualMethod()
        {
            var mockWithVirtual = new MockWithVirtualDerived();
            var copy = SerializerClone(mockWithVirtual);
            Assert.AreEqual(1, mockWithVirtual.BasePreSerializationCounter);
            Assert.AreEqual(2, mockWithVirtual.DerivedPreSerializationCounter);
            Assert.AreEqual(3, mockWithVirtual.BasePostSerializationCounter);
            Assert.AreEqual(4, mockWithVirtual.DerivedPostSerializationCounter);
            Assert.AreEqual(3, copy.BasePostDeserializationCounter);
            Assert.AreEqual(4, copy.DerivedPostDeserializationCounter);
        }

        [Test]
        public void ShouldFailWithSurrogateAndLatePostDeserializationHook()
        {
            try
            {
                var lateHook = new LateDeserializationMockA();
                PseudoClone(lateHook, serializer => serializer.ForSurrogate<LateDeserializationMockA>().SetObject(x => new object()));
                Assert.Fail("Serialization finished while it should fail.");
            }
            catch(InvalidOperationException)
            {

            }
        }

        private void ShouldInvokePostDeserializationEvenIfExceptionWasThrownDuringSerialization<T>(T prePostMock) where T : IPrePostMock
        {
            try
            {
                SerializerClone(prePostMock);
                Assert.Fail("The exception has not propagated.");
            }
            catch(InvalidOperationException)
            {

            }
            Assert.AreEqual(true, prePostMock.PreExecuted);
            Assert.AreEqual(true, prePostMock.PostExecuted);
        }
    }

    public class PreSerializationMock
    {
        [PreSerialization]
        private void PreSerialization()
        {
            Invoked = true;
        }

        public bool Invoked { get; private set; }
    }

    public class PreSerializationMockDerived : PreSerializationMock
    {
        [PreSerialization]
        private void PreSerializationDerived()
        {
            DerivedInvoked = true;
        }

        public bool DerivedInvoked { get; private set; }
    }

    public class ImmediatePostSerializationMock
    {
        [PostSerializationAttribute]
        private void PostSerialization()
        {
            Invoked = true;
        }

        public bool Invoked { get; private set; }
    }

    public class ImmediatePostSerializationMockDerived : ImmediatePostSerializationMock
    {
        [PostSerializationAttribute]
        private void PostSerializationDerived()
        {
            DerivedInvoked = true;
        }

        public bool DerivedInvoked { get; private set; }
    }

    public class StaticImmediatePostSerializationMock
    {
        [PostSerializationAttribute]
        private static void PostSerialization()
        {
            Invoked = true;
        }

        public static bool Invoked { get; private set; }
    }

    public class PostDeserializationMock
    {
        [LatePostDeserializationAttribute]
        private void PostDeserialization()
        {
            Invoked = true;
        }

        public bool Invoked { get; private set; }
    }

    public class PostDeserializationMockDerived : PostDeserializationMock
    {
        [LatePostDeserializationAttribute]
        private void PostDeserializationDerived()
        {
            DerivedInvoked = true;
        }

        public bool DerivedInvoked { get; private set; }
    }

    public class ReferencingPostDeserializationMock
    {
        public ReferencingPostDeserializationMock()
        {
            mock = new ReferencedPostDeserializationMock();
        }

        [LatePostDeserializationAttribute]
        private void PostDeserialization()
        {
            if(mock.TestObject == null)
            {
                throw new InvalidOperationException("Referenced mock was still not deserialized when invoking hook.");
            }
        }

        private readonly ReferencedPostDeserializationMock mock;
    }

    public class ReferencedPostDeserializationMock
    {
        public ReferencedPostDeserializationMock()
        {
            TestObject = new object();
        }

        public object TestObject { get; private set; }
    }

    public class CyclicReferenceMockA
    {
        public CyclicReferenceMockB B { get; set; }

        public string Str { get; set; }

        public CyclicReferenceMockA()
        {
            Str = "Something";
        }

        [LatePostDeserializationAttribute]
        public void TestIfBIsReady()
        {
            if(B == null || B.Str == null)
            {
                throw new InvalidOperationException("B is not ready after deserialization.");
            }
        }
    }

    public class CyclicReferenceMockB
    {
        public CyclicReferenceMockA A { get; set; }

        public string Str { get; set; }

        public CyclicReferenceMockB()
        {
            Str = "Something different";
        }

        [LatePostDeserializationAttribute]
        public void TestIfAIsReady()
        {
            if(A == null || A.Str == null)
            {
                throw new InvalidOperationException("A is not ready after deserialization.");
            }
        }
    }

    public class LatePostSerializationMockA
    {
        public LatePostSerializationMockA()
        {
            B = new PostSerializationMockB();
        }

        public PostSerializationMockB B { get; set; }

        [LatePostSerialization]
        private void PostSerialization()
        {
            if(!B.PostSerialized)
            {
                throw new InvalidOperationException("Late post serialization hook happened earlier than immediate one on referenced class.");
            }
        }
    }

    public class PostSerializationMockB
    {
        public bool PostSerialized { get; private set; }

        [PostSerializationAttribute]
        private void PostSerialization()
        {
            PostSerialized = true;
        }
    }

    public class LateDeserializationMockA
    {
        public LateDeserializationMockA()
        {
            B = new DeserializationMockB();
        }

        public DeserializationMockB B { get; set; }

        [LatePostDeserializationAttribute]
        private void PostDeserialization()
        {
            if(!B.PostDeserialized)
            {
                throw new InvalidOperationException("Late post serialization hook happened earlier than immediate one on referenced class.");
            }
        }
    }

    public class DeserializationMockB
    {
        public bool PostDeserialized { get; private set; }

        [PostDeserializationAttribute]
        private void PostDeserialization()
        {
            PostDeserialized = true;
        }
    }

    public class ForOrderTestA : ForOrderTestB
    {
        public ForOrderTestB B { get; set; }
    }

    public class ForOrderTestB
    {
        public DateTime HookInvokedOn { get; private set; }

        [PostDeserialization]
        [PostSerialization]
        public void AfterDeOrSerialization()
        {
            HookInvokedOn = DateTime.Now;
            Thread.Sleep(100);
        }
    }

    public class ForLateOrderTestA : ForLateOrderTestB
    {
        public ForLateOrderTestB B { get; set; }
    }

    public class ForLateOrderTestB
    {
        public DateTime HookInvokedOn { get; private set; }

        [LatePostDeserialization]
        [LatePostSerialization]
        public void AfterDeOrSerialization()
        {
            HookInvokedOn = DateTime.Now;
            Thread.Sleep(100);
        }
    }

    public class ClassSendingExcetpionDuringSerialization
    {
        [PreSerialization]
        private void SendException()
        {
            throw new InvalidOperationException();
        }
    }

    public interface IPrePostMock
    {
        bool PreExecuted { get; }

        bool PostExecuted { get; }
    }

    public class PrePostSerializationMock : IPrePostMock
    {
        public PrePostSerializationMock()
        {
            sendingException = new ClassSendingExcetpionDuringSerialization();
        }

        public bool PreExecuted { get; private set; }

        public bool PostExecuted { get; private set; }

        [PreSerialization]
        private void BeforeSerialization()
        {
            PreExecuted = true;
        }

        [PostSerialization]
        private void AfterSerialization()
        {
            PostExecuted = true;
        }

        #pragma warning disable 0414
        private ClassSendingExcetpionDuringSerialization sendingException;
        #pragma warning restore 0414
    }

    public class LatePrePostSerializationMock : IPrePostMock
    {
        public LatePrePostSerializationMock()
        {
            sendingException = new ClassSendingExcetpionDuringSerialization();
        }

        public bool PreExecuted { get; private set; }

        public bool PostExecuted { get; private set; }

        [PreSerialization]
        private void BeforeSerialization()
        {
            PreExecuted = true;
        }

        [LatePostSerialization]
        private void AfterSerialization()
        {
            PostExecuted = true;
        }

        #pragma warning disable 0414
        private ClassSendingExcetpionDuringSerialization sendingException;
        #pragma warning restore 0414
    }

    public class BaseClassA
    {
        [Transient]
        protected bool FlagA;

        [PostDeserialization]
        private void AfterDeserialization()
        {
            FlagA = true;
        }
    }

    public class DerivedFromBaseClassA : BaseClassA
    {
        [Transient]
        protected bool FlagB;

        [PostDeserialization]
        private void AfterDeserialization()
        {
            Assert.IsTrue(FlagA);
            FlagB = true;
        }
    }

    public class DerivedFromClassB : DerivedFromBaseClassA
    {
        [Transient]
        public bool FlagC;

        [PostDeserialization]
        private void AfterDeserialization()
        {
            Assert.IsTrue(FlagA);
            Assert.IsTrue(FlagB);
            FlagC = true;
        }
    }

    public class MockWithVirtualBase
    {
        public int BasePreSerializationCounter { get; private set; }
        public int BasePostSerializationCounter { get; private set; }
        public int BasePostDeserializationCounter { get; private set; }

        [PreSerialization]
        protected virtual void BeforeSerialization()
        {
            BasePreSerializationCounter = Counter;
        }

        [PostSerialization]
        protected virtual void AfterSerialization()
        {
            BasePostSerializationCounter = Counter;
        }

        [PostDeserialization]
        protected virtual void AfterDeserialization()
        {
            BasePostDeserializationCounter = Counter;
        }

        protected int Counter
        {
            get
            {
                return ++counterValue;
            }
        }

        private int counterValue;
    }

    public class MockWithVirtualDerived : MockWithVirtualBase
    {
        public int DerivedPreSerializationCounter { get; private set; }
        public int DerivedPostSerializationCounter { get; private set; }
        public int DerivedPostDeserializationCounter { get; private set; }

        protected override void BeforeSerialization()
        {
            base.BeforeSerialization();
            DerivedPreSerializationCounter = Counter;
        }

        protected override void AfterSerialization()
        {
            base.AfterSerialization();
            DerivedPostSerializationCounter = Counter;
        }

        protected override void AfterDeserialization()
        {
            base.AfterDeserialization();
            DerivedPostDeserializationCounter = Counter;
        }
    }
}

