module Beaker
  module DSL
    module InstallUtils
      #
      # This module contains methods useful for Windows installs
      #
      module WindowsUtils
        # Given a host, returns it's system TEMP path
        #
        # @param [Host] host An object implementing {Beaker::Hosts}'s interface.
        #
        # @return [String] system temp path
        def get_system_temp_path(host)
          host.system_temp_path
        end
        alias get_temp_path get_system_temp_path

        # Generates commands to be inserted into a Windows batch file to launch an MSI install
        # @param [String] msi_path The path of the MSI - can be a local Windows style file path like
        #                   c:\temp\puppet.msi OR a url like https://download.com/puppet.msi or file://c:\temp\puppet.msi
        # @param  [Hash{String=>String}] msi_opts MSI installer options
        #                   See https://docs.puppetlabs.com/guides/install_puppet/install_windows.html#msi-properties
        # @param [String] log_path The path to write the MSI log - must be a local Windows style file path
        #
        # @api private
        def msi_install_script(msi_path, msi_opts, log_path)
          # msiexec requires backslashes in file paths launched under cmd.exe start /w
          url_pattern = %r{^(https?|file)://}
          msi_path = msi_path.gsub(%r{/}, '\\') if msi_path !~ url_pattern

          msi_params = msi_opts.map { |k, v| "#{k}=#{v}" }.join(' ')

          # msiexec requires quotes around paths with backslashes - c:\ or file://c:\
          # not strictly needed for http:// but it simplifies this code
          batch_contents = <<~BATCH
            start /w msiexec.exe /i "#{msi_path}" /qn /L*V #{log_path} #{msi_params}
            exit /B %errorlevel%
          BATCH
        end

        # Given a host, path to MSI and MSI options, will create a batch file
        #   on the host, returning the path to the randomized batch file and
        #   the randomized log file
        #
        # @param [Host] host An object implementing {Beaker::Hosts}'s interface.
        # @param [String] msi_path The path of the MSI - can be a local Windows
        #   style file path like c:\temp\puppet.msi OR a url like
        #   https://download.com/puppet.msi or file://c:\temp\puppet.msi
        # @param  [Hash{String=>String}] msi_opts MSI installer options
        #   See https://docs.puppetlabs.com/guides/install_puppet/install_windows.html#msi-properties
        #
        # @api private
        # @return [String, String] path to the batch file, patch to the log file
        def create_install_msi_batch_on(host, msi_path, msi_opts)
          timestamp = Time.new.strftime('%Y-%m-%d_%H.%M.%S')
          tmp_path = host.system_temp_path
          tmp_path.gsub!('/', '\\')

          batch_name = "install-puppet-msi-#{timestamp}.bat"
          batch_path = "#{tmp_path}#{host.scp_separator}#{batch_name}"
          log_path = "#{tmp_path}\\install-puppet-#{timestamp}.log"

          Tempfile.open(batch_name) do |tmp_file|
            batch_contents = msi_install_script(msi_path, msi_opts, log_path)

            File.open(tmp_file.path, 'w') { |file| file.puts(batch_contents) }
            host.do_scp_to(tmp_file.path, batch_path, {})
          end

          [batch_path, log_path]
        end

        # Given hosts construct a PATH that includes puppetbindir, facterbindir and hierabindir
        # @param [Host, Array<Host>, String, Symbol] hosts    One or more hosts to act upon,
        #                            or a role (String or Symbol) that identifies one or more hosts.
        # @param [String] msi_path The path of the MSI - can be a local Windows style file path like
        #                   c:\temp\puppet.msi OR a url like https://download.com/puppet.msi or file://c:\temp\puppet.msi
        # @param  [Hash{String=>String}] msi_opts MSI installer options
        #                   See https://docs.puppetlabs.com/guides/install_puppet/install_windows.html#msi-properties
        # @option msi_opts [String] INSTALLIDIR Where Puppet and its dependencies should be installed.
        #                  (Defaults vary based on operating system and intaller architecture)
        #                  Requires Puppet 2.7.12 / PE 2.5.0
        # @option msi_opts [String] PUPPET_MASTER_SERVER The hostname where the puppet master server can be reached.
        #                  (Defaults to puppet)
        #                  Requires Puppet 2.7.12 / PE 2.5.0
        # @option msi_opts [String] PUPPET_CA_SERVER The hostname where the CA puppet master server can be reached, if you are using multiple masters and only one of them is acting as the CA.
        #                  (Defaults the value of PUPPET_MASTER_SERVER)
        #                  Requires Puppet 2.7.12 / PE 2.5.0
        # @option msi_opts [String] PUPPET_AGENT_CERTNAME The node’s certificate name, and the name it uses when requesting catalogs. This will set a value for
        #                  (Defaults to the node's fqdn as discovered by facter fqdn)
        #                  Requires Puppet 2.7.12 / PE 2.5.0
        # @option msi_opts [String] PUPPET_AGENT_ENVIRONMENT The node’s environment.
        #                  (Defaults to production)
        #                  Requires Puppet 3.3.1 / PE 3.1.0
        # @option msi_opts [String] PUPPET_AGENT_STARTUP_MODE Whether the puppet agent service should run (or be allowed to run)
        #                  (Defaults to Manual - valid values are Automatic, Manual or Disabled)
        #                  Requires Puppet 3.4.0 / PE 3.2.0
        # @option msi_opts [String] PUPPET_AGENT_ACCOUNT_USER Whether the puppet agent service should run (or be allowed to run)
        #                  (Defaults to LocalSystem)
        #                  Requires Puppet 3.4.0 / PE 3.2.0
        # @option msi_opts [String] PUPPET_AGENT_ACCOUNT_PASSWORD The password to use for puppet agent’s user account
        #                  (No default)
        #                  Requires Puppet 3.4.0 / PE 3.2.0
        # @option msi_opts [String] PUPPET_AGENT_ACCOUNT_DOMAIN The domain of puppet agent’s user account.
        #                  (Defaults to .)
        #                  Requires Puppet 3.4.0 / PE 3.2.0
        # @option opts [Boolean] :debug output the MSI installation log when set to true
        #                 otherwise do not output log (false; default behavior)
        #
        # @example
        #  install_msi_on(hosts, 'c:\puppet.msi', {:debug => true})
        #
        # @api private
        def install_msi_on(hosts, msi_path, msi_opts = {}, opts = {})
          block_on hosts do |host|
            msi_opts['PUPPET_AGENT_STARTUP_MODE'] ||= 'Manual'
            batch_path, log_file = create_install_msi_batch_on(host, msi_path, msi_opts)
            # Powershell command looses an escaped slash resulting in cygwin relative path
            # See https://github.com/puppetlabs/beaker/pull/1626#issuecomment-621341555
            log_file_escaped = log_file.gsub('\\', '\\\\\\')
            # begin / rescue here so that we can reuse existing error msg propagation
            begin
              # 1641 = ERROR_SUCCESS_REBOOT_INITIATED
              # 3010 = ERROR_SUCCESS_REBOOT_REQUIRED
              on host, Command.new("\"#{batch_path}\"", [], { cmdexe: true }), acceptable_exit_codes: [0, 1641, 3010]
            rescue StandardError
              logger.info(file_contents_on(host, log_file_escaped))
              raise
            end

            logger.info(file_contents_on(host, log_file_escaped)) if opts[:debug]

            unless host.is_cygwin?
              # Enable the PATH updates
              host.close

              # Some systems require a full reboot to trigger the enabled path
              host.reboot unless on(host, Command.new('puppet -h', [], { cmdexe: true }),
                                    accept_all_exit_codes: true).exit_code == 0
            end

            # verify service status post install
            # if puppet service exists, then pe-puppet is not queried
            # if puppet service does not exist, pe-puppet is queried and that exit code is used
            # therefore, this command will always exit 0 if either service is installed
            #
            # We also take advantage of this output to verify the startup
            # settings are honored as supplied to the MSI
            on host, Command.new('sc qc puppet || sc qc pe-puppet', [], { cmdexe: true }) do |result|
              output = result.stdout
              startup_mode = msi_opts['PUPPET_AGENT_STARTUP_MODE'].upcase

              search = case startup_mode
                       when 'AUTOMATIC'
                         { code: 2, name: 'AUTO_START' }
                       when 'MANUAL'
                         { code: 3, name: 'DEMAND_START' }
                       when 'DISABLED'
                         { code: 4, name: 'DISABLED' }
                       end

              if output !~ /^\s+START_TYPE\s+:\s+#{search[:code]}\s+#{search[:name]}/
                raise "puppet service startup mode did not match supplied MSI option '#{startup_mode}'"
              end
            end

            # (PA-514) value for PUPPET_AGENT_STARTUP_MODE should be present in
            # registry and honored after install/upgrade.
            reg_key = if host.is_x86_64?
                        'HKLM\\SOFTWARE\\Wow6432Node\\Puppet Labs\\PuppetInstaller'
                      else
                        'HKLM\\SOFTWARE\\Puppet Labs\\PuppetInstaller'
                      end
            reg_query_command = %(reg query "#{reg_key}" /v "RememberedPuppetAgentStartupMode" | findstr #{msi_opts['PUPPET_AGENT_STARTUP_MODE']})
            on host, Command.new(reg_query_command, [], { cmdexe: true })

            # emit the misc/versions.txt file which contains component versions for
            # puppet, facter, hiera, pxp-agent, packaging and vendored Ruby
            [
              "'%PROGRAMFILES%\\Puppet Labs\\puppet\\misc\\versions.txt'",
              "'%PROGRAMFILES(X86)%\\Puppet Labs\\puppet\\misc\\versions.txt'",
            ].each do |path|
              result = on(host, "cmd /c type #{path}", accept_all_exit_codes: true)
              if result.exit_code == 0
                logger.info(result.stdout)
                break
              end
            end
          end
        end

        # Installs a specified msi path on given hosts
        # @param [Host, Array<Host>, String, Symbol] hosts    One or more hosts to act upon,
        #                            or a role (String or Symbol) that identifies one or more hosts.
        # @param [String] msi_path The path of the MSI - can be a local Windows style file path like
        #                   c:\temp\foo.msi OR a url like https://download.com/foo.msi or file://c:\temp\foo.msi
        # @param  [Hash{String=>String}] msi_opts MSI installer options
        # @option opts [Boolean] :debug output the MSI installation log when set to true
        #                 otherwise do not output log (false; default behavior)
        #
        # @example
        #  generic_install_msi_on(hosts, 'https://releases.hashicorp.com/vagrant/1.8.4/vagrant_1.8.4.msi', {}, {:debug => true})
        #
        # @api private
        def generic_install_msi_on(hosts, msi_path, msi_opts = {}, opts = {})
          block_on hosts do |host|
            batch_path, log_file = create_install_msi_batch_on(host, msi_path, msi_opts)
            # Powershell command looses an escaped slash resulting in cygwin relative path
            # See https://github.com/puppetlabs/beaker/pull/1626#issuecomment-621341555
            log_file_escaped = log_file.gsub('\\', '\\\\\\')
            # begin / rescue here so that we can reuse existing error msg propagation
            begin
              # 1641 = ERROR_SUCCESS_REBOOT_INITIATED
              # 3010 = ERROR_SUCCESS_REBOOT_REQUIRED
              on host, Command.new("\"#{batch_path}\"", [], { cmdexe: true }), acceptable_exit_codes: [0, 1641, 3010]
            rescue StandardError
              logger.info(file_contents_on(host, log_file_escaped))

              raise
            end

            logger.info(file_contents_on(host, log_file_escaped)) if opts[:debug]

            host.close unless host.is_cygwin?
          end
        end
      end
    end
  end
end