#
# Copyright:: Copyright (c) 2014-2019 Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require "chef-cli/exceptions"
require "chef-cli/helpers"

module ChefWorkstation
  class MissingComponentError < RuntimeError
    def initialize(component_name, reason)
      super("Component #{component_name} is missing.\nReason: #{reason}")
    end
  end

  class ComponentTest
    class NullTestResult
      def exitstatus
        0
      end

      def stdout
        ""
      end

      def stderr
        ""
      end
    end

    DEFAULT_TEST = lambda { |context| NullTestResult.new }

    include ChefCLI::Helpers

    attr_accessor :base_dir

    attr_writer :omnibus_root

    attr_reader :name

    def initialize(name)
      @name = name
      @unit_test = DEFAULT_TEST
      @integration_test = DEFAULT_TEST
      @smoke_test = DEFAULT_TEST
      @base_dir = nil
      @gem_name_for_base_dir = nil
    end

    def unit_test(&test_block)
      @unit_test = test_block
    end

    def run_unit_test
      instance_eval(&@unit_test)
    end

    def integration_test(&test_block)
      @integration_test = test_block
    end

    def run_integration_test
      instance_eval(&@integration_test)
    end

    def smoke_test(&test_block)
      @smoke_test = test_block
    end

    def run_smoke_test
      instance_eval(&@smoke_test)
    end

    def bin(binary)
      File.join(omnibus_bin_dir, binary)
    end

    def embedded_bin(binary)
      File.join(omnibus_embedded_bin_dir, binary)
    end

    def sh(command, options = {})
      puts command
      combined_opts = default_command_options.merge(options)

      # Env is a hash, so it needs to be merged separately
      if options.key?(:env)
        combined_opts[:env] = default_command_options[:env].merge(options[:env])
      end
      system_command(command, combined_opts)
    end

    # Just like #sh but raises an error if the the command returns an
    # unexpected exit code.
    #
    # Most verification steps just run a single command, then
    # ChefCLI::Command::Verify#invoke_tests handles the results by inspecting
    # the return value of the test block. For tests that run a lot of commands,
    # this is inconvenient so you can use #sh! instead.
    def sh!(*args)
      sh(*args).tap(&:error!)
    end

    # Run a command, if the command returns zero, raise an error,
    # otherwise, if it returns non-zero or doesn't exist,
    # return a passing command so that the test parser doesn't
    # crash.
    def fail_if_exit_zero(cmd_string, failure_string = "")
      result = sh(cmd_string)
      if result.status.exitstatus == 0
        raise failure_string
      else
        sh("true")
      end
    rescue Errno::ENOENT
      sh("true")
    end

    def nix_platform_native_bin_dir
      if /darwin/ =~ RUBY_PLATFORM
        "/usr/local/bin"
      else
        "/usr/bin"
      end
    end

    def run_in_tmpdir(command, options = {})
      tmpdir do |dir|
        options[:cwd] = dir
        sh(command, options)
      end
    end

    def tmpdir
      Dir.mktmpdir do |tmpdir|
        yield tmpdir
      end
    end

    def assert_present!
      unless File.exist?( component_path )
        raise MissingComponentError.new(name, "Could not find #{component_path}")
      end
    rescue Gem::LoadError => e
      raise MissingComponentError.new(name, e)
    end

    def default_command_options
      {
        cwd: component_path,
        env: {
          # Add the embedded/bin to the PATH so that bundle executable can
          # be found while running the tests.
          path_variable_key => omnibus_path,
          "CHEF_LICENSE" => "accept-no-persist",
        },
        timeout: 3600,
      }
    end

    def component_path
      if base_dir
        File.join(omnibus_root, base_dir)
      elsif gem_base_dir
        gem_base_dir
      else
        raise "`base_dir` or `gem_base_dir` must be defined for component `#{name}`"
      end
    end

    def gem_base_dir
      return nil if @gem_name_for_base_dir.nil?

      # There is no way to say "give me the latest prerelease OR normal version of this gem.
      # So we first ask if there is a normal version, and if there is not, we ask if there
      # is a prerelease version.  ">= 0.a" is how we ask for a prerelease version, because a
      # prerelease version is defined as "any version with a letter in it."
      gem = Gem::Specification.find_by_name(@gem_name_for_base_dir)
      gem ||= Gem::Specification.find_by_name(@gem_name_for_base_dir, ">= 0.a")
      gem.gem_dir
    end

    def gem_base_dir=(gem_name)
      @gem_name_for_base_dir = gem_name
    end

    def omnibus_root
      @omnibus_root || raise("`omnibus_root` must be set before running tests")
    end

    def omnibus_path
      [omnibus_bin_dir, omnibus_embedded_bin_dir, ENV["PATH"]].join(File::PATH_SEPARATOR)
    end

    def path_variable_key
      ENV.keys.grep(/\Apath\Z/i).first
    end

  end
end
