require 'rspec/core/backward_compatibility'
require 'rake'
require 'rake/tasklib'
require 'shellwords'

module RSpec
  module Core
    class RakeTask < ::Rake::TaskLib
      include ::Rake::DSL if defined?(::Rake::DSL)

      # Name of task.
      #
      # default:
      #   :spec
      attr_accessor :name

      # Glob pattern to match files.
      #
      # default:
      #   'spec/**/*_spec.rb'
      attr_accessor :pattern

      # @deprecated
      # Has no effect. The rake task now checks ENV['BUNDLE_GEMFILE'] instead.
      def skip_bundler=(*)
        deprecate("RSpec::Core::RakeTask#skip_bundler=")
      end

      # @deprecated
      # Has no effect. The rake task now checks ENV['BUNDLE_GEMFILE'] instead.
      def gemfile=(*)
        deprecate("RSpec::Core::RakeTask#gemfile=", :replacement => 'ENV["BUNDLE_GEMFILE"]')
      end

      # @deprecated
      # Use ruby_opts="-w" instead.
      #
      # When true, requests that the specs be run with the warning flag set.
      # e.g. "ruby -w"
      #
      # default:
      #   false
      def warning=(true_or_false)
        deprecate("RSpec::Core::RakeTask#warning=", :replacement => 'ruby_opts="-w"')
        @warning = true_or_false
      end

      # Whether or not to fail Rake when an error occurs (typically when examples fail).
      #
      # default:
      #   true
      attr_accessor :fail_on_error

      # A message to print to stderr when there are failures.
      attr_accessor :failure_message

      # Use verbose output. If this is set to true, the task will print the
      # executed spec command to stdout.
      #
      # default:
      #   true
      attr_accessor :verbose

      # Use rcov for code coverage?
      #
      # Due to the many ways `rcov` can run, if this option is enabled, it is
      # required that `require 'rspec/autorun'` appears in `spec_helper`.rb
      #
      # default:
      #   false
      def rcov
        deprecate("RSpec::Core::RakeTask#rcov")
        @rcov
      end

      def rcov=(true_or_false)
        deprecate("RSpec::Core::RakeTask#rcov=")
        @rcov = true_or_false
      end

      # Path to rcov.
      #
      # default:
      #   'rcov'
      def rcov_path
        deprecate("RSpec::Core::RakeTask#rcov_path")
        @rcov_path
      end

      def rcov_path=(path)
        deprecate("RSpec::Core::RakeTask#rcov_path=")
        @rcov_path = path
      end

      # Command line options to pass to rcov.
      #
      # default:
      #   nil
      def rcov_opts
        deprecate("RSpec::Core::RakeTask#rcov_opts")
        @rcov_opts
      end

      def rcov_opts=(opts)
        deprecate("RSpec::Core::RakeTask#rcov_opts=")
        @rcov_opts = opts
      end

      # Command line options to pass to ruby.
      #
      # default:
      #   nil
      attr_accessor :ruby_opts

      # Path to rspec
      #
      # default:
      #   'rspec'
      attr_accessor :rspec_path

      # Command line options to pass to rspec.
      #
      # default:
      #   nil
      attr_accessor :rspec_opts

      # @deprecated
      # Use rspec_opts instead.
      #
      # Command line options to pass to rspec.
      #
      # default:
      #   nil
      def spec_opts=(opts)
        deprecate('RSpec::Core::RakeTask#spec_opts=', :replacement => 'rspec_opts=')
        @rspec_opts = opts
      end

      def initialize(*args, &task_block)
        setup_ivars(args)

        desc "Run RSpec code examples" unless ::Rake.application.last_comment

        task name, *args do |_, task_args|
          RakeFileUtils.send(:verbose, verbose) do
            task_block.call(*[self, task_args].slice(0, task_block.arity)) if task_block
            run_task verbose
          end
        end
      end

      def setup_ivars(args)
        @name = args.shift || :spec
        @rcov_opts, @ruby_opts, @rspec_opts = nil, nil, nil
        @warning, @rcov = false, false
        @verbose, @fail_on_error = true, true

        @rcov_path  = 'rcov'
        @rspec_path = 'rspec'
        @pattern    = './spec{,/*/**}/*_spec.rb'
      end

      def run_task(verbose)
        command = spec_command

        begin
          puts command if verbose
          success = system(command)
        rescue
          puts failure_message if failure_message
        end
        abort("#{command} failed") if fail_on_error unless success
      end

    private

      if "".respond_to?(:shellescape)
        def shellescape(string)
          string.shellescape
        end
      else # 1.8.6's shellwords doesn't provide shellescape :(.
        def shellescape(string)
          string.gsub(/"/, '\"').gsub(/'/, "\\\\'")
        end
      end

      def files_to_run
        if ENV['SPEC']
          FileList[ ENV['SPEC'] ].sort
        else
          FileList[ pattern ].sort.map { |f| shellescape(f) }
        end
      end

      def spec_command
        cmd_parts = []
        cmd_parts << RUBY
        cmd_parts << ruby_opts
        cmd_parts << "-w" if @warning
        cmd_parts << "-S" << runner
        cmd_parts << "-Ispec:lib" << @rcov_opts if @rcov
        cmd_parts << files_to_run
        cmd_parts << "--" if @rcov && rspec_opts
        cmd_parts << rspec_opts
        cmd_parts.flatten.reject(&blank).join(" ")
      end

      def runner
        @rcov ? @rcov_path : rspec_path
      end

      def blank
        lambda {|s| s.nil? || s == ""}
      end

      def deprecate deprecated, opts = {}
        # unless RSpec is loaded, deprecate won't work (simply requiring the
        # deprecate file isn't enough) so this is a check for "is rspec already
        # loaded?" "ok use the main deprecate hook" otherwise "simple fallback"
        # Note that we don't need rspec to be loaded for the rake task to work
        if RSpec.respond_to?(:deprecate)
          RSpec.deprecate deprecated, opts
        else
          warn "DEPRECATION: #{deprecated} is deprecated."
        end
      end

    end
  end
end
