// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.AspNetCore.Server.IntegrationTesting.Common;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Server.IntegrationTesting
{
    public class RemoteWindowsDeployer : ApplicationDeployer
    {
        /// <summary>
        /// Example: If the share path is '\\dir1\dir2', then this returns the full path to the
        /// deployed folder. Example: '\\dir1\dir2\048f6c99-de3e-488a-8020-f9eb277818d9'
        /// </summary>
        private string _deployedFolderPathInFileShare;
        private readonly RemoteWindowsDeploymentParameters _deploymentParameters;
        private bool _isDisposed;
        private static readonly Lazy<Scripts> _scripts = new Lazy<Scripts>(() => CopyEmbeddedScriptFilesToDisk());

        public RemoteWindowsDeployer(RemoteWindowsDeploymentParameters deploymentParameters, ILoggerFactory loggerFactory)
            : base(deploymentParameters, loggerFactory)
        {
            _deploymentParameters = deploymentParameters;

            if (_deploymentParameters.ServerType != ServerType.IIS
                && _deploymentParameters.ServerType != ServerType.Kestrel
                && _deploymentParameters.ServerType != ServerType.HttpSys)
            {
                throw new InvalidOperationException($"Server type {_deploymentParameters.ServerType} is not supported for remote deployment." +
                    $" Supported server types are {nameof(ServerType.Kestrel)}, {nameof(ServerType.IIS)} and {nameof(ServerType.HttpSys)}");
            }

            if (string.IsNullOrEmpty(_deploymentParameters.ServerName))
            {
                throw new ArgumentException($"Invalid value '{_deploymentParameters.ServerName}' for {nameof(RemoteWindowsDeploymentParameters.ServerName)}");
            }

            if (string.IsNullOrEmpty(_deploymentParameters.ServerAccountName))
            {
                throw new ArgumentException($"Invalid value '{_deploymentParameters.ServerAccountName}' for {nameof(RemoteWindowsDeploymentParameters.ServerAccountName)}." +
                    " Account credentials are required to enable creating a powershell session to the remote server.");
            }

            if (string.IsNullOrEmpty(_deploymentParameters.ServerAccountPassword))
            {
                throw new ArgumentException($"Invalid value '{_deploymentParameters.ServerAccountPassword}' for {nameof(RemoteWindowsDeploymentParameters.ServerAccountPassword)}." +
                    " Account credentials are required to enable creating a powershell session to the remote server.");
            }

            if (_deploymentParameters.ApplicationType == ApplicationType.Portable
                && string.IsNullOrEmpty(_deploymentParameters.DotnetRuntimePath))
            {
                throw new ArgumentException($"Invalid value '{_deploymentParameters.DotnetRuntimePath}' for {nameof(RemoteWindowsDeploymentParameters.DotnetRuntimePath)}. " +
                    "It must be non-empty for portable apps.");
            }

            if (string.IsNullOrEmpty(_deploymentParameters.RemoteServerFileSharePath))
            {
                throw new ArgumentException($"Invalid value for {nameof(RemoteWindowsDeploymentParameters.RemoteServerFileSharePath)}." +
                    " . A file share is required to copy the application's published output.");
            }

            if (string.IsNullOrEmpty(_deploymentParameters.ApplicationBaseUriHint))
            {
                throw new ArgumentException($"Invalid value for {nameof(RemoteWindowsDeploymentParameters.ApplicationBaseUriHint)}.");
            }
        }

        public override async Task<DeploymentResult> DeployAsync()
        {
            using (Logger.BeginScope("Deploy"))
            {
                if (_isDisposed)
                {
                    throw new ObjectDisposedException("This instance of deployer has already been disposed.");
                }

                // Publish the app to a local temp folder on the machine where the test is running
                DotnetPublish();

                if (_deploymentParameters.ServerType == ServerType.IIS)
                {
                    UpdateWebConfig();
                }

                var folderId = Guid.NewGuid().ToString();
                _deployedFolderPathInFileShare = Path.Combine(_deploymentParameters.RemoteServerFileSharePath, folderId);

                DirectoryCopy(
                    _deploymentParameters.PublishedApplicationRootPath,
                    _deployedFolderPathInFileShare,
                    copySubDirs: true);
                Logger.LogInformation($"Copied the locally published folder to the file share path '{_deployedFolderPathInFileShare}'");

                await RunScriptAsync("StartServer");

                return new DeploymentResult(
                    LoggerFactory,
                    DeploymentParameters,
                    DeploymentParameters.ApplicationBaseUriHint);
            }
        }

        public override void Dispose()
        {
            using (Logger.BeginScope("Dispose"))
            {
                if (_isDisposed)
                {
                    return;
                }

                _isDisposed = true;

                try
                {
                    Logger.LogInformation($"Stopping the application on the server '{_deploymentParameters.ServerName}'");
                    RunScriptAsync("StopServer").Wait();
                }
                catch (Exception ex)
                {
                    Logger.LogWarning(0, "Failed to stop the server.", ex);
                }

                try
                {
                    Logger.LogInformation($"Deleting the deployed folder '{_deployedFolderPathInFileShare}'");
                    Directory.Delete(_deployedFolderPathInFileShare, recursive: true);
                }
                catch (Exception ex)
                {
                    Logger.LogWarning(0, $"Failed to delete the deployed folder '{_deployedFolderPathInFileShare}'.", ex);
                }

                try
                {
                    Logger.LogInformation($"Deleting the locally published folder '{DeploymentParameters.PublishedApplicationRootPath}'");
                    Directory.Delete(DeploymentParameters.PublishedApplicationRootPath, recursive: true);
                }
                catch (Exception ex)
                {
                    Logger.LogWarning(0, $"Failed to delete the locally published folder '{DeploymentParameters.PublishedApplicationRootPath}'.", ex);
                }
            }
        }

        private void UpdateWebConfig()
        {
            var webConfigFilePath = Path.Combine(_deploymentParameters.PublishedApplicationRootPath, "web.config");
            var webConfig = XDocument.Load(webConfigFilePath);
            var aspNetCoreSection = webConfig.Descendants("aspNetCore")
                .Single();

            // if the dotnet runtime path is specified, update the published web.config file to have that path
            if (!string.IsNullOrEmpty(_deploymentParameters.DotnetRuntimePath))
            {
                aspNetCoreSection.SetAttributeValue(
                    "processPath",
                    Path.Combine(_deploymentParameters.DotnetRuntimePath, "dotnet.exe"));
            }

            var environmentVariablesSection = aspNetCoreSection.Elements("environmentVariables").FirstOrDefault();
            if (environmentVariablesSection == null)
            {
                environmentVariablesSection = new XElement("environmentVariables");
                aspNetCoreSection.Add(environmentVariablesSection);
            }

            foreach (var envVariablePair in _deploymentParameters.EnvironmentVariables)
            {
                var environmentVariable = new XElement("environmentVariable");
                environmentVariable.SetAttributeValue("name", envVariablePair.Key);
                environmentVariable.SetAttributeValue("value", envVariablePair.Value);
                environmentVariablesSection.Add(environmentVariable);
            }

            if(Logger.IsEnabled(LogLevel.Trace))
            {
                Logger.LogTrace($"Config File Content:{Environment.NewLine}===START CONFIG==={Environment.NewLine}{{configContent}}{Environment.NewLine}===END CONFIG===", webConfig.ToString());
            }

            using (var fileStream = File.Open(webConfigFilePath, FileMode.Open))
            {
                webConfig.Save(fileStream);
            }
        }

        private async Task RunScriptAsync(string serverAction)
        {
            using (Logger.BeginScope($"RunScript:{serverAction}"))
            {
                var remotePSSessionHelperScript = _scripts.Value.RemotePSSessionHelper;

                string executablePath = null;
                string executableParameters = null;
                var applicationName = new DirectoryInfo(DeploymentParameters.ApplicationPath).Name;
                if (DeploymentParameters.ApplicationType == ApplicationType.Portable)
                {
                    executablePath = "dotnet.exe";
                    executableParameters = Path.Combine(_deployedFolderPathInFileShare, applicationName + ".dll");
                }
                else
                {
                    executablePath = Path.Combine(_deployedFolderPathInFileShare, applicationName + ".exe");
                }

                var parameterBuilder = new StringBuilder();
                parameterBuilder.Append($"\"{remotePSSessionHelperScript}\"");
                parameterBuilder.Append($" -serverName {_deploymentParameters.ServerName}");
                parameterBuilder.Append($" -accountName {_deploymentParameters.ServerAccountName}");
                parameterBuilder.Append($" -accountPassword {_deploymentParameters.ServerAccountPassword}");
                parameterBuilder.Append($" -deployedFolderPath {_deployedFolderPathInFileShare}");

                if (!string.IsNullOrEmpty(_deploymentParameters.DotnetRuntimePath))
                {
                    parameterBuilder.Append($" -dotnetRuntimePath \"{_deploymentParameters.DotnetRuntimePath}\"");
                }

                parameterBuilder.Append($" -executablePath \"{executablePath}\"");

                if (!string.IsNullOrEmpty(executableParameters))
                {
                    parameterBuilder.Append($" -executableParameters \"{executableParameters}\"");
                }

                parameterBuilder.Append($" -serverType {_deploymentParameters.ServerType}");
                parameterBuilder.Append($" -serverAction {serverAction}");
                parameterBuilder.Append($" -applicationBaseUrl {_deploymentParameters.ApplicationBaseUriHint}");
                var environmentVariables = string.Join("`,", _deploymentParameters.EnvironmentVariables.Select(envVariable => $"{envVariable.Key}={envVariable.Value}"));
                parameterBuilder.Append($" -environmentVariables \"{environmentVariables}\"");

                var startInfo = new ProcessStartInfo
                {
                    FileName = "powershell.exe",
                    Arguments = parameterBuilder.ToString(),
                    UseShellExecute = false,
                    CreateNoWindow = true,
                    RedirectStandardError = true,
                    RedirectStandardOutput = true,
                    RedirectStandardInput = true
                };

                using (var runScriptsOnRemoteServerProcess = new Process() { StartInfo = startInfo })
                {
                    runScriptsOnRemoteServerProcess.EnableRaisingEvents = true;
                    runScriptsOnRemoteServerProcess.Exited += (sender, exitedArgs) =>
                    {
                        Logger.LogInformation($"[{_deploymentParameters.ServerName} {serverAction} stdout]: script complete");
                    };

                    runScriptsOnRemoteServerProcess.StartAndCaptureOutAndErrToLogger(serverAction, Logger);

                    // Wait a second for the script to run or fail. The StartServer script will only terminate when the Deployer is disposed,
                    // so we don't want to wait for it to terminate here because it would deadlock.
                    await Task.Delay(TimeSpan.FromMinutes(1));

                    if (runScriptsOnRemoteServerProcess.HasExited && runScriptsOnRemoteServerProcess.ExitCode != 0)
                    {
                        throw new Exception($"Failed to execute the script on '{_deploymentParameters.ServerName}'.");
                    }
                }
            }
        }

        private static void DirectoryCopy(string sourceDirName, string destDirName, bool copySubDirs)
        {
            var dir = new DirectoryInfo(sourceDirName);

            if (!dir.Exists)
            {
                throw new DirectoryNotFoundException(
                    "Source directory does not exist or could not be found: "
                    + sourceDirName);
            }

            var dirs = dir.GetDirectories();
            if (!Directory.Exists(destDirName))
            {
                Directory.CreateDirectory(destDirName);
            }

            var files = dir.GetFiles();
            foreach (var file in files)
            {
                var temppath = Path.Combine(destDirName, file.Name);
                file.CopyTo(temppath, false);
            }

            if (copySubDirs)
            {
                foreach (var subdir in dirs)
                {
                    var temppath = Path.Combine(destDirName, subdir.Name);
                    DirectoryCopy(subdir.FullName, temppath, copySubDirs);
                }
            }
        }

        private static Scripts CopyEmbeddedScriptFilesToDisk()
        {
            var embeddedFileNames = new[] { "RemotePSSessionHelper.ps1", "StartServer.ps1", "StopServer.ps1" };

            // Copy the scripts from this assembly's embedded resources to the temp path on the machine where these
            // tests are being run
            var assembly = typeof(RemoteWindowsDeployer).GetTypeInfo().Assembly;
            var embeddedFileProvider = new EmbeddedFileProvider(
                assembly,
                $"{assembly.GetName().Name}.Deployers.RemoteWindowsDeployer");

            var filesOnDisk = new string[embeddedFileNames.Length];
            for (var i = 0; i < embeddedFileNames.Length; i++)
            {
                var embeddedFileName = embeddedFileNames[i];
                var physicalFilePath = Path.Combine(Path.GetTempPath(), embeddedFileName);
                var sourceStream = embeddedFileProvider
                    .GetFileInfo(embeddedFileName)
                    .CreateReadStream();

                using (sourceStream)
                {
                    var destinationStream = File.Create(physicalFilePath);
                    using (destinationStream)
                    {
                        sourceStream.CopyTo(destinationStream);
                    }
                }

                filesOnDisk[i] = physicalFilePath;
            }

            var scripts = new Scripts(filesOnDisk[0], filesOnDisk[1], filesOnDisk[2]);

            return scripts;
        }

        private class Scripts
        {
            public Scripts(string remotePSSessionHelper, string startServer, string stopServer)
            {
                RemotePSSessionHelper = remotePSSessionHelper;
                StartServer = startServer;
                StopServer = stopServer;
            }

            public string RemotePSSessionHelper { get; }

            public string StartServer { get; }

            public string StopServer { get; }
        }
    }
}
