require 'guard/minitest/inspector'
require 'English'

module Guard
  class Minitest < Plugin
    class Runner
      attr_accessor :inspector

      def initialize(options = {})
        @options = {
          all_after_pass:     false,
          bundler:            File.exist?("#{Dir.pwd}/Gemfile"),
          rubygems:           false,
          drb:                false,
          zeus:               false,
          spring:             false,
          all_env:            {},
          env:                {},
          include:            [],
          test_folders:       %w(test spec),
          test_file_patterns: %w(*_test.rb test_*.rb *_spec.rb),
          cli:                nil,
          autorun:            true
        }.merge(options)

        parse_deprecated_options

        [:test_folders, :test_file_patterns].each do |k|
          @options[k] = Array(@options[k]).uniq.compact
        end

        @inspector = Inspector.new(test_folders, test_file_patterns)
      end

      def run(paths, options = {})
        return unless options[:all] || !paths.empty?

        message = "Running: #{options[:all] ? 'all tests' : paths.join(' ')}"
        Compat::UI.info message, reset: true

        begin
          status = _run_possibly_bundled_command(paths, options[:all])
        rescue Errno::ENOENT => e
          Compat::UI.error e.message
          throw :task_has_failed
        end

        success = status.zero?

        # When using zeus or spring, the Guard::Minitest::Reporter can't be used because the minitests run in another
        # process, but we can use the exit status of the client process to distinguish between :success and :failed.
        if zeus? || spring?
          Compat::UI.notify(message, title: 'Minitest results', image: success ? :success : :failed)
        end

        run_all_coz_ok = @options[:all_after_pass] && success && !options[:all]
        run_all_coz_ok ?  run_all : success
      end

      def run_all
        paths = inspector.clean_all
        run(paths, all: true)
      end

      def run_on_modifications(paths = [])
        paths = inspector.clean(paths)
        run(paths, all: all_paths?(paths))
      end

      def run_on_additions(_paths)
        inspector.clear_memoized_test_files
        true
      end

      def run_on_removals(_paths)
        inspector.clear_memoized_test_files
      end

      private

      def cli_options
        @cli_options ||= Array(@options[:cli])
      end

      def bundler?
        @options[:bundler] && !@options[:spring]
      end

      def rubygems?
        !bundler? && @options[:rubygems]
      end

      def drb?
        @options[:drb]
      end

      def zeus?
        @options[:zeus].is_a?(String) || @options[:zeus]
      end

      def spring?
        @options[:spring].is_a?(String) || @options[:spring]
      end

      def all_after_pass?
        @options[:all_after_pass]
      end

      def test_folders
        @options[:test_folders]
      end

      def include_folders
        @options[:include]
      end

      def test_file_patterns
        @options[:test_file_patterns]
      end

      def autorun?
        @options[:autorun]
      end

      def _run(*args)
        Compat::UI.debug "Running: #{args.join(' ')}"
        return $CHILD_STATUS.exitstatus unless Kernel.system(*args).nil?

        fail Errno::ENOENT, args.join(' ')
      end

      def _run_possibly_bundled_command(paths, all)
        args = minitest_command(paths, all)
        bundler_env = !bundler? && defined?(::Bundler)
        bundler_env ? ::Bundler.with_original_env { _run(*args) } : _run(*args)
      end

      def _commander(paths)
        return drb_command(paths) if drb?
        return zeus_command(paths) if zeus?
        return spring_command(paths) if spring?
        ruby_command(paths)
      end

      def minitest_command(paths, all)
        cmd_parts = []

        cmd_parts << 'bundle exec' if bundler?
        cmd_parts << _commander(paths)

        [cmd_parts.compact.join(' ')].tap do |args|
          env = generate_env(all)
          args.unshift(env) if env.length > 0
        end
      end

      def drb_command(paths)
        %w(testdrb) + generate_includes(false) + relative_paths(paths)
      end

      def zeus_command(paths)
        command = @options[:zeus].is_a?(String) ? @options[:zeus] : 'test'
        ['zeus', command] + relative_paths(paths)
      end

      def spring_command(paths)
        command = @options[:spring].is_a?(String) ? @options[:spring] : 'bin/rake test'
        cmd_parts = [command]
        cmd_parts << File.expand_path('../runners/old_runner.rb', __FILE__) unless Utils.minitest_version_gte_5? || command != 'spring testunit'
        if cli_options.length > 0
          cmd_parts + paths + ['--'] + cli_options
        else
          cmd_parts + paths
        end
      end

      def ruby_command(paths)
        cmd_parts  = ['ruby']
        cmd_parts.concat(generate_includes)
        cmd_parts << '-r rubygems' if rubygems?
        cmd_parts << '-r bundler/setup' if bundler?
        cmd_parts << '-r minitest/autorun' if autorun?
        cmd_parts.concat(paths.map { |path| "-r ./#{path}" })

        unless Utils.minitest_version_gte_5?
          cmd_parts << "-r #{File.expand_path('../runners/old_runner.rb', __FILE__)}"
        end

        # All the work is done through minitest/autorun
        # and requiring the test files, so this is just
        # a placeholder so Ruby doesn't try to exceute
        # code from STDIN.
        cmd_parts << '-e ""'

        cmd_parts << '--'
        cmd_parts << '--guard'
        cmd_parts += cli_options
        cmd_parts
      end

      def generate_includes(include_test_folders = true)
        if include_test_folders
          folders = test_folders + include_folders
        else
          folders = include_folders
        end

        folders.map { |f| %(-I"#{f}") }
      end

      def generate_env(all = false)
        base_env.merge(all ? all_env : {})
      end

      def base_env
        Hash[(@options[:env] || {}).map { |key, value| [key.to_s, value.to_s] }]
      end

      def all_env
        return { @options[:all_env].to_s => 'true' } unless @options[:all_env].is_a? Hash
        Hash[@options[:all_env].map { |key, value| [key.to_s, value.to_s] }]
      end

      def relative_paths(paths)
        paths.map { |p| "./#{p}" }
      end

      def all_paths?(paths)
        paths == inspector.all_test_files
      end

      def parse_deprecated_options
        if @options.key?(:notify)
          # TODO: no coverage
          Compat::UI.info %(DEPRECATION WARNING: The :notify option is deprecated. Guard notification configuration is used.)
        end

        [:seed, :verbose].each do |key|
          next unless (value = @options.delete(key))

          final_value = "--#{key}"
          final_value << " #{value}" unless [TrueClass, FalseClass].include?(value.class)
          cli_options << final_value

          Compat::UI.info %(DEPRECATION WARNING: The :#{key} option is deprecated. Pass standard command line argument "--#{key}" to Minitest with the :cli option.)
        end
      end
    end
  end
end
