# Adapted from Autotest::Rails, RSpec's autotest class, as well as merb-core's.
require 'autotest'

class RspecCommandError < StandardError; end

# This class maps your application's structure so Autotest can understand what 
# specs to run when files change.
#
# Fixtures are _not_ covered by this class. If you change a fixture file, you 
# will have to run your spec suite manually, or, better yet, provide your own 
# Autotest map explaining how your fixtures are set up.
class Autotest::MerbRspec < Autotest
  def initialize
    super

    # Ignore any happenings in these directories
    add_exception %r%^\./(?:doc|log|public|tmp|\.git|\.hg|\.svn|framework|gems|schema|\.DS_Store|autotest|bin|.*\.sqlite3)% 
    # Ignore SCM directories and custom Autotest mappings
    %w[.svn .hg .git .autotest].each { |exception| add_exception(exception) }

    # Ignore any mappings that Autotest may have already set up
    clear_mappings

    # Anything in /lib could have a spec anywhere, if at all. So, look for
    # files with roughly the same name as the file in /lib
    add_mapping %r%^lib\/(.*)\.rb% do |_, m|
      files_matching %r%^spec\/#{m[1]}%
    end

    add_mapping %r%^spec/(spec_helper|shared/.*)\.rb$% do
      all_specs
    end

    # Changing a spec will cause it to run itself
    add_mapping %r%^spec/.*\.rb$% do |filename, _|
      filename
    end

    # Any change to a model will cause it's corresponding test to be run
    add_mapping %r%^app/models/(.*)\.rb$% do |_, m|
      spec_for(m[1], 'model')
    end

    # Any change to global_helpers will result in all view and controller
    # tests being run
    add_mapping %r%^app/helpers/global_helpers\.rb% do
      files_matching %r%^spec/(views|controllers|helpers)/.*_spec\.rb$%
    end

    # Any change to a helper will cause its spec to be run
    add_mapping %r%^app/helpers/((.*)_helper(s)?)\.rb% do |_, m|
      spec_for(m[1], 'helper')
    end

    # Changes to a view cause its spec to be run
    add_mapping %r%^app/views/(.*)/% do |_, m|
      spec_for(m[1], 'view')
    end

    # Changes to a controller result in its corresponding spec being run. If
    # the controller is the exception or application controller, all
    # controller specs are run.
    add_mapping %r%^app/controllers/(.*)\.rb$% do |_, m|
      if ["application", "exception"].include?(m[1])
        files_matching %r%^spec/controllers/.*_spec\.rb$%
      else
        spec_for(m[1], 'controller')
      end
    end

    # If a change is made to the router, run controller, view and helper specs
    add_mapping %r%^config/router.rb$% do
      files_matching %r%^spec/(controllers|views|helpers)/.*_spec\.rb$%
    end

    # If any of the major files governing the environment are altered, run
    # everything
    add_mapping %r%^config/(init|rack|environments/test).*\.rb|database\.yml% do 
      all_specs
    end
  end

  def failed_results(results)
    results.scan(/^\d+\)\n(?:\e\[\d*m)?(?:.*?Error in )?'([^\n]*)'(?: FAILED)?(?:\e\[\d*m)?\n(.*?)\n\n/m)
  end

  def handle_results(results)
    @failures      = failed_results(results)
    @files_to_test = consolidate_failures(@failures)
    @files_to_test.empty? && !$TESTING ? hook(:green) : hook(:red)
    @tainted = !@files_to_test.empty?
  end

  def consolidate_failures(failed)
    filters = Hash.new { |h,k| h[k] = [] }
    failed.each do |spec, failed_trace|
      if f = test_files_for(failed).find { |f| f =~ /spec\// }
        filters[f] << spec
        break
      end
    end
    filters
  end

  def make_test_cmd(specs_to_runs)
    [
      ruby,
      "-S",
      spec_command,
      add_options_if_present,
      files_to_test.keys.flatten.join(' ')
    ].join(' ')
  end

  def add_options_if_present
    File.exist?("spec/spec.opts") ? "-O spec/spec.opts " : ""
  end

  # Finds the proper spec command to use. Precendence is set in the
  # lazily-evaluated method spec_commands.  Alias + Override that in
  # ~/.autotest to provide a different spec command then the default
  # paths provided.
  def spec_command(separator=File::ALT_SEPARATOR)
    unless defined?(@spec_command)
      @spec_command = spec_commands.find { |cmd| File.exists?(cmd) }

      raise RspecCommandError, "No spec command could be found" unless @spec_command

      @spec_command.gsub!(File::SEPARATOR, separator) if separator
    end
    @spec_command
  end

  # Autotest will look for spec commands in the following
  # locations, in this order:
  #
  #   * default spec bin/loader installed in Rubygems
  #   * any spec command found in PATH
  def spec_commands
    [File.join(Config::CONFIG['bindir'], 'spec'), 'spec']
  end

private

  # Runs +files_matching+ for all specs
  def all_specs
    files_matching %r%^spec/.*_spec\.rb$%
  end

  # Generates a path to some spec given its kind and the match from a mapping
  #
  # ==== Arguments
  # match<String>:: the match from a mapping
  # kind<String>:: the kind of spec that the match represents
  #
  # ==== Returns
  # String
  #
  # ==== Example
  #   > spec_for('post', :view')
  #   => "spec/views/post_spec.rb"
  def spec_for(match, kind)
    File.join("spec", kind + 's', "#{match}_spec.rb")
  end
end
