// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#if !WINDOWS_UWP && !WIN_UI

#if NETFRAMEWORK
using System.Security;

using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter;

#endif

using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment;
#if NETFRAMEWORK
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Extensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
#endif

namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities;

internal sealed class DeploymentUtility : DeploymentUtilityBase
{
    public DeploymentUtility()
        : base()
    {
    }

    public DeploymentUtility(DeploymentItemUtility deploymentItemUtility, AssemblyUtility assemblyUtility, FileUtility fileUtility)
        : base(deploymentItemUtility, assemblyUtility, fileUtility)
    {
    }

    public override void AddDeploymentItemsBasedOnMsTestSetting(string testSourceHandler, IList<DeploymentItem> deploymentItems, List<string> warnings)
    {
#if NETFRAMEWORK
        if (MSTestSettingsProvider.Settings.DeployTestSourceDependencies)
        {
            PlatformServiceProvider.Instance.AdapterTraceLogger.Info("Adding the references and satellite assemblies to the deployment items list");

            // Get the referenced assemblies.
            ProcessNewStorage(testSourceHandler, deploymentItems, warnings);

            // Get the satellite assemblies
            IEnumerable<DeploymentItem> satelliteItems = GetSatellites(deploymentItems, testSourceHandler, warnings);
            foreach (DeploymentItem satelliteItem in satelliteItems)
            {
                DeploymentItemUtility.AddDeploymentItem(deploymentItems, satelliteItem);
            }
        }
        else
        {
            PlatformServiceProvider.Instance.AdapterTraceLogger.Info("Adding the test source directory to the deployment items list");
            DeploymentItemUtility.AddDeploymentItem(deploymentItems, new DeploymentItem(Path.GetDirectoryName(testSourceHandler)));
        }
#else
        // It should add items from bin\debug but since deployment items in netcore are run from bin\debug only, so no need to implement it
#endif
    }

#if !NETCOREAPP
    private static int ProcessId
    {
        get
        {
            int processId = field;
            if (processId == 0)
            {
                field = processId = GetProcessId();
                // Assume that process Id zero is invalid for user processes. It holds for all mainstream operating systems.
                Debug.Assert(processId != 0, "processId is expected to be non-zero.");
            }

            return processId;

            static int GetProcessId()
            {
                using var process = Process.GetCurrentProcess();
                return process.Id;
            }
        }
    }
#else
    private static int ProcessId => Environment.ProcessId;
#endif

    /// <summary>
    /// Get root deployment directory.
    /// </summary>
    /// <param name="baseDirectory">The base directory.</param>
    /// <returns>Root deployment directory.</returns>
    public override string GetRootDeploymentDirectory(string baseDirectory)
    {
        string dateTimeSuffix = $"{DateTime.Now.ToString("yyyyMMddTHHmmss", DateTimeFormatInfo.InvariantInfo)}_{ProcessId}";

        string directoryName = string.Format(CultureInfo.InvariantCulture, Resource.TestRunName, DeploymentFolderPrefix,
#if NETFRAMEWORK
            Environment.UserName,
#else
            Environment.GetEnvironmentVariable("USERNAME") ?? Environment.GetEnvironmentVariable("USER"),
#endif
            dateTimeSuffix);
        directoryName = FileUtility.ReplaceInvalidFileNameCharacters(directoryName);

        return FileUtility.GetNextIterationDirectoryName(baseDirectory, directoryName);
    }

    protected override void AddDependenciesOfDeploymentItem(string deploymentItemFile, IList<string> filesToDeploy, IList<string> warnings)
    {
#if NETFRAMEWORK
        var dependencies = new List<DeploymentItem>();

        AddDependencies(deploymentItemFile, null, dependencies, warnings);

        foreach (DeploymentItem dependencyItem in dependencies)
        {
            DebugEx.Assert(Path.IsPathRooted(dependencyItem.SourcePath), "Path of the dependency " + dependencyItem.SourcePath + " is not rooted.");

            // Add dependencies to filesToDeploy.
            filesToDeploy.Add(dependencyItem.SourcePath);
        }
#else
        // Its implemented only in full framework project as dependent files are not fetched in netcore.
#endif
    }

#if NETFRAMEWORK
    [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")]
    public void ProcessNewStorage(string testSourceHandler, IList<DeploymentItem> deploymentItems, IList<string> warnings)
    {
        // Add deployment items and process .config files only for storages we have not processed before.
        if (!DeploymentItemUtility.IsValidDeploymentItem(testSourceHandler, string.Empty, out string? errorMessage))
        {
            warnings.Add(errorMessage);
            return;
        }

        DeploymentItemUtility.AddDeploymentItem(deploymentItems, new DeploymentItem(testSourceHandler, string.Empty, DeploymentItemOriginType.TestStorage));

        // Deploy .config file if exists, only for assemblies, i.e. DLL and EXE.
        // First check <TestStorage>.config, then if not found check for App.Config
        // and deploy AppConfig to <TestStorage>.config.
        if (AssemblyUtility.IsAssemblyExtension(Path.GetExtension(testSourceHandler)))
        {
            string? configFile = AddTestSourceConfigFileIfExists(testSourceHandler, deploymentItems);

            // Deal with test dependencies: update dependencyDeploymentItems and missingDependentAssemblies.
            try
            {
                // We look for dependent assemblies only for DLL and EXE's.
                AddDependencies(testSourceHandler, configFile, deploymentItems, warnings);
            }
            catch (Exception e)
            {
                string warning = string.Format(CultureInfo.CurrentCulture, Resource.DeploymentErrorFailedToDeployDependencies, testSourceHandler, e);
                warnings.Add(warning);
            }
        }
    }

    public IEnumerable<DeploymentItem> GetSatellites(IEnumerable<DeploymentItem> deploymentItems, string testSourceHandler, IList<string> warnings)
    {
        List<DeploymentItem> satellites = [];
        foreach (DeploymentItem item in deploymentItems)
        {
            // We do not care about deployment items which are directories because in that case we deploy all files underneath anyway.
            string? path = null;
            try
            {
                path = GetFullPathToDeploymentItemSource(item.SourcePath, testSourceHandler);
                path = Path.GetFullPath(path);

                if (StringEx.IsNullOrEmpty(path) || !AssemblyUtility.IsAssemblyExtension(Path.GetExtension(path))
                    || !FileUtility.DoesFileExist(path) || !AssemblyUtility.IsAssembly(path))
                {
                    continue;
                }
            }
            catch (Exception ex) when (ex is ArgumentException or SecurityException or IOException or NotSupportedException)
            {
                // IOException covers PathTooLongException.
                if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsWarningEnabled)
                {
                    PlatformServiceProvider.Instance.AdapterTraceLogger.Warning("DeploymentManager.GetSatellites: {0}", ex);
                }
            }

            // Note: now Path operations with itemPath should not result in any exceptions.
            // path is already canonicalized.

            // If we cannot access satellite due to security, etc, we report warning.
            try
            {
                string itemDir = Path.GetDirectoryName(path);
                List<string> itemSatellites = AssemblyUtility.GetSatelliteAssemblies(path!);
                foreach (string satellite in itemSatellites)
                {
                    DebugEx.Assert(!StringEx.IsNullOrEmpty(satellite), "DeploymentManager.DoDeployment: got empty satellite!");
                    DebugEx.Assert(
                        satellite.StartsWith(itemDir, StringComparison.OrdinalIgnoreCase),
                        "DeploymentManager.DoDeployment: Got satellite that does not start with original item path");

                    string satelliteDir = Path.GetDirectoryName(satellite);

                    string localeDir = itemDir.Length > satelliteDir.Length
                        ? string.Empty
                        : satelliteDir.Substring(itemDir.Length + 1);
                    string relativeOutputDir = Path.Combine(item.RelativeOutputDirectory, localeDir);

                    // Now finally add the item!
                    DeploymentItem satelliteItem = new(satellite, relativeOutputDir, DeploymentItemOriginType.Satellite);
                    DeploymentItemUtility.AddDeploymentItem(satellites, satelliteItem);
                }
            }
            catch (Exception ex) when (ex is ArgumentException or SecurityException or IOException)
            {
                // IOException covers PathTooLongException.
                if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsWarningEnabled)
                {
                    PlatformServiceProvider.Instance.AdapterTraceLogger.Warning("DeploymentManager.GetSatellites: {0}", ex);
                }

                string warning = string.Format(CultureInfo.CurrentCulture, Resource.DeploymentErrorGettingSatellite, item, ex.GetType(), ex.GetExceptionMessage());
                warnings.Add(warning);
            }
        }

        return satellites;
    }

    /// <summary>
    /// Process test storage and add dependent assemblies to dependencyDeploymentItems.
    /// </summary>
    /// <param name="testSourceHandler">The test source.</param>
    /// <param name="configFile">The config file.</param>
    /// <param name="deploymentItems">Deployment items.</param>
    /// <param name="warnings">Warnings.</param>
    private void AddDependencies(string testSourceHandler, string? configFile, IList<DeploymentItem> deploymentItems, IList<string> warnings)
    {
        DebugEx.Assert(!StringEx.IsNullOrEmpty(testSourceHandler), "testSourceHandler should not be null or empty.");

        // config file can be null.
        DebugEx.Assert(deploymentItems != null, "deploymentItems should not be null.");
        DebugEx.Assert(Path.IsPathRooted(testSourceHandler), "path should be rooted.");

        var sw = Stopwatch.StartNew();

        // Note: if this is not an assembly we simply return empty array, also:
        //       we do recursive search and report missing.
        IReadOnlyList<string> references = AssemblyUtility.GetFullPathToDependentAssemblies(testSourceHandler, configFile, out IList<string>? warningList);
        foreach (string warning in warningList)
        {
            warnings.Add(warning);
        }

        if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled)
        {
            PlatformServiceProvider.Instance.AdapterTraceLogger.Info("DeploymentManager: Source:{0} has following references", testSourceHandler);
            PlatformServiceProvider.Instance.AdapterTraceLogger.Info("DeploymentManager: Resolving dependencies took {0} ms", sw.ElapsedMilliseconds);
        }

        foreach (string reference in references)
        {
            DeploymentItem deploymentItem = new(reference, string.Empty, DeploymentItemOriginType.Dependency);
            DeploymentItemUtility.AddDeploymentItem(deploymentItems, deploymentItem);

            if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled)
            {
                PlatformServiceProvider.Instance.AdapterTraceLogger.Info("DeploymentManager: Reference:{0} ", reference);
            }
        }
    }
#endif
}

#endif
