require "rbconfig"

module RSpec
  module Core
    class Configuration
      include RSpec::Core::Hooks

      def self.add_setting(name, opts={})
        if opts[:alias]
          alias_method name, opts[:alias]
          alias_method "#{name}=", "#{opts[:alias]}="
          alias_method "#{name}?", "#{opts[:alias]}?"
        else
          define_method("#{name}=") {|val| settings[name] = val}
          define_method(name)       { settings.has_key?(name) ? settings[name] : opts[:default] }
          define_method("#{name}?") { send name }
        end
      end

      add_setting :error_stream
      add_setting :output_stream
      add_setting :output, :alias => :output_stream
      add_setting :out, :alias => :output_stream
      add_setting :drb
      add_setting :drb_port
      add_setting :profile_examples
      add_setting :fail_fast
      add_setting :run_all_when_everything_filtered
      add_setting :filter
      add_setting :exclusion_filter
      add_setting :filename_pattern, :default => '**/*_spec.rb'
      add_setting :files_to_run
      add_setting :include_or_extend_modules
      add_setting :backtrace_clean_patterns
      add_setting :tty

      def initialize
        @color_enabled = false
        self.include_or_extend_modules = []
        self.files_to_run = []
        self.backtrace_clean_patterns = [
          /\/lib\d*\/ruby\//,
          /bin\//,
          /gems/,
          /spec\/spec_helper\.rb/,
          /lib\/rspec\/(core|expectations|matchers|mocks)/
        ]

        filter_run_excluding(
          :if     => lambda { |value, metadata| metadata.has_key?(:if) && !value },
          :unless => lambda { |value| value }
        )
      end

      # :call-seq:
      #   add_setting(:name)
      #   add_setting(:name, :default => "default_value")
      #   add_setting(:name, :alias => :other_setting)
      #
      # Use this to add custom settings to the RSpec.configuration object.
      #
      #   RSpec.configuration.add_setting :foo
      #
      # Creates three methods on the configuration object, a setter, a getter,
      # and a predicate:
      #
      #   RSpec.configuration.foo=(value)
      #   RSpec.configuration.foo()
      #   RSpec.configuration.foo?() # returns true if foo returns anything but nil or false
      #
      # Intended for extension frameworks like rspec-rails, so they can add config
      # settings that are domain specific. For example:
      #
      #   RSpec.configure do |c|
      #     c.add_setting :use_transactional_fixtures, :default => true
      #     c.add_setting :use_transactional_examples, :alias => :use_transactional_fixtures
      #   end
      #
      # == Options
      #
      # +add_setting+ takes an optional hash that supports the following
      # keys:
      #
      #   :default => "default value"
      #
      # This sets the default value for the getter and the predicate (which
      # will return +true+ as long as the value is not +false+ or +nil+).
      #
      #   :alias => :other_setting
      #
      # Aliases its setter, getter, and predicate, to those for the
      # +other_setting+.
      def add_setting(name, opts={})
        self.class.add_setting(name, opts)
      end

      def puts(message)
        output_stream.puts(message)
      end

      def settings
        @settings ||= {}
      end

      def clear_inclusion_filter # :nodoc:
        self.filter = nil
      end

      def cleaned_from_backtrace?(line)
        backtrace_clean_patterns.any? { |regex| line =~ regex }
      end

      # Returns the configured mock framework adapter module
      def mock_framework
        settings[:mock_framework] ||= begin
                                        require 'rspec/core/mocking/with_rspec'
                                        RSpec::Core::MockFrameworkAdapter
                                      end
      end

      # Delegates to mock_framework=(framework)
      def mock_with(framework)
        self.mock_framework = framework
      end

      # Sets the mock framework adapter module.
      #
      # +framework+ can be a Symbol or a Module.
      #
      # Given any of :rspec, :mocha, :flexmock, or :rr, configures the named
      # framework.
      #
      # Given :nothing, configures no framework. Use this if you don't use any
      # mocking framework to save a little bit of overhead.
      #
      # Given a Module, includes that module in every example group. The module
      # should adhere to RSpec's mock framework adapter API:
      #
      #   setup_mocks_for_rspec
      #     - called before each example
      #
      #   verify_mocks_for_rspec
      #     - called after each example. Framework should raise an exception
      #       when expectations fail
      #
      #   teardown_mocks_for_rspec
      #     - called after verify_mocks_for_rspec (even if there are errors)
      def mock_framework=(framework)
        case framework
        when Module
          settings[:mock_framework] = framework
        when String, Symbol
          require case framework.to_s
                  when /rspec/i
                    'rspec/core/mocking/with_rspec'
                  when /mocha/i
                    'rspec/core/mocking/with_mocha'
                  when /rr/i
                    'rspec/core/mocking/with_rr'
                  when /flexmock/i
                    'rspec/core/mocking/with_flexmock'
                  else
                    'rspec/core/mocking/with_absolutely_nothing'
                  end
          settings[:mock_framework] = RSpec::Core::MockFrameworkAdapter
        else
        end
      end

      # Returns the configured expectation framework adapter module(s)
      def expectation_frameworks
        settings[:expectation_frameworks] ||= begin
                                               require 'rspec/core/expecting/with_rspec'
                                               [RSpec::Core::ExpectationFrameworkAdapter]
                                             end
      end

      # Delegates to expect_with=([framework])
      def expectation_framework=(framework)
        expect_with([framework])
      end

      # Sets the expectation framework module(s).
      #
      # +frameworks+ can be :rspec, :stdlib, or both 
      #
      # Given :rspec, configures rspec/expectations.
      # Given :stdlib, configures test/unit/assertions
      # Given both, configures both
      def expect_with(*frameworks)
        settings[:expectation_frameworks] = []
        frameworks.each do |framework|
          case framework
          when Symbol
            case framework
            when :rspec
              require 'rspec/core/expecting/with_rspec'
            when :stdlib
              require 'rspec/core/expecting/with_stdlib'
            else
              raise ArgumentError, "#{framework.inspect} is not supported"
            end
            settings[:expectation_frameworks] << RSpec::Core::ExpectationFrameworkAdapter
          end
        end
      end

      def full_backtrace=(bool)
        settings[:backtrace_clean_patterns] = []
      end

      def color_enabled
        @color_enabled && output_to_tty?
      end

      def color_enabled?
        color_enabled
      end

      def color_enabled=(bool)
        return unless bool
        @color_enabled = true
        if bool && ::RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
          unless ENV['ANSICON']
            warn "You must use ANSICON 1.31 or later (http://adoxa.110mb.com/ansicon/) to use colour on Windows"
            @color_enabled = false
          end
        end
      end

      def libs=(libs)
        libs.map {|lib| $LOAD_PATH.unshift lib}
      end

      def requires=(paths)
        paths.map {|path| require path}
      end

      def debug=(bool)
        return unless bool
        begin
          require 'ruby-debug'
          Debugger.start
        rescue LoadError
          raise <<-EOM

#{'*'*50}
You must install ruby-debug to run rspec with the --debug option.

If you have ruby-debug installed as a ruby gem, then you need to either
require 'rubygems' or configure the RUBYOPT environment variable with
the value 'rubygems'.
#{'*'*50}
EOM
        end
      end

      def line_number=(line_number)
        filter_run({ :line_number => line_number.to_i }, true)
      end

      def full_description=(description)
        filter_run({ :full_description => /#{description}/ }, true)
      end

      def add_formatter(formatter_to_use, output=nil)
        formatter_class =
          built_in_formatter(formatter_to_use) ||
          custom_formatter(formatter_to_use) ||
          (raise ArgumentError, "Formatter '#{formatter_to_use}' unknown - maybe you meant 'documentation' or 'progress'?.")

        formatters << formatter_class.new(output ? File.new(output, 'w') : self.output)
      end

      alias_method :formatter=, :add_formatter

      def formatters
        @formatters ||= []
      end

      def reporter
        @reporter ||= begin
                        add_formatter('progress') if formatters.empty?
                        Reporter.new(*formatters)
                      end
      end

      def files_or_directories_to_run=(*files)
        self.files_to_run = files.flatten.collect do |file|
          if File.directory?(file)
            filename_pattern.split(",").collect do |pattern|
              Dir["#{file}/#{pattern.strip}"]
            end
          else
            if file =~ /(\:(\d+))$/
              self.line_number = $2
              file.sub($1,'')
            else
              file
            end
          end
        end.flatten
      end

      # E.g. alias_example_to :crazy_slow, :speed => 'crazy_slow' defines
      # crazy_slow as an example variant that has the crazy_slow speed option
      def alias_example_to(new_name, extra_options={})
        RSpec::Core::ExampleGroup.alias_example_to(new_name, extra_options)
      end

      # Define an alias for it_should_behave_like that allows different
      # language (like "it_has_behavior" or "it_behaves_like") to be
      # employed when including shared examples.
      #
      # Example:
      #
      #     alias_it_should_behave_like_to(:it_has_behavior, 'has behavior:')
      #
      # allows the user to include a shared example group like:
      #
      #     describe Entity do
      #       it_has_behavior 'sortability' do
      #         let(:sortable) { Entity.new }
      #       end
      #     end
      #
      # which is reported in the output as:
      #
      #     Entity
      #       has behavior: sortability
      #         # sortability examples here
      def alias_it_should_behave_like_to(new_name, report_label = '')
        RSpec::Core::ExampleGroup.alias_it_should_behave_like_to(new_name, report_label)
      end

      def filter_run_including(options={}, force_overwrite = false)
        if filter and filter[:line_number] || filter[:full_description]
          warn "Filtering by #{options.inspect} is not possible since " \
               "you are already filtering by #{filter.inspect}"
        else
          if force_overwrite
            self.filter = options
          else
            self.filter = (filter || {}).merge(options)
          end
        end
      end

      alias_method :filter_run, :filter_run_including

      def filter_run_excluding(options={})
        self.exclusion_filter = (exclusion_filter || {}).merge(options)
      end

      def include(mod, filters={})
        include_or_extend_modules << [:include, mod, filters]
      end

      def extend(mod, filters={})
        include_or_extend_modules << [:extend, mod, filters]
      end

      def configure_group(group)
        modules = {
          :include => group.included_modules.dup,
          :extend  => group.ancestors.dup
        }

        include_or_extend_modules.each do |include_or_extend, mod, filters|
          next unless filters.empty? || group.apply?(:any?, filters)
          group.send(include_or_extend, mod)
        end
      end

      def configure_mock_framework
        RSpec::Core::ExampleGroup.send(:include, mock_framework)
      end

      def configure_expectation_framework
        expectation_frameworks.each do |framework|
          RSpec::Core::ExampleGroup.send(:include, framework)
        end
      end

      def load_spec_files
        files_to_run.map {|f| load File.expand_path(f) }
        raise_if_rspec_1_is_loaded
      end

    private

      def raise_if_rspec_1_is_loaded
        if defined?(Spec) && defined?(Spec::VERSION::MAJOR) && Spec::VERSION::MAJOR == 1
          raise <<-MESSAGE

#{'*'*80}
  You are running rspec-2, but it seems as though rspec-1 has been loaded as
  well.  This is likely due to a statement like this somewhere in the specs:

      require 'spec'

  Please locate that statement, remove it, and try again.
#{'*'*80}
MESSAGE
        end
      end

      def output_to_tty?
        begin
          output_stream.tty? || tty?
        rescue NoMethodError
          false
        end
      end

      def built_in_formatter(key)
        case key.to_s
        when 'd', 'doc', 'documentation', 's', 'n', 'spec', 'nested'
          require 'rspec/core/formatters/documentation_formatter'
          RSpec::Core::Formatters::DocumentationFormatter
        when 'h', 'html'
          require 'rspec/core/formatters/html_formatter'
          RSpec::Core::Formatters::HtmlFormatter
        when 't', 'textmate'
          require 'rspec/core/formatters/text_mate_formatter'
          RSpec::Core::Formatters::TextMateFormatter
        when 'p', 'progress'
          require 'rspec/core/formatters/progress_formatter'
          RSpec::Core::Formatters::ProgressFormatter
        end
      end

      def custom_formatter(formatter_ref)
        if Class === formatter_ref
          formatter_ref
        elsif string_const?(formatter_ref)
          begin
            eval(formatter_ref)
          rescue NameError
            require path_for(formatter_ref)
            eval(formatter_ref)
          end
        end
      end

      def string_const?(str)
        str.is_a?(String) && /\A[A-Z][a-zA-Z0-9_:]*\z/ =~ str
      end

      def path_for(const_ref)
        underscore_with_fix_for_non_standard_rspec_naming(const_ref)
      end

      def underscore_with_fix_for_non_standard_rspec_naming(string)
        underscore(string).sub(%r{(^|/)r_spec($|/)}, '\\1rspec\\2')
      end

      # activesupport/lib/active_support/inflector/methods.rb, line 48
      def underscore(camel_cased_word)
        word = camel_cased_word.to_s.dup
        word.gsub!(/::/, '/')
        word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
        word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
        word.tr!("-", "_")
        word.downcase!
        word
      end
    end
  end
end
