using System;
using System.Reflection;
using System.Text;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.TestRunner.NUnitExtensions.Runner;
using UnityEngine.TestTools.NUnitExtensions;
using UnityEngine.TestTools.Logging;

namespace UnityEditor.TestTools.TestRunner
{
    [Serializable]
    internal class TestRunnerStateSerializer : IStateSerializer
    {
        private const BindingFlags Flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy;

        [SerializeField]
        private HideFlags m_OriginalHideFlags;

        [SerializeField]
        private bool m_ShouldRestore;

        [SerializeField]
        private string m_TestObjectTypeName;

        [SerializeField]
        private ScriptableObject m_TestObject;

        [SerializeField]
        private string m_TestObjectTxt;

        [SerializeField]
        private long StartTicks;

        [SerializeField]
        private double StartTimeOA;

        [SerializeField]
        private string output;

        [SerializeField]
        private LogMatch[] m_ExpectedLogs;

        public bool ShouldRestore()
        {
            return m_ShouldRestore;
        }

        public void SaveContext()
        {
            var currentContext = UnityTestExecutionContext.CurrentContext;

            if (currentContext.TestObject != null)
            {
                m_TestObjectTypeName = currentContext.TestObject.GetType().AssemblyQualifiedName;
                m_TestObject = null;
                m_TestObjectTxt = null;
                if (currentContext.TestObject is ScriptableObject)
                {
                    m_TestObject = currentContext.TestObject as ScriptableObject;
                    m_OriginalHideFlags = m_TestObject.hideFlags;
                    m_TestObject.hideFlags |= HideFlags.DontSave;
                }
                else
                {
                    m_TestObjectTxt = JsonUtility.ToJson(currentContext.TestObject);
                }
            }

            output = currentContext.CurrentResult.Output;
            StartTicks = currentContext.StartTicks;
            StartTimeOA = currentContext.StartTime.ToOADate();
            if (LogScope.HasCurrentLogScope())
            {
                m_ExpectedLogs = LogScope.Current.ExpectedLogs.ToArray();
            }

            m_ShouldRestore = true;
        }

        public void RestoreContext()
        {
            var currentContext = UnityTestExecutionContext.CurrentContext;

            var outputProp = currentContext.CurrentResult.GetType().BaseType.GetField("_output", Flags);
            (outputProp.GetValue(currentContext.CurrentResult) as StringBuilder).Append(output);

            currentContext.StartTicks = StartTicks;
            currentContext.StartTime = DateTime.FromOADate(StartTimeOA);
            if (LogScope.HasCurrentLogScope())
            {
                LogScope.Current.ExpectedLogs = new Queue<LogMatch>(m_ExpectedLogs);
            }

            m_ShouldRestore = false;
        }

        public bool CanRestoreFromScriptableObject(Type requestedType)
        {
            if (m_TestObject == null)
            {
                return false;
            }
            return m_TestObjectTypeName == requestedType.AssemblyQualifiedName;
        }

        public ScriptableObject RestoreScriptableObjectInstance()
        {
            if (m_TestObject == null)
            {
                Debug.LogError("No object to restore");
                return null;
            }
            EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
            var temp = m_TestObject;
            m_TestObject = null;
            m_TestObjectTypeName = null;
            return temp;
        }

        public bool CanRestoreFromJson(Type requestedType)
        {
            if (string.IsNullOrEmpty(m_TestObjectTxt))
            {
                return false;
            }
            return m_TestObjectTypeName == requestedType.AssemblyQualifiedName;
        }

        public void RestoreClassFromJson(ref object instance)
        {
            if (string.IsNullOrEmpty(m_TestObjectTxt))
            {
                Debug.LogWarning("No JSON representation to restore");
                return;
            }
            JsonUtility.FromJsonOverwrite(m_TestObjectTxt, instance);
            m_TestObjectTxt = null;
            m_TestObjectTypeName = null;
        }

        private void OnPlayModeStateChanged(PlayModeStateChange state)
        {
            if (m_TestObject == null)
            {
                EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
                return;
            }

            //We set the DontSave flag here because the ScriptableObject would be nulled right before entering EditMode
            if (state == PlayModeStateChange.ExitingPlayMode)
            {
                m_TestObject.hideFlags |= HideFlags.DontSave;
            }
            else if (state == PlayModeStateChange.EnteredEditMode)
            {
                m_TestObject.hideFlags = m_OriginalHideFlags;
                EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
            }
        }
    }
}
