require "test/unit"

require "fileutils"

module Test
  module Unit
    class TestCase
      class << self
        def inherited(sub)
          super
          sub.instance_variable_set("@priority_initialized", true)
          sub.instance_variable_set("@priority_table", {})
          sub.priority :normal
        end

        def include(*args)
          args.reverse_each do |mod|
            super(mod)
            next unless defined?(@priority_initialized)
            mod.instance_methods(false).each do |name|
              set_priority(name)
            end
          end
        end

        def method_added(name)
          set_priority(name) if defined?(@priority_initialized)
        end

        def priority(name, *tests)
          singleton_class = (class << self; self; end)
          priority_check_method = priority_check_method_name(name)
          unless singleton_class.private_method_defined?(priority_check_method)
            raise ArgumentError, "unknown priority: #{name}"
          end
          if tests.empty?
            @current_priority = name
          else
            tests.each do |test|
              set_priority(test, name)
            end
          end
        end

        def need_to_run?(test_name)
          normalized_test_name = normalize_test_name(test_name)
          priority = @priority_table[normalized_test_name]
          return true unless priority
          __send__(priority_check_method_name(priority), test_name)
        end

        private
        def priority_check_method_name(priority_name)
          "run_priority_#{priority_name}?"
        end

        def normalize_test_name(test_name)
          "test_#{test_name.to_s.sub(/^test_/, '')}"
        end

        def set_priority(name, priority=@current_priority)
          @priority_table[normalize_test_name(name)] = priority
        end

        def run_priority_must?(test_name)
          true
        end

        def run_priority_important?(test_name)
          rand > 0.1
        end

        def run_priority_high?(test_name)
          rand > 0.3
        end

        def run_priority_normal?(test_name)
          rand > 0.5
        end

        def run_priority_low?(test_name)
          rand > 0.75
        end

        def run_priority_never?(test_name)
          false
        end
      end

      def need_to_run?
        !previous_test_success? or self.class.need_to_run?(@method_name)
      end

      alias_method :original_run, :run
      def run(result, &block)
        original_run(result, &block)
      ensure
        if passed?
          FileUtils.touch(passed_file)
        else
          FileUtils.rm_f(passed_file)
        end
      end

      private
      def previous_test_success?
        File.exist?(passed_file)
      end

      def result_dir
        dir = File.join(File.dirname($0), ".test-result",
                        self.class.name, escaped_method_name)
        dir = File.expand_path(dir)
        FileUtils.mkdir_p(dir)
        dir
      end

      def passed_file
        File.join(result_dir, "passed")
      end

      def escaped_method_name
        @method_name.to_s.gsub(/[!?]$/) do |matched|
          case matched
          when "!"
            ".destructive"
          when "?"
            ".predicate"
          end
        end
      end
    end

    class TestSuite
      @@priority_mode = false

      class << self
        def priority_mode=(bool)
          @@priority_mode = bool
        end
      end

      alias_method :original_run, :run
      def run(*args, &block)
        priority_mode = @@priority_mode
        if priority_mode
          @original_tests = @tests
          apply_priority
        end
        original_run(*args, &block)
      ensure
        @tests = @original_tests if priority_mode
      end

      def apply_priority
        @tests = @tests.reject {|test| !test.need_to_run?}
      end

      def need_to_run?
        apply_priority
        !@tests.empty?
      end
    end

    class AutoRunner
      alias_method :original_options, :options
      def options
        opts = original_options
        opts.on("--[no-]priority", "use priority mode") do |bool|
          TestSuite.priority_mode = bool
        end
        opts
      end
    end
  end
end
