using NUnit.Framework;
using System;
using System.Collections;
using System.Linq;
using UnityEngine;
using UnityEngine.Pool;

namespace UnityEditor.Rendering.Tests
{
    class AnalyticsUtilsTests
    {
        private TestScriptableObject m_SO;
        private TestObject m_Current;

        [SetUp]
        public void SetUp()
        {
            m_Current = new TestObject();
            m_SO = ScriptableObject.CreateInstance(typeof(TestScriptableObject)) as TestScriptableObject;
        }

        [TearDown]
        public void TearDown()
        {
            ScriptableObject.DestroyImmediate(m_SO);
        }

        [Serializable]
        public class TestObject
        {
            public int integer;
            public float floatNumber;
            public double doubleNumber;
            public bool boolean;
            public string test;
            public int[] array;

            public enum TestDiffEnum
            {
                a, b, c
            }

            public TestDiffEnum diffEnum;

            [Serializable]
            public struct InnerTestStruct
            {
                public int myValue;
            }

            public InnerTestStruct innerStruct;
        }

        public class TestScriptableObject : ScriptableObject
        {
            public TestObject myData;

            public TestScriptableObject()
            {
                myData = new TestObject();
            }
        }

        static TestCaseData[] s_TestCaseDatas =
        {
            new TestCaseData("integer", 1)
                .Returns(new string[] { "{\"myData.integer\":\"1\"}" })
                .SetName("Given an int field, the nested column is generated correctly"),
            new TestCaseData("floatNumber", 1.5f)
                .Returns(new string[] { "{\"myData.floatNumber\":\"1.5\"}" })
                .SetName("Given a float field, the nested column is generated correctly"),
            new TestCaseData("doubleNumber", 1.2)
                .Returns(new string[] { "{\"myData.doubleNumber\":\"1.2\"}" })
                .SetName("Given a double field, the nested column is generated correctly"),
            new TestCaseData("boolean", true)
                .Returns(new string[] { "{\"myData.boolean\":\"True\"}" })
                .SetName("Given a bool field, the nested column is generated correctly"),
            new TestCaseData("test", "Hello World")
                .Returns(new string[] { "{\"myData.test\":\"Hello World\"}" })
                .SetName("Given a string field, the nested column is generated correctly"),
            new TestCaseData("array", new int[] { 1,2,3} )
                .Returns(new string[] { "{\"myData.array\":\"[1,2,3]\"}" })
                .SetName("Given an array of ints field, the nested column is generated correctly"),
            new TestCaseData("diffEnum", TestObject.TestDiffEnum.b)
                .Returns(new string[] { "{\"myData.diffEnum\":\"b\"}" })
                .SetName("Given an enum field, the nested column is generated correctly"),
             new TestCaseData("innerStruct", new TestObject.InnerTestStruct{ myValue = 5 } )
                .Returns(new string[] { "{\"myData.innerStruct.myValue\":\"5\"}" })
                .SetName("Given an inner struct field, the nested column is generated correctly"),
        };

        private void SetValue(TestObject current, string fieldName, object value)
        {
            current.GetType().GetField(fieldName).SetValue(current, value);
        }

        [Test, TestCaseSource(nameof(s_TestCaseDatas))]
        public string[] ToNestedColumnUnityObject(string fieldName, object value)
        {
            SetValue(m_Current, fieldName, value);
            m_SO.myData = m_Current;
            return m_SO.ToNestedColumn(true);
        }

        static TestCaseData[] s_TestCaseDatasWithDefault =
        {
            new TestCaseData("integer", 1)
                .Returns(new string[] { "{\"integer\":\"1\"}" })
                .SetName("Given an int field, the nested column is generated correctly"),
            new TestCaseData("floatNumber", 1.5f)
                .Returns(new string[] { "{\"floatNumber\":\"1.5\"}" })
                .SetName("Given a float field, the nested column is generated correctly"),
            new TestCaseData("doubleNumber", 1.2)
                .Returns(new string[] { "{\"doubleNumber\":\"1.2\"}" })
                .SetName("Given a double field, the nested column is generated correctly"),
            new TestCaseData("boolean", true)
                .Returns(new string[] { "{\"boolean\":\"True\"}" })
                .SetName("Given a bool field, the nested column is generated correctly"),
            new TestCaseData("test", "Hello World")
                .Returns(new string[] { "{\"test\":\"Hello World\"}" })
                .SetName("Given a string field, the nested column is generated correctly"),
            new TestCaseData("array", new int[] { 1,2,3} )
                .Returns(new string[] { "{\"array\":\"[1,2,3]\"}" })
                .SetName("Given an array of ints field, the nested column is generated correctly"),
            new TestCaseData("diffEnum", TestObject.TestDiffEnum.b)
                .Returns(new string[] { "{\"diffEnum\":\"b\"}" })
                .SetName("Given an enum field, the nested column is generated correctly"),
             new TestCaseData("innerStruct", new TestObject.InnerTestStruct{ myValue = 5 } )
                .Returns(new string[] { "{\"innerStruct.myValue\":\"5\"}" })
                .SetName("Given an inner struct field, the nested column is generated correctly"),
        };

        [Test, TestCaseSource(nameof(s_TestCaseDatasWithDefault))]
        public string[] ToNestedColumnWithDefault(string fieldName, object value)
        {
            SetValue(m_Current, fieldName, value);
            m_SO.myData = m_Current;
            return m_Current.ToNestedColumnWithDefault<TestObject>(new TestObject(), true);
        }

        [Test]
        public void GivenAnObjectWithSerializableFields_WhenAllFieldsAreRequested_AllOfThemAreReturnedCorrectly()
        {
            using (ListPool<string>.Get(out var tmp))
            {
                foreach (var testCase in s_TestCaseDatas)
                {
                    SetValue(m_Current, (string)testCase.Arguments[0], testCase.Arguments[1]);
                    tmp.Add(((string[])testCase.ExpectedResult)[0]);
                }

                m_SO.myData = m_Current;
                Assert.AreEqual(m_SO.ToNestedColumn(false), tmp.ToArray());
            }
        }

        [Test]
        public void CheckEditorAnalyticsAreDisabledOnCI()
        {
            var ci = Environment.GetEnvironmentVariable("CI");
            if (!string.IsNullOrWhiteSpace(ci) && bool.TryParse(ci, out var result) && result)
            {
                Assert.IsFalse(EditorAnalytics.enabled, "Analytics must be only enabled for user projects not on CI");
                Debug.Log("Assert for CheckEditorAnalyticsAreDisabledOnCI was executed.");
            }
        }


        [Serializable]
        class ClassWithPublicInt
        {
            public int myPublicInt;
        }

        [Serializable]
        class ClassWithPrivateBool
        {
            private bool myPrivateBoolNotSerializable;
        }

        [Serializable]
        class ClassWithPrivateIntWithSerializableFieldAttribute
        {
            [SerializeField]
            private int myPrivateIntSerializable;
        }

        [Serializable]
        class ClassWithPublicIntWithNonSerializableFieldAttribute
        {
            [NonSerialized]
            public int myPublicNonSerializedInt;
        }

        [Serializable]
        class ClassWithPublicPropertytWithSerializableFieldAttributeForTheBackingField
        {
            [field: SerializeField]
            public int myProperty { get; private set; }
        }

        [Serializable]
        class ClassWithObsoletePublicField
        {
            [Obsolete("this is obsolete")]
            public int myPublicInt;
        }


        [Serializable]
        class BaseClassWithField
        {
            public int myPublicInt;
        }

        [Serializable]
        class ChildClass : BaseClassWithField
        {
            public bool myPublicBool;
        }

        static TestCaseData[] s_ListTestsCaseDatasSerializableFields =
        {
            new TestCaseData(typeof(ClassWithPublicInt), false)
                .SetName("Given a class with a public field, the field is returned")
                .Returns(new string[] { "myPublicInt" }),
            new TestCaseData(typeof(ClassWithPrivateBool), false)
                .SetName("Given a class with a private field, nothing is returned")
                .Returns(new string[] { }),
            new TestCaseData(typeof(ClassWithPrivateIntWithSerializableFieldAttribute), false)
                .SetName("Given a class with a private field with [SerializeField] attribute, the field is returned")
                .Returns(new string[] {"myPrivateIntSerializable" }),
            new TestCaseData(typeof(ClassWithPublicIntWithNonSerializableFieldAttribute), false)
                .SetName("Given a class with public field with [NonSerialized] attribute, nothing is returned")
                .Returns(new string[] { }),
            new TestCaseData(typeof(ClassWithPublicPropertytWithSerializableFieldAttributeForTheBackingField), false)
                .SetName("Given a class with public property with [field: SerializeField] attribute, the backing field is returned")
                .Returns(new string[] {"<myProperty>k__BackingField" }),
            new TestCaseData(typeof(ClassWithObsoletePublicField), false)
                .SetName("Given a class with public field with [Obsolete] attribute and not removing obsolete fields, the obsolete is returned")
                .Returns(new string[] {"myPublicInt" }),
             new TestCaseData(typeof(ClassWithObsoletePublicField), true)
                .SetName("Given a class with public field with [Obsolete] attribute, nothing is returned when obsolete fields must be removed")
                .Returns(new string[] { }),
             new TestCaseData(typeof(ChildClass), false)
                .SetName("Given a class with inheriting from another with a public field, the parent field is returned")
                .Returns(new string[] { "myPublicInt", "myPublicBool" }),
        };


        [Test, TestCaseSource(nameof(s_ListTestsCaseDatasSerializableFields))]
        public string[] GetSerializableFields(Type type, bool removeObsolete)
        {
            return type
                .GetSerializableFields(removeObsolete)
                .Select(f => f.Name)
                .ToArray();
        }

        private static IEnumerable ValueComparisonTestsCases
        {
            get
            {
                // String test cases
                yield return new TestCaseData(typeof(string), "test", "test", true).SetName("StringEqual");
                yield return new TestCaseData(typeof(string), "test1", "test2", false).SetName("StringNotEqual");

                // Primitive and enum test cases
                yield return new TestCaseData(typeof(int), 1, 1, true).SetName("IntEqual");
                yield return new TestCaseData(typeof(int), 1, 2, false).SetName("IntNotEqual");
                yield return new TestCaseData(typeof(DayOfWeek), DayOfWeek.Monday, DayOfWeek.Monday, true).SetName("EnumEqual");
                yield return new TestCaseData(typeof(DayOfWeek), DayOfWeek.Monday, DayOfWeek.Tuesday, false).SetName("EnumNotEqual");

                // Array test cases
                yield return new TestCaseData(typeof(int[]), new int[] { 1, 2, 3 }, new int[] { 1, 2, 3 }, true).SetName("ArrayEqual");
                yield return new TestCaseData(typeof(int[]), new int[] { 1, 2, 3 }, new int[] { 1, 2, 4 }, false).SetName("ArrayNotEqual");
                yield return new TestCaseData(typeof(int[]), new int[] { 1, 2, 3 }, new int[] { 1, 2 }, false).SetName("ArrayDifferentLength");

                // Null test cases
                yield return new TestCaseData(typeof(object), null, null, true).SetName("BothNull");
                yield return new TestCaseData(typeof(object), new object(), null, false).SetName("OneNull");
                yield return new TestCaseData(typeof(object), null, new object(), false).SetName("OneNullOtherWay");
            }
        }

        [Test, TestCaseSource(nameof(ValueComparisonTestsCases))]
        public void TestAreValuesEqual(Type fieldType, object valueCurrent, object valueDefault, bool expected)
        {
            bool result = AnalyticsUtils.AreValuesEqual(fieldType, valueCurrent, valueDefault);
            Assert.AreEqual(expected, result);
        }

        private class TestClass { }
        private static IEnumerable TypeComplexityTestCases
        {
            get
            {
                // Primitive types test cases
                yield return new TestCaseData(typeof(int), false).SetName("IntIsNotComplex");
                yield return new TestCaseData(typeof(double), false).SetName("DoubleIsNotComplex");

                // Enum test cases
                yield return new TestCaseData(typeof(DayOfWeek), false).SetName("EnumIsNotComplex");

                // String test cases
                yield return new TestCaseData(typeof(string), false).SetName("StringIsNotComplex");

                // Array test cases
                yield return new TestCaseData(typeof(int[]), false).SetName("IntArrayIsNotComplex");
                yield return new TestCaseData(typeof(string[]), false).SetName("StringArrayIsNotComplex");

                // Struct test cases
                yield return new TestCaseData(typeof(DateTime), true).SetName("DateTimeIsComplex");

                // Class test cases
                yield return new TestCaseData(typeof(object), true).SetName("ObjectIsComplex");
                yield return new TestCaseData(typeof(TestClass), true).SetName("CustomClassIsComplex");
            }
        }

        [Test, TestCaseSource(nameof(TypeComplexityTestCases))]
        public void TestIsComplexType(Type fieldType, bool expected)
        {
            bool result = AnalyticsUtils.IsComplexType(fieldType);
            Assert.AreEqual(expected, result);
        }
    }
}