/// <summary>
        /// Installs the specific version of an ASI to the specified target
        /// </summary>
        /// <param name="modVersion"></param>
        /// <param name="target"></param>
        /// <param name="forceSource">Null to let application choose the source, true to force online, false to force local cache. This parameter is used for testing</param>
        /// <returns></returns>
        public static bool InstallASIToTarget(ASIModVersion asi, GameTarget target, bool?forceSource = null)
        {
            if (asi.Game != target.Game)
            {
                throw new Exception($@"ASI {asi.Name} cannot be installed to game {target.Game}");
            }
            Log.Information($@"Processing ASI installation request: {asi.Name} v{asi.Version} -> {target.TargetPath}");
            string destinationFilename  = $@"{asi.InstalledPrefix}-v{asi.Version}.asi";
            string cachedPath           = Path.Combine(CachedASIsFolder, destinationFilename);
            string destinationDirectory = MEDirectories.ASIPath(target);

            if (!Directory.Exists(destinationDirectory))
            {
                Log.Information(@"Creating ASI directory in game: " + destinationDirectory);
                Directory.CreateDirectory(destinationDirectory);
            }
            string finalPath = Path.Combine(destinationDirectory, destinationFilename);

            // Delete existing ASIs from the same group to ensure we don't install the same mod
            var  existingSameGroupMods            = target.GetInstalledASIs().OfType <KnownInstalledASIMod>().Where(x => x.AssociatedManifestItem.OwningMod == asi.OwningMod).ToList();
            bool hasExistingVersionOfModInstalled = false;

            if (existingSameGroupMods.Any())
            {
                foreach (var v in existingSameGroupMods)
                {
                    if (v.Hash == asi.Hash && !forceSource.HasValue && !hasExistingVersionOfModInstalled) //If we are forcing a source, we should always install. Delete duplicates past the first one
                    {
                        Log.Information($@"{v.AssociatedManifestItem.Name} is already installed. We will not remove the existing correct installed ASI for this install request");
                        hasExistingVersionOfModInstalled = true;
                        continue; //Don't delete this one. We are already installed. There is no reason to install it again.
                    }
                    Log.Information($@"Deleting existing ASI from same group: {v.InstalledPath}");
                    v.Uninstall();
                }
            }

            if (hasExistingVersionOfModInstalled && !forceSource.HasValue) //Let app decide
            {
                return(true);                                              // This asi was "Installed" (because it was already installed).
            }

            // Install the ASI
            if (forceSource == null || forceSource.Value == false)
            {
                Debug.WriteLine("Hit me");
            }
            string md5;
            bool   useLocal = forceSource.HasValue && !forceSource.Value; // false (forceLocal)

            if (!useLocal && !forceSource.HasValue)
            {
                useLocal = File.Exists(cachedPath);
            }
            if (useLocal)
            {
                //Check hash first
                md5 = Utilities.CalculateMD5(cachedPath);
                if (md5 == asi.Hash)
                {
                    Log.Information($@"Copying ASI from cached library to destination: {cachedPath} -> {finalPath}");

                    File.Copy(cachedPath, finalPath, true);
                    Log.Information($@"Installed ASI to {finalPath}");
                    Analytics.TrackEvent(@"Installed ASI", new Dictionary <string, string>()
                    {
                        { @"Filename", Path.GetFileNameWithoutExtension(finalPath) }
                    });
                    return(true);
                }
            }

            if (!forceSource.HasValue || forceSource.Value)
            {
                WebRequest request = WebRequest.Create(asi.DownloadLink);
                Log.Information(@"Fetching remote ASI from server");

                using WebResponse response = request.GetResponse();
                var memoryStream = new MemoryStream();
                response.GetResponseStream().CopyTo(memoryStream);
                //MD5 check on file for security
                md5 = Utilities.CalculateMD5(memoryStream);
                if (md5 != asi.Hash)
                {
                    //ERROR!
                    Log.Error(@"Downloaded ASI did not match the manifest! It has the wrong hash.");
                    return(false);
                }

                Log.Information(@"Fetched remote ASI from server. Installing ASI to " + finalPath);
                memoryStream.WriteToFile(finalPath);
                Log.Information(@"ASI successfully installed.");
                Analytics.TrackEvent(@"Installed ASI", new Dictionary <string, string>()
                {
                    { @"Filename", Path.GetFileNameWithoutExtension(finalPath) }
                });

                //Cache ASI
                if (!Directory.Exists(CachedASIsFolder))
                {
                    Log.Information(@"Creating cached ASIs folder");
                    Directory.CreateDirectory(CachedASIsFolder);
                }

                Log.Information(@"Caching ASI to local ASI library: " + cachedPath);
                memoryStream.WriteToFile(cachedPath);
                return(true);
            }

            // We could not install the ASI
            return(false);
        }
        public void TestASIManager()
        {
            GlobalTest.Init();
            Random random = new Random();

            Console.WriteLine(@"Loading ASI Manager Manifest");
            ASIManager.LoadManifest();

            var games = new[] { Mod.MEGame.ME1, Mod.MEGame.ME2, Mod.MEGame.ME3 };

            foreach (var game in games)
            {
                var        root   = GlobalTest.GetTestGameFoldersDirectory(game);
                var        normal = Path.Combine(root, "normal");
                GameTarget gt     = new GameTarget(game, normal, true, false);

                var asiDir = MEDirectories.ASIPath(gt);
                if (Directory.Exists(asiDir))
                {
                    // Clean slate
                    Utilities.DeleteFilesAndFoldersRecursively(asiDir);
                }

                var asisForGame = ASIManager.GetASIModsByGame(game);

                // 1: Test Installs of upgrades of versions
                foreach (var asi in asisForGame)
                {
                    // Install every version of an ASI and then ensure only one ASI of that type exists in the directory.
                    foreach (var v in asi.Versions)
                    {
                        var sourceBools = new bool?[] { true, false, null }; //online, local, let app decide
                        foreach (var sourceBool in sourceBools)
                        {
                            // INSTALL FROM SOURCE
                            Console.WriteLine($@"Install source variable: {sourceBool}");
                            Assert.IsTrue(ASIManager.InstallASIToTarget(v, gt, sourceBool),
                                          $"Installation of ASI failed: {v.Name}");
                            Assert.AreEqual(1, Directory.GetFiles(asiDir).Length,
                                            "The count of files in the ASI directory is not 1 after install of an ASI!");

                            // Check is installed
                            var installedASIs = gt.GetInstalledASIs().OfType <KnownInstalledASIMod>().ToList();
                            Assert.AreEqual(1, installedASIs.Count,
                                            "The amount of installed ASIs as fetched by GameTarget GetInstalledASIs() is not equal to 1!");

                            // Check it maps to the correct one.
                            var instASI = installedASIs.First();
                            Assert.AreEqual(v, instASI.AssociatedManifestItem, "The parsed installed ASI does not match the one we fed to ASIManager.InstallASIToTarget()!");

                            // Rename it to something random so the next version has to find it by the hash and not the filename.
                            var newPath = Path.Combine(asiDir, Guid.NewGuid() + ".asi");
                            File.Move(instASI.InstalledPath, newPath, false);

                            // Ensure it still can be found.
                            installedASIs = gt.GetInstalledASIs().OfType <KnownInstalledASIMod>().ToList();
                            Assert.AreEqual(1, installedASIs.Count, "The amount of installed ASIs as fetched by GameTarget GetInstalledASIs() is not equal to 1 after renaming the file!");

                            // Make multiple clones, to ensure all old ones get deleted on upgrades.
                            for (int i = 0; i < 5; i++)
                            {
                                var clonePath = Path.Combine(asiDir, instASI.AssociatedManifestItem.InstalledPrefix + i + ".asi");
                                File.Copy(newPath, clonePath, true);
                            }

                            installedASIs = gt.GetInstalledASIs().OfType <KnownInstalledASIMod>().ToList();
                            Assert.AreEqual(6, installedASIs.Count, "The amount of installed ASIs as fetched by GameTarget GetInstalledASIs() is not equal to 6 after cloning the file 5 times!");
                        }
                    }

                    var finalASIsPreRandomization = gt.GetInstalledASIs();
                    int randomCount = 0;
                    foreach (var iam in finalASIsPreRandomization)
                    {
                        // Test randomly editing it.
                        byte[] randomData = new byte[256];
                        random.NextBytes(randomData);
                        File.WriteAllBytes(iam.InstalledPath, randomData);
                        randomCount++;

                        var unknownInstalledASIs = gt.GetInstalledASIs().OfType <UnknownInstalledASIMod>().ToList();
                        Assert.AreEqual(randomCount, unknownInstalledASIs.Count, "Writing random bytes to installed ASI made amount of installed ASIs not correct!");
                    }

                    foreach (var v in finalASIsPreRandomization)
                    {
                        // Test uninstall and remove
                        Assert.IsTrue(v.Uninstall(), $"ASI failed to uninstall: {v.InstalledPath}");
                    }
                    Assert.AreEqual(0, Directory.GetFiles(asiDir).Length, "Leftover files remain after uninstalling all ASIs from target");
                }
            }
        }