require "optparse"
require "rake/file_list"
require "method_source"

module Rails
  class TestRunner
    class Options
      def self.parse(args)
        options = { backtrace: !ENV["BACKTRACE"].nil?, name: nil, environment: "test" }

        opt_parser = ::OptionParser.new do |opts|
          opts.banner = "Usage: bin/rails test [options] [file or directory]"

          opts.separator ""
          opts.on("-e", "--environment [ENV]",
                  "Run tests in the ENV environment") do |env|
            options[:environment] = env.strip
          end
          opts.separator ""
          opts.separator "Filter options:"
          opts.separator ""
          opts.separator <<-DESC
  You can run a single test by appending the line number to filename:

    bin/rails test test/models/user_test.rb:27

          DESC

          opts.on("-n", "--name [NAME]",
                  "Only run tests matching NAME") do |name|
            options[:name] = name
          end
          opts.on("-p", "--pattern [PATTERN]",
                  "Only run tests matching PATTERN") do |pattern|
            options[:name] = "/#{pattern}/"
          end

          opts.separator ""
          opts.separator "Output options:"

          opts.on("-b", "--backtrace",
                  "Show the complete backtrace") do
            options[:backtrace] = true
          end

          opts.separator ""
          opts.separator "Common options:"

          opts.on_tail("-h", "--help", "Show this message") do
            puts opts
            exit
          end
        end

        opt_parser.order!(args)

        options[:patterns] = []
        while arg = args.shift
          if (file_and_line = arg.split(':')).size > 1
            options[:filename], options[:line] = file_and_line
            options[:filename] = File.expand_path options[:filename]
            options[:line] &&= options[:line].to_i
          else
            arg = arg.gsub(':', '')
            if Dir.exist?("#{arg}")
              options[:patterns] << File.expand_path("#{arg}/**/*_test.rb")
            elsif File.file?(arg)
              options[:patterns] << File.expand_path(arg)
            end
          end
        end
        options
      end
    end

    def initialize(options = {})
      @options = options
    end

    def self.run(arguments)
      options = Rails::TestRunner::Options.parse(arguments)
      Rails::TestRunner.new(options).run
    end

    def run
      $rails_test_runner = self
      ENV["RAILS_ENV"] = @options[:environment]
      run_tests
    end

    def find_method
      return @options[:name] if @options[:name]
      return unless @options[:line]
      method = test_methods.find do |location, test_method, start_line, end_line|
        location == @options[:filename] &&
          (start_line..end_line).include?(@options[:line].to_i)
      end
      method[1] if method
    end

    def show_backtrace?
      @options[:backtrace]
    end

    def test_files
      return [@options[:filename]] if @options[:filename]
      if @options[:patterns] && @options[:patterns].count > 0
        pattern = @options[:patterns]
      else
        pattern = "test/**/*_test.rb"
      end
      Rake::FileList[pattern]
    end

    private
    def run_tests
      test_files.to_a.each do |file|
        require File.expand_path file
      end
    end

    def test_methods
      methods_map = []
      suites = Minitest::Runnable.runnables.shuffle
      suites.each do |suite_class|
        suite_class.runnable_methods.each do |test_method|
          method = suite_class.instance_method(test_method)
          location = method.source_location
          start_line = location.last
          end_line = method.source.split("\n").size + start_line - 1
          methods_map << [File.expand_path(location.first), test_method, start_line, end_line]
        end
      end
      methods_map
    end
  end
end
