using System;
using NUnit.Framework;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor.Build.Pipeline.Utilities;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEditor.SceneManagement;

namespace UnityEditor.Build.Pipeline.Tests
{
    [TestFixture]
    abstract internal class BuildCacheTestBase
    {
        protected const string kBuildCacheTestPath = "Assets/BuildCacheTestAssets";

        protected string kTestFile1
        {
            get { return Path.Combine(kBuildCacheTestPath, "testfile1.txt"); }
        }

        protected string kUncachedTestFilename
        {
            get { return Path.Combine(kBuildCacheTestPath, "uncached.txt"); }
        }

        protected string kTempAssetFilename
        {
            get { return Path.Combine(kBuildCacheTestPath, "temporary.txt"); }
        }

        protected string kTestScenePath
        {
            get { return Path.Combine(kBuildCacheTestPath, "testScene.unity"); }
        }

        protected GUID TestFile1GUID
        {
            get { return new GUID(AssetDatabase.AssetPathToGUID(kTestFile1)); }
        }

        protected GUID UncachedGUID
        {
            get { return new GUID(AssetDatabase.AssetPathToGUID(kUncachedTestFilename)); }
        }

        protected GUID TempAssetGUID
        {
            get { return new GUID(AssetDatabase.AssetPathToGUID(kTempAssetFilename)); }
        }

        protected GUID TestSceneGUID
        {
            get { return new GUID(AssetDatabase.AssetPathToGUID(kTestScenePath)); }
        }

        protected BuildCache m_Cache;

        internal virtual void OneTimeSetupDerived() {}
        internal virtual void OneTimeTearDownDerived() {}
        internal virtual void SetupDerived() {}
        internal virtual void TeardownDerived() {}

        [OneTimeSetUp]
        public void OneTimeSetUp()
        {
            Directory.CreateDirectory(kBuildCacheTestPath);
            File.WriteAllText(kTestFile1, "t1");
            File.WriteAllText(kUncachedTestFilename, "uncached");
            File.WriteAllText(kTempAssetFilename, "delete me");
            AssetDatabase.Refresh();
            OneTimeSetupDerived();
        }

        [OneTimeTearDown]
        public void OneTimeTearDown()
        {
            Directory.Delete(kBuildCacheTestPath, true);
            File.Delete(kBuildCacheTestPath + ".meta");
            AssetDatabase.Refresh();
            OneTimeTearDownDerived();
        }

        [SetUp]
        public void Setup()
        {
            BuildCacheUtility.ClearCacheHashes();
            PurgeBuildCache();
            RecreateBuildCache();
            SetupDerived();
        }

        [TearDown]
        public void TearDown()
        {
            if (m_Cache != null)
            {
                m_Cache.Dispose();
                m_Cache = null;
            }
            TeardownDerived();
        }

        protected virtual void RecreateBuildCache()
        {
            BuildCacheUtility.ClearCacheHashes();
            if (m_Cache != null)
            {
                m_Cache.SyncPendingSaves();
                m_Cache.Dispose();
                m_Cache = null;
            }
            m_Cache = new BuildCache();
        }

        protected virtual void PurgeBuildCache()
        {
            if (m_Cache != null)
                m_Cache.SyncPendingSaves();
            BuildCache.PurgeCache(false);
        }

        static protected CachedInfo LoadCachedInfoForGUID(BuildCache cache, GUID guid)
        {
            IList<CachedInfo> infos;
            CacheEntry entry1 = cache.GetCacheEntry(guid);
            cache.LoadCachedData(new List<CacheEntry>() { entry1 }, out infos);
            return infos[0];
        }

        static protected string StoreDataInCacheWithGUID(BuildCache cache, GUID guid, object data, GUID depGUID = new GUID())
        {
            List<CacheEntry> deps = new List<CacheEntry>();
            if (!depGUID.Empty())
                deps.Add(cache.GetCacheEntry(depGUID));

            CacheEntry entry1 = cache.GetCacheEntry(guid);
            CachedInfo info = new CachedInfo();
            info.Asset = entry1;
            info.Dependencies = deps.ToArray();
            info.Data = new object[] { data };
            cache.SaveCachedData(new List<CachedInfo>() { info });
            cache.SyncPendingSaves();
            return cache.GetCachedInfoFile(info.Asset);
        }

        static protected GUID CreateTestTextAsset(string contents)
        {
            string filename;
            return CreateTestTextAsset(contents, out filename);
        }

        static protected GUID CreateTestTextAsset(string contents, out string filename)
        {
            int fileIndex = 0;
            while (true)
            {
                filename = Path.Combine(kBuildCacheTestPath, "testasset" + fileIndex);
                if (!File.Exists(filename))
                {
                    File.WriteAllText(filename, contents);
                    AssetDatabase.Refresh();
                    return new GUID(AssetDatabase.AssetPathToGUID(filename));
                }

                fileIndex++;
            }
        }

        static protected void ModifyTestTextAsset(GUID guid, string text)
        {
            string filename = AssetDatabase.GUIDToAssetPath(guid.ToString());
            File.WriteAllText(filename, text);
            AssetDatabase.ImportAsset(filename);
        }

        [Test]
        public void WhenLoadingCachedDataForGUIDWithModifiedDependency_CachedInfoIsNull()
        {
            GUID depGuid = CreateTestTextAsset("mytext");
            StoreDataInCacheWithGUID(m_Cache, TestFile1GUID, "data", depGuid);
            ModifyTestTextAsset(depGuid, "mytext2");
            RecreateBuildCache();
            CachedInfo info = LoadCachedInfoForGUID(m_Cache, TestFile1GUID);
            Assert.IsNull(info);
        }

        [Test]
        public void WhenLoadingCachedDataForModifiedGUID_CachedInfoIsNull()
        {
            GUID guid = CreateTestTextAsset("mytext");
            StoreDataInCacheWithGUID(m_Cache, guid, "data");
            ModifyTestTextAsset(guid, "mytext2");
            RecreateBuildCache();
            CachedInfo info = LoadCachedInfoForGUID(m_Cache, guid);
            Assert.IsNull(info);
        }

        [Test]
        public void WhenLoadingCachedDataForGUIDWithInvalidCacheData_CachedInfoIsNull()
        {
            GUID depGuid = CreateTestTextAsset("mytext");
            string path = StoreDataInCacheWithGUID(m_Cache, TestFile1GUID, "data", depGuid);
            RecreateBuildCache();
            File.WriteAllText(path, "Invalidating cache file! Good luck deserializing this! =P");
            CachedInfo info = LoadCachedInfoForGUID(m_Cache, TestFile1GUID);
            Assert.IsNull(info);
        }

        [Test]
        public void WhenLoadingStoredCachedData_CachedInfoIsValid()
        {
            StoreDataInCacheWithGUID(m_Cache, TestFile1GUID, "data");
            RecreateBuildCache();
            CachedInfo info = LoadCachedInfoForGUID(m_Cache, TestFile1GUID);
            Assert.AreEqual("data", (string)info.Data[0]);
        }

        [Test]
        public void WhenLoadingUncachedData_CachedInfoIsNull()
        {
            CachedInfo info = LoadCachedInfoForGUID(m_Cache, UncachedGUID);
            Assert.IsNull(info);
        }

        [Test]
        public void WhenGlobalVersionChanges_OriginalCachedInfoDoesNotNeedRebuild()
        {
            m_Cache.OverrideGlobalHash(new Hash128(0, 1, 0, 0));

            StoreDataInCacheWithGUID(m_Cache, TestFile1GUID, "data");
            CachedInfo info1 = LoadCachedInfoForGUID(m_Cache, TestFile1GUID);

            RecreateBuildCache();
            m_Cache.OverrideGlobalHash(new Hash128(0, 2, 0, 0));
            CachedInfo info2 = LoadCachedInfoForGUID(m_Cache, TestFile1GUID);

            Assert.IsFalse(m_Cache.HasAssetOrDependencyChanged(info1)); // original info still doesn't need rebuild. Required for content update
            Assert.IsNull(info2); // however the data cannot be recovered
        }

        [Test]
        public void WhenLocalVersionChanges_AssetReturnsDifferentCacheEntry()
        {
            GUID guid = CreateTestTextAsset("mytext");

            var entry1 = m_Cache.GetCacheEntry(guid, 2);
            var entry2 = m_Cache.GetCacheEntry(guid, 4);

            Assert.AreEqual(entry1.Guid, entry2.Guid);
            Assert.AreEqual(entry1.File, entry2.File);
            Assert.AreEqual(entry1.Type, entry2.Type);
            Assert.AreNotEqual(entry1.Version, entry2.Version);
            Assert.AreNotEqual(entry1.Hash, entry2.Hash);
        }

        [Test]
        public void WhenLocalVersionChanges_FileReturnsDifferentCacheEntry()
        {
            string filename;
            CreateTestTextAsset("mytext", out filename);

            var entry1 = m_Cache.GetCacheEntry(filename, 2);
            var entry2 = m_Cache.GetCacheEntry(filename, 4);

            Assert.AreEqual(entry1.Guid, entry2.Guid);
            Assert.AreEqual(entry1.File, entry2.File);
            Assert.AreEqual(entry1.Type, entry2.Type);
            Assert.AreNotEqual(entry1.Version, entry2.Version);
            Assert.AreNotEqual(entry1.Hash, entry2.Hash);
        }

        [Test]
        public void GetUpdatedCacheEntry_ReturnsCacheEntryWithSameVersionAndHash_IfAssetHasNotChanged()
        {
            GUID guid = CreateTestTextAsset("mytext");

            var entry1 = m_Cache.GetCacheEntry(guid, 2);
            m_Cache.ClearCacheEntryMaps();
            var entry2 = m_Cache.GetUpdatedCacheEntry(entry1);

            Assert.AreEqual(entry1.Guid, entry2.Guid);
            Assert.AreEqual(entry1.File, entry2.File);
            Assert.AreEqual(entry1.Type, entry2.Type);
            Assert.AreEqual(entry1.Version, entry2.Version);
            Assert.AreEqual(entry1.Hash, entry2.Hash);
        }

        [Test]
        public void GetUpdatedCacheEntry_ReturnsCacheEntryWithSameVersionAndHash_IfFileHasNotChanged()
        {
            string filename;
            CreateTestTextAsset("mytext", out filename);

            var entry1 = m_Cache.GetCacheEntry(filename, 2);
            m_Cache.ClearCacheEntryMaps();
            var entry2 = m_Cache.GetUpdatedCacheEntry(entry1);

            Assert.AreEqual(entry1.Guid, entry2.Guid);
            Assert.AreEqual(entry1.File, entry2.File);
            Assert.AreEqual(entry1.Type, entry2.Type);
            Assert.AreEqual(entry1.Version, entry2.Version);
            Assert.AreEqual(entry1.Hash, entry2.Hash);
        }

        [Test]
        public void GetCacheEntry_InvalidPath_ReturnsInvalidCacheEntry()
        {
            var entry = m_Cache.GetCacheEntry("this/path/does/not/exist");
            Assert.IsFalse(entry.IsValid());
        }

        [Test]
        public void GetCacheEntry_InvalidGUID_ReturnsInvalidCacheEntry()
        {
            var entry = m_Cache.GetCacheEntry(new GUID("00000000000000000000000000000000"));
            Assert.IsFalse(entry.IsValid());
        }

        [Test]
        public void GetCacheEntry_FormerValidPathAndGUID_ReturnsInvalidCacheEntry()
        {
            var tempAssetGUID = TempAssetGUID;

            var entry1 = m_Cache.GetCacheEntry(kTempAssetFilename);
            var entry2 = m_Cache.GetCacheEntry(tempAssetGUID);
            Assert.IsTrue(entry1.IsValid());
            Assert.IsTrue(entry2.IsValid());

            File.Delete(kTempAssetFilename);
            AssetDatabase.Refresh();
            m_Cache.ClearCacheEntryMaps();

            entry1 = m_Cache.GetCacheEntry(kTempAssetFilename);
            entry2 = m_Cache.GetCacheEntry(tempAssetGUID);
            Assert.IsFalse(entry1.IsValid());
            Assert.IsFalse(entry2.IsValid());
        }

        [Test]
        public void GetCacheEntry_PathForAsset_ReturnsAssetBasedCacheEntry()
        {
            var entry1 = m_Cache.GetCacheEntry(kTestFile1);
            var entry2 = m_Cache.GetCacheEntry(TestFile1GUID);

            Assert.IsTrue(entry1.Type == CacheEntry.EntryType.Asset);
            Assert.AreEqual(entry2, entry1);
        }

        [Test]
        public void GetCacheEntry_DiffStripUnusedMeshComponentsSettings_ReturnsDiffHashes()
        {
            Scene scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene);
            EditorSceneManager.SaveScene(scene, kTestScenePath);
            EditorSceneManager.NewScene(NewSceneSetup.EmptyScene); // clear active scene

            int version = 2;
            var kvp = new KeyValuePair<GUID, int>(TestSceneGUID, version);
            bool stripUnusedMeshComponents = PlayerSettings.stripUnusedMeshComponents;
            try
            {
                PlayerSettings.stripUnusedMeshComponents = false;
                CacheEntry entry1 = m_Cache.GetCacheEntry(TestSceneGUID, version);
                BuildCacheUtility.m_GuidToHash.Remove(kvp);

                PlayerSettings.stripUnusedMeshComponents = true;
                CacheEntry entry2 = m_Cache.GetCacheEntry(TestSceneGUID);
                BuildCacheUtility.m_GuidToHash.Remove(kvp);

                Assert.AreNotEqual(entry1.Hash, entry2.Hash);
            }
            finally
            {
                PlayerSettings.stripUnusedMeshComponents = stripUnusedMeshComponents;
                AssetDatabase.DeleteAsset(kTestScenePath);
            }
        }

        [Test]
        public void GetCachedInfoFile_IsInside_GetCachedArtifactsDirectory()
        {
            var entry = m_Cache.GetCacheEntry(kTestFile1);
            var infoFile = m_Cache.GetCachedInfoFile(entry);
            var artifactsDirectory = m_Cache.GetCachedArtifactsDirectory(entry);

            StringAssert.Contains(artifactsDirectory, infoFile);
        }

        void WriteRandomFile(string fileName, int sizeInMB)
        {
            byte[] data = new byte[sizeInMB * 1024 * 1024];
            var rng = new System.Random();
            rng.NextBytes(data);
            File.WriteAllBytes(fileName, data);
        }

        void PopulateCache(out int filesWritten, out string[] artifactsDirectories)
        {
            var entries = new[]
            {
                m_Cache.GetCacheEntry(kTestFile1),
                m_Cache.GetCacheEntry(kUncachedTestFilename)
            };
            artifactsDirectories = new[]
            {
                Path.GetFullPath(m_Cache.GetCachedArtifactsDirectory(entries[0])),
                Path.GetFullPath(m_Cache.GetCachedArtifactsDirectory(entries[1]))
            };

            // Setup cache
            filesWritten = 0;
            foreach (var directory in artifactsDirectories)
            {
                Directory.CreateDirectory(directory);
                for (int i = 0; i < 2; i++)
                {
                    WriteRandomFile($"{directory}/cachefile_{i}.bytes", 2);
                    filesWritten++;
                }
            }
        }

        [Test]
        public void PruneCache_ComputeCacheSizeAndFolders_ReturnsCorrectSizeAndFolders()
        {
            PopulateCache(out int filesWritten, out string[] artifactsDirectories);

            BuildCache.ComputeCacheSizeAndFolders(out long currentCacheSize, out List<BuildCache.CacheFolder> cacheFolders);
            //filesWritten * 2mb each * 1024 to kb * 1024 to b
            Assert.AreEqual(filesWritten * 2 * 1024 * 1024, currentCacheSize);
            Assert.AreEqual(artifactsDirectories.Length, cacheFolders.Count);
            var folders = cacheFolders.Select(x => x.directory.FullName);
            CollectionAssert.AreEquivalent(artifactsDirectories, folders);
        }

        [Test]
        public void PruneCache_PruneCacheFolders_WillRemoveOldestFolders()
        {
            PopulateCache(out int filesWritten, out string[] artifactsDirectories);
            BuildCache.ComputeCacheSizeAndFolders(out long currentCacheSize, out List<BuildCache.CacheFolder> cacheFolders);

            // Set folder older
            var folder = cacheFolders[0];
            folder.LastAccessTimeUtc = folder.LastAccessTimeUtc.Subtract(new TimeSpan(1, 0, 0));
            cacheFolders[0] = folder;

            // delete just under the first folder size
            long maximumCacheSize = currentCacheSize - folder.Length + 1;
            BuildCache.PruneCacheFolders(maximumCacheSize, currentCacheSize, cacheFolders);
            BuildCache.ComputeCacheSizeAndFolders(out long newCurrentCacheSize, out List<BuildCache.CacheFolder> newCacheFolders);

            Assert.AreNotEqual(0, newCurrentCacheSize);
            Assert.GreaterOrEqual(maximumCacheSize, newCurrentCacheSize);
            Assert.AreEqual(artifactsDirectories.Length - 1, newCacheFolders.Count);
        }
    }
}