/// <summary>
        /// Async function to "Run" the <see cref="FileValidator"/> on a specific directory with a specific
        /// <see cref="ConfigFile"/>
        /// </summary>
        /// <param name="configFilePath">the path to the <see cref="ConfigFile"/></param>
        /// <param name="searchDirectoryPath">the path to the directory to search for files</param>
        /// <returns>awaitable <see cref="Task"/></returns>
        private async Task Run(string configFilePath, string searchDirectoryPath)
        {
            try
            {
                if (!Directory.Exists(searchDirectoryPath))
                {
                    throw new ArgumentException($"Search Directory '{searchDirectoryPath}' does not exist");
                }

                ConfigLoader configLoader = new ConfigLoader();
                FileLoader   fileLoader   = new FileLoader();

                //Load the config file
                ConfigFile configFile = await configLoader.AsyncLoadConfigFile(configFilePath);

                //Get all file names in the search directory (and possibly sub folders)
                SearchOption searchOption = configFile.SearchAllSubFolders ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
                string[]     allFiles     = Directory.GetFiles(searchDirectoryPath, "*.*", searchOption);

                //For each file, create an appropriate IFileProperties object
                int fileCount = allFiles.Length;
                int validationConfigFileCount = configFile.ValidationConfigs.Count;
                Dictionary <string, IFileProperties> loadedFileProperties = new Dictionary <string, IFileProperties>();
                for (int fileIndex = 0; fileIndex < fileCount; ++fileIndex)
                {
                    string filePath = Path.GetFullPath(allFiles[fileIndex]);
                    //TODO rather than calling "await" in the loop, we should get the Tasks and do Task.WhenAll()
                    IFileProperties fileProperties = await fileLoader.FetchFileProperties(filePath);

                    loadedFileProperties.Add(filePath, fileProperties);
                }

                //for each file, find the best ValidationConfig (the first with the highest match)
                Dictionary <string, ValidationConfig> bestMatchingConfigs = new Dictionary <string, ValidationConfig>();
                foreach (var entry in loadedFileProperties)
                {
                    string           filePath         = entry.Key;
                    IFileProperties  fileProperties   = entry.Value;
                    uint             higestMatchCount = 0;
                    ValidationConfig bestMatchConfig  = null;
                    for (int configIndex = 0; configIndex < validationConfigFileCount; ++configIndex)
                    {
                        ValidationConfig validationConfig      = configFile.ValidationConfigs[configIndex];
                        uint             filterRulesMatchCount = validationConfig.NumberOfMatchingFilterRules(fileProperties);
                        //NOTE: if there are multiple validationConfigs that match the same number of filter_rules, the first will always win
                        if (higestMatchCount < filterRulesMatchCount)
                        {
                            higestMatchCount = filterRulesMatchCount;
                            bestMatchConfig  = validationConfig;
                        }
                    }
                    bestMatchingConfigs.Add(filePath, bestMatchConfig);
                }

                //for each file resolve the ValidationConfig
                Dictionary <string, uint> validationConfigCount = new Dictionary <string, uint>();
                foreach (var entry in bestMatchingConfigs)
                {
                    string           filePath        = entry.Key;
                    ValidationConfig bestMatchConfig = entry.Value;
                    IFileProperties  fileProperties  = loadedFileProperties[filePath];

                    if (bestMatchConfig == null)
                    {
                        Console.WriteLine($"No match could be found for file: {fileProperties.FileName}");
                        continue;
                    }
                    else
                    {
                        Console.WriteLine($"file: {fileProperties.FileName} matched validation config: {bestMatchConfig.Name}");
                    }

                    string configName = bestMatchConfig.Name;
                    if (!validationConfigCount.ContainsKey(configName))
                    {
                        validationConfigCount.Add(configName, 0);
                    }
                    validationConfigCount[configName] += 1;

                    //TODO possibly don't pass the count if it's 1?
                    string resolvedCombinedPath = bestMatchConfig.GetResolvedCombinedPath(searchDirectoryPath, fileProperties, validationConfigCount[configName]);
                    string fullResolvedPath     = Path.GetFullPath(resolvedCombinedPath);

                    //If the file isn't already named correctly and in the expected path
                    if (!string.Equals(fullResolvedPath, filePath, StringComparison.OrdinalIgnoreCase))
                    {
                        //If "auto_convert" is set, move/rename the file
                        if (bestMatchConfig.AutoConvert)
                        {
                            try
                            {
                                FileInfo newFileInfo = new FileInfo(fullResolvedPath);
                                //If the expected folder doesn't exist, create it
                                if (!newFileInfo.Directory.Exists)
                                {
                                    newFileInfo.Directory.Create();
                                }
                                //Move the file
                                File.Move(filePath, fullResolvedPath);
                                loadedFileProperties.Remove(filePath);
                                //Reload the IFileProperties in case the change will now change the result of ValidationConfig.Validate()
                                //TODO can we do this async call NOT in the loop?
                                fileProperties = await fileLoader.FetchFileProperties(fullResolvedPath);

                                filePath = fullResolvedPath;
                                loadedFileProperties[fullResolvedPath] = fileProperties;
                            }
                            catch (IOException e)
                            {
                                Console.WriteLine($"Unable to move/rename: {filePath} to: {fullResolvedPath}, exception: {e}");
                            }
                        }
                        else
                        {
                            //If "auto_convert" is not set, just output the recommended output
                            Console.WriteLine($"file: {fileProperties.FileName} is not set to 'auto_convert' and should moved/renamed to: {fullResolvedPath}");
                        }
                    }
                    else
                    {
                        Console.WriteLine($"file: {fileProperties.FileName} matches expected output");
                    }

                    if (bestMatchConfig.Validate(fileProperties))
                    {
                        Console.WriteLine($"File: {fileProperties.FileName} is valid");
                    }
                    else
                    {
                        Console.WriteLine($"File: {fileProperties.FileName} failed to validate {bestMatchConfig.ValidationRuleName}");
                    }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine($"\n Unhandled exception: {e}");
            }
        }