using System;
using System.Collections;
using NUnit.Framework;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
#if UNITY_EDITOR
using UnityEditor.AddressableAssets.Settings;
using UnityEditor.AddressableAssets.Settings.GroupSchemas;
#endif
using UnityEngine.AddressableAssets.Initialization;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceLocations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.TestTools;
using UnityEngine.AddressableAssets.ResourceLocators;

namespace UnityEngine.AddressableAssets.ResourceProviders.Tests
{
    [TestFixture]
    public class ContentCatalogProviderTests : AddressablesTestFixture
    {
        const string k_LocationName = "TestLocation";
        const string k_LocationId = "TestLocationID";
        const string k_CacheLocationId = "CacheLocationID";
        const string k_RemoteLocationId = "RemoteLocationID";
        private const string k_TempAssetFolder = "Assets/TempFolder";
        private const string k_TempBuildFolder = "TempBuildFolder";
        private readonly string m_RuntimeCatalogFilename;

        public ContentCatalogProviderTests()
        {
            m_RuntimeCatalogFilename = "catalog" + m_UniqueTestName + ".bundle";
        }

        ResourceLocationBase m_SimpleLocation = new ResourceLocationBase(k_LocationName, k_LocationId, typeof(ContentCatalogProvider).FullName, typeof(object));

        protected override TestBuildScriptMode BuildScriptMode => TestBuildScriptMode.Packed;

#if UNITY_EDITOR
        internal override void Setup(AddressableAssetSettings settings, string tempAssetFolder)
        {
            settings.BundleLocalCatalog = true;
            settings.DefaultGroup.GetSchema<BundledAssetGroupSchema>().BundleNaming = BundledAssetGroupSchema.BundleNamingStyle.AppendHash;
        }

#endif

        [Test]
        public void DetermineIdToLoad_IfLocalCatalogsOnly_ReturnsMainId()
        {
            var contentCatalogOp = new ContentCatalogProvider.InternalOp();

            IResourceLocation[] dependencies = new IResourceLocation[(int) ContentCatalogProvider.DependencyHashIndex.Count];

            dependencies[(int) ContentCatalogProvider.DependencyHashIndex.Remote] = new ResourceLocationBase(string.Empty, k_RemoteLocationId, typeof(ContentCatalogProvider).FullName, typeof(object));
            dependencies[(int) ContentCatalogProvider.DependencyHashIndex.Cache] = new ResourceLocationBase(string.Empty, k_CacheLocationId, typeof(ContentCatalogProvider).FullName, typeof(object));

            var location = new ResourceLocationBase(k_LocationName, k_LocationId, typeof(ContentCatalogProvider).FullName, typeof(object), dependencies);
            var loadedId = contentCatalogOp.DetermineIdToLoad(location, new List<object> {"hash", string.Empty}, true);

            Assert.AreEqual(k_LocationId, loadedId);
        }

        [Test]
        public void DetermineIdToLoad_IfNoDependencies_ReturnsMainId()
        {
            var contentCatalogOp = new ContentCatalogProvider.InternalOp();

            var loadedId = contentCatalogOp.DetermineIdToLoad(m_SimpleLocation, null);

            Assert.AreEqual(k_LocationId, loadedId);
        }

        [Test]
        public void DetermineIdToLoad_IfTooFewDependencies_ReturnsMainId()
        {
            var contentCatalogOp = new ContentCatalogProvider.InternalOp();

            var loadedId = contentCatalogOp.DetermineIdToLoad(m_SimpleLocation, new List<object> {1});

            Assert.AreEqual(k_LocationId, loadedId);
        }

        [Test]
        public void DetermineIdToLoad_IfTooManyDependencies_ReturnsMainId()
        {
            var contentCatalogOp = new ContentCatalogProvider.InternalOp();

            var loadedId = contentCatalogOp.DetermineIdToLoad(m_SimpleLocation, new List<object> {1, 2, 3});

            Assert.AreEqual(k_LocationId, loadedId);
        }

        [Test]
        public void DetermineIdToLoad_IfOfflineAndNoCache_ReturnsMainId()
        {
            var contentCatalogOp = new ContentCatalogProvider.InternalOp();

            var loadedId = contentCatalogOp.DetermineIdToLoad(m_SimpleLocation, new List<object> {string.Empty, string.Empty});

            Assert.AreEqual(k_LocationId, loadedId);
        }

        [Test]
        public void DetermineIdToLoad_IfOfflineAndHasCache_ReturnsCacheId()
        {
            var contentCatalogOp = new ContentCatalogProvider.InternalOp();

            IResourceLocation[] dependencies = new IResourceLocation[(int) ContentCatalogProvider.DependencyHashIndex.Count];

            dependencies[(int) ContentCatalogProvider.DependencyHashIndex.Remote] = new ResourceLocationBase(string.Empty, k_RemoteLocationId, typeof(ContentCatalogProvider).FullName, typeof(object));
            dependencies[(int) ContentCatalogProvider.DependencyHashIndex.Cache] = new ResourceLocationBase(string.Empty, k_CacheLocationId, typeof(ContentCatalogProvider).FullName, typeof(object));

            var location = new ResourceLocationBase(k_LocationName, k_LocationId, typeof(ContentCatalogProvider).FullName, typeof(object), dependencies);
            var loadedId = contentCatalogOp.DetermineIdToLoad(location, new List<object> {string.Empty, "hash"});

            Assert.AreEqual(k_CacheLocationId, loadedId);
        }

        [Test]
        public void DetermineIdToLoad_IfOnlineMatchesCache_ReturnsCacheId()
        {
            var contentCatalogOp = new ContentCatalogProvider.InternalOp();

            IResourceLocation[] dependencies = new IResourceLocation[(int) ContentCatalogProvider.DependencyHashIndex.Count];

            dependencies[(int) ContentCatalogProvider.DependencyHashIndex.Remote] = new ResourceLocationBase(string.Empty, k_RemoteLocationId, typeof(ContentCatalogProvider).FullName, typeof(object));
            dependencies[(int) ContentCatalogProvider.DependencyHashIndex.Cache] = new ResourceLocationBase(string.Empty, k_CacheLocationId, typeof(ContentCatalogProvider).FullName, typeof(object));

            var location = new ResourceLocationBase(k_LocationName, k_LocationId, typeof(ContentCatalogProvider).FullName, typeof(object), dependencies);
            var loadedId = contentCatalogOp.DetermineIdToLoad(location, new List<object> {"hash", "hash"});

            Assert.AreEqual(k_CacheLocationId, loadedId);
        }

        [Test]
        public void DetermineIdToLoad_IfDisableContentCatalogUpdateTrue_ForcesLocalId()
        {
            var contentCatalogOp = new ContentCatalogProvider.InternalOp();

            IResourceLocation[] dependencies = new IResourceLocation[(int) ContentCatalogProvider.DependencyHashIndex.Count];

            dependencies[(int) ContentCatalogProvider.DependencyHashIndex.Remote] = new ResourceLocationBase(string.Empty, k_RemoteLocationId, typeof(ContentCatalogProvider).FullName, typeof(object));
            dependencies[(int) ContentCatalogProvider.DependencyHashIndex.Cache] = new ResourceLocationBase(string.Empty, k_CacheLocationId, typeof(ContentCatalogProvider).FullName, typeof(object));

            var location = new ResourceLocationBase(k_LocationName, k_LocationId, typeof(ContentCatalogProvider).FullName, typeof(object), dependencies);
            var loadedId = contentCatalogOp.DetermineIdToLoad(location, new List<object> {"hash", ""}, true);

            Assert.AreEqual(k_LocationId, loadedId);
        }

        [Test]
        public void DetermineIdToLoad_IfDisableContentCatalogUpdateTrue_ForcesCachedIdWhenLocalHashExists()
        {
            var contentCatalogOp = new ContentCatalogProvider.InternalOp();

            IResourceLocation[] dependencies = new IResourceLocation[(int) ContentCatalogProvider.DependencyHashIndex.Count];

            dependencies[(int) ContentCatalogProvider.DependencyHashIndex.Remote] = new ResourceLocationBase(string.Empty, k_RemoteLocationId, typeof(ContentCatalogProvider).FullName, typeof(object));
            dependencies[(int) ContentCatalogProvider.DependencyHashIndex.Cache] = new ResourceLocationBase(string.Empty, k_CacheLocationId, typeof(ContentCatalogProvider).FullName, typeof(object));

            var location = new ResourceLocationBase(k_LocationName, k_LocationId, typeof(ContentCatalogProvider).FullName, typeof(object), dependencies);
            var loadedId = contentCatalogOp.DetermineIdToLoad(location, new List<object> {"hash", "local"}, true);
            Assert.AreEqual(k_CacheLocationId, loadedId);
        }

        [Test]
        public void DetermineIdToLoad_SetsLocalHash_WhenDisableContentCatalogIsTrue_AndNoLocalHashExists()
        {
            var contentCatalogOp = new ContentCatalogProvider.InternalOp();

            IResourceLocation[] dependencies = new IResourceLocation[(int) ContentCatalogProvider.DependencyHashIndex.Count];

            dependencies[(int) ContentCatalogProvider.DependencyHashIndex.Remote] = new ResourceLocationBase(string.Empty, k_RemoteLocationId, typeof(ContentCatalogProvider).FullName, typeof(object));
            dependencies[(int) ContentCatalogProvider.DependencyHashIndex.Cache] = new ResourceLocationBase(string.Empty, k_CacheLocationId, typeof(ContentCatalogProvider).FullName, typeof(object));

            var location = new ResourceLocationBase(k_LocationName, k_LocationId, typeof(ContentCatalogProvider).FullName, typeof(object), dependencies);
            Assert.IsTrue(string.IsNullOrEmpty(contentCatalogOp.m_LocalHashValue));
            var loadedId = contentCatalogOp.DetermineIdToLoad(location, new List<object> {"hash", ""}, true);
            Assert.IsFalse(string.IsNullOrEmpty(contentCatalogOp.m_LocalHashValue));
        }

        [Test]
        public void DetermineIdToLoad_IfOnlineMismatchesCache_ReturnsRemoteId()
        {
            var contentCatalogOp = new ContentCatalogProvider.InternalOp();

            IResourceLocation[] dependencies = new IResourceLocation[(int) ContentCatalogProvider.DependencyHashIndex.Count];

            dependencies[(int) ContentCatalogProvider.DependencyHashIndex.Remote] = new ResourceLocationBase(string.Empty, k_RemoteLocationId, typeof(ContentCatalogProvider).FullName, typeof(object));
            dependencies[(int) ContentCatalogProvider.DependencyHashIndex.Cache] = new ResourceLocationBase(string.Empty, k_CacheLocationId, typeof(ContentCatalogProvider).FullName, typeof(object));

            var location = new ResourceLocationBase(k_LocationName, k_LocationId, typeof(ContentCatalogProvider).FullName, typeof(object), dependencies);


            var loadedId = contentCatalogOp.DetermineIdToLoad(location, new List<object> {"newHash", "hash"});
            Assert.AreEqual(k_RemoteLocationId, loadedId);

            loadedId = contentCatalogOp.DetermineIdToLoad(location, new List<object> {"newHash", string.Empty});
            Assert.AreEqual(k_RemoteLocationId, loadedId);
        }

        [Test]
        [TestCase(null, typeof(ArgumentNullException))]
        [TestCase("invalid", typeof(ArgumentException))]
        [TestCase("file.txt", typeof(ArgumentException))]
        public void BundledCatalog_LoadCatalogFromBundle_InvalidBundlePath_ShouldThrow(string path, Type exceptionType)
        {
            Assert.Throws(exceptionType, () => new ContentCatalogProvider.InternalOp.BundledCatalog(path));
        }

        [UnityTest]
        [Ignore("https://jira.unity3d.com/browse/ADDR-1451")]
        public IEnumerator BundledCatalog_LoadCatalogFromBundle_InvalidBundleFileFormat_ShouldFail()
        {
            var bundleFilePath = Path.Combine(k_TempBuildFolder, "catalog.bundle");
            Directory.CreateDirectory(Path.GetDirectoryName(bundleFilePath));

            var bytes = new byte[] {1, 2, 3, 4, 5, 6};
            File.WriteAllBytes(bundleFilePath, bytes);

            LogAssert.Expect(LogType.Error, new Regex("Failed to read data for the AssetBundle", RegexOptions.IgnoreCase));

            LogAssert.Expect(LogType.Error, new Regex("Unable to load dependent " +
                                                      $"bundle from location :", RegexOptions.IgnoreCase));

            var bundledCatalog = new ContentCatalogProvider.InternalOp.BundledCatalog(bundleFilePath);
            bundledCatalog.LoadCatalogFromBundleAsync();

            yield return new WaitWhile(() => bundledCatalog.OpInProgress);

            Assert.IsFalse(bundledCatalog.OpIsSuccess);

            if (Directory.Exists(k_TempBuildFolder))
                Directory.Delete(k_TempBuildFolder, true);
        }

        [UnityTest]
        public IEnumerator BundledCatalog_WhenCatalogIsLocal_LoadCatalogFromBundle_ShouldLoadCatalogAndUnloadResources()
        {
            var bundleFilePath = Path.Combine(Addressables.RuntimePath, m_RuntimeCatalogFilename);

            var bundledCatalog = new ContentCatalogProvider.InternalOp.BundledCatalog(bundleFilePath);
            bundledCatalog.LoadCatalogFromBundleAsync();
            bundledCatalog.OnLoaded += catalogData =>
            {
                Assert.NotNull(catalogData);
                Assert.AreEqual(ResourceManagerRuntimeData.kCatalogAddress, catalogData.ProviderId);
            };

            yield return new WaitWhile(() => bundledCatalog.OpInProgress);

            Assert.IsTrue(bundledCatalog.OpIsSuccess);
            Assert.Null(bundledCatalog.m_CatalogAssetBundle);
        }

        [UnityTest]
        [UnityPlatform(exclude = new[] {RuntimePlatform.Android})]
        public IEnumerator BundledCatalog_WhenCatalogIsRemote_LoadCatalogFromBundle_ShouldLoadCatalogAndUnloadResources()
        {
            string localBundleFilePath = Path.Combine(Addressables.RuntimePath, m_RuntimeCatalogFilename);
            string bundleFilePath = "file:///" + Path.GetFullPath(localBundleFilePath);

            var bundledCatalog = new ContentCatalogProvider.InternalOp.BundledCatalog(bundleFilePath);
            bundledCatalog.LoadCatalogFromBundleAsync();
            bundledCatalog.OnLoaded += catalogData =>
            {
                Assert.NotNull(catalogData);
                Assert.AreEqual(ResourceManagerRuntimeData.kCatalogAddress, catalogData.ProviderId);
            };

            yield return new WaitWhile(() => bundledCatalog.OpInProgress);

            Assert.IsTrue(bundledCatalog.OpIsSuccess);
            Assert.Null(bundledCatalog.m_CatalogAssetBundle);
        }

        [UnityTest]
        public IEnumerator BundledCatalog_WhenRemoteCatalogDoesNotExist_LoadCatalogFromBundle_LogsErrorAndOpFails()
        {
            string bundleFilePath = "file:///doesnotexist.bundle";

            var bundledCatalog = new ContentCatalogProvider.InternalOp.BundledCatalog(bundleFilePath);
            bundledCatalog.LoadCatalogFromBundleAsync();

            LogAssert.Expect(LogType.Error, $"Unable to load dependent bundle from location : {bundleFilePath}");

            yield return new WaitWhile(() => bundledCatalog.OpInProgress);

            Assert.IsFalse(bundledCatalog.OpIsSuccess);
        }

        [UnityTest]
        public IEnumerator BundledCatalog_LoadCatalogFromBundle_WhenCalledMultipleTimes_OpNotCompleted_FirstShouldSucceedAndOthersShouldFail()
        {
            var bundleFilePath = Path.Combine(Addressables.RuntimePath, m_RuntimeCatalogFilename);

            var timesCalled = 0;
            var bundledCatalog = new ContentCatalogProvider.InternalOp.BundledCatalog(bundleFilePath);
            bundledCatalog.OnLoaded += catalogData =>
            {
                Assert.NotNull(catalogData);
                Assert.AreEqual(ResourceManagerRuntimeData.kCatalogAddress, catalogData.ProviderId);
                timesCalled++;
            };

            bundledCatalog.LoadCatalogFromBundleAsync();
            bundledCatalog.LoadCatalogFromBundleAsync();
            LogAssert.Expect(LogType.Error, new Regex("progress", RegexOptions.IgnoreCase));

            yield return new WaitWhile(() => bundledCatalog.OpInProgress);

            Assert.AreEqual(1, timesCalled);
        }

        [UnityTest]
        public IEnumerator BundledCatalog_LoadCatalogFromBundle_WhenCalledMultipleTimes_OpCompleted_AllShouldSucceed()
        {
            var bundleFilePath = Path.Combine(Addressables.RuntimePath, m_RuntimeCatalogFilename);

            var timesCalled = 0;
            var bundledCatalog = new ContentCatalogProvider.InternalOp.BundledCatalog(bundleFilePath);
            bundledCatalog.OnLoaded += catalogData =>
            {
                Assert.NotNull(catalogData);
                Assert.AreEqual(ResourceManagerRuntimeData.kCatalogAddress, catalogData.ProviderId);
                timesCalled++;
            };

            bundledCatalog.LoadCatalogFromBundleAsync();
            yield return new WaitWhile(() => bundledCatalog.OpInProgress);

            bundledCatalog.LoadCatalogFromBundleAsync();
            yield return new WaitWhile(() => bundledCatalog.OpInProgress);

            Assert.AreEqual(2, timesCalled);
            Assert.IsTrue(bundledCatalog.OpIsSuccess);
        }

        [Test]
        public void ContentCatalogProvider_InternalOp_LoadCatalog_InvalidId_Throws()
        {
            Assert.Throws<NullReferenceException>(() => new ContentCatalogProvider.InternalOp().LoadCatalog("fakeId", false));
        }

#if ENABLE_BINARY_CATALOG
        [TestCase("http://127.0.0.1/catalog.bin", false)]
#else
        [TestCase("http://127.0.0.1/catalog.json", false)]
#endif
        [TestCase("http://127.0.0.1/catalog.bundle", true)]
        public void BundledCatalog_WhenRequestingRemoteCatalog_CanLoadCatalogFromBundle_ReturnsExpectedResult(string internalId, bool result)
        {
            var loc = new ResourceLocationBase(internalId, internalId, typeof(ContentCatalogProvider).FullName, typeof(IResourceLocator));
            ProviderOperation<Object> op = new ProviderOperation<Object>();
            op.Init(m_Addressables.ResourceManager, null, loc, new AsyncOperationHandle<IList<AsyncOperationHandle>>());
            ProvideHandle handle = new ProvideHandle(m_Addressables.ResourceManager, op);

            bool loadCatalogFromLocalBundle = new ContentCatalogProvider.InternalOp().CanLoadCatalogFromBundle(internalId, handle.Location);
            Assert.AreEqual(result, loadCatalogFromLocalBundle);
        }

        [Test]
        public void BundledCatalog_WhenRequestingLocalCatalog_CanLoadCatalogFromBundle_ReturnsTrue()
        {
            string internalId = Path.Combine(Addressables.RuntimePath, m_RuntimeCatalogFilename);
            var loc = new ResourceLocationBase(internalId, internalId, typeof(ContentCatalogProvider).FullName, typeof(IResourceLocator));
            ProviderOperation<Object> op = new ProviderOperation<Object>();
            op.Init(m_Addressables.ResourceManager, null, loc, new AsyncOperationHandle<IList<AsyncOperationHandle>>());
            ProvideHandle handle = new ProvideHandle(m_Addressables.ResourceManager, op);

            bool loadCatalogFromLocalBundle = new ContentCatalogProvider.InternalOp().CanLoadCatalogFromBundle(internalId, handle.Location);
            Assert.IsTrue(loadCatalogFromLocalBundle);
        }
    }
}