#
# Copyright:: Copyright (c) 2014 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-dk/exceptions'
require 'chef-dk/helpers'

module ChefDK
  class ComponentTest

    class NullTestResult
      def exitstatus
        0
      end

      def stdout
        ""
      end

      def stderr
        ""
      end
    end

    DEFAULT_TEST = lambda { |context| NullTestResult.new }

    include 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 sh(command, options={})
      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
    # ChefDK::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 { |result| result.error! }
    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.exists?( 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
        },
        :timeout => 3600
      }
    end

    def component_path
      if base_dir
        File.join(omnibus_apps_dir, 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 or 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