#--
#
# Author:: Nathaniel Talbott.
# Copyright::
#   * Copyright (c) 2000-2003 Nathaniel Talbott. All rights reserved.
#   * Copyright (c) 2008-2009 Kouhei Sutou <tt><kou@clear-code.com></tt>
# License:: Ruby license.

require 'test/unit/attribute'
require 'test/unit/fixture'
require 'test/unit/exceptionhandler'
require 'test/unit/assertions'
require 'test/unit/failure'
require 'test/unit/error'
require 'test/unit/pending'
require 'test/unit/omission'
require 'test/unit/notification'
require 'test/unit/priority'
require 'test/unit/testsuite'
require 'test/unit/assertionfailederror'
require 'test/unit/util/backtracefilter'
require 'test/unit/util/output'
require 'test/unit/util/method-owner-finder'

module Test
  module Unit

    # Ties everything together. If you subclass and add your own
    # test methods, it takes care of making them into tests and
    # wrapping those tests into a suite. It also does the
    # nitty-gritty of actually running an individual test and
    # collecting its results into a Test::Unit::TestResult object.
    #
    # You can run two hooks before/after a TestCase run.
    #
    # Example:
    #   class TestMyClass < Test::Unit::TestCase
    #     class << self
    #       def startup
    #         ...
    #       end
    #
    #       def shutdown
    #         ...
    #       end
    #     end
    #
    #     def setup
    #       ...
    #     end
    #
    #     def teardown
    #       ...
    #     end
    #
    #     def test_my_method1
    #       ...
    #     end
    #
    #     def test_my_method2
    #       ...
    #     end
    #   end
    #
    # Here is a call order:
    # * startup
    # * setup
    # * test_my_method1
    # * teardown
    # * setup
    # * test_my_method2
    # * teardown
    # * shutdown
    class TestCase
      include Attribute
      include Fixture
      include ExceptionHandler
      include ErrorHandler
      include FailureHandler
      include TestCasePendingSupport
      include TestCaseOmissionSupport
      include TestCaseNotificationSupport
      include Priority
      include Assertions
      include Util::BacktraceFilter
      include Util::Output

      STARTED = name + "::STARTED" # :nodoc:
      FINISHED = name + "::FINISHED" # :nodoc:
      STARTED_OBJECT = name + "::STARTED::OBJECT" # :nodoc:
      FINISHED_OBJECT = name + "::FINISHED::OBJECT" # :nodoc:

      DESCENDANTS = [] # :nodoc:
      AVAILABLE_ORDERS = [:alphabetic, :random, :defined] # :nodoc:

      class << self
        def inherited(sub_class) # :nodoc:
          DESCENDANTS << sub_class
        end

        @@added_methods = {}
        def method_added(name) # :nodoc:
          super
          added_methods = (@@added_methods[self] ||= [])
          stringified_name = name.to_s
          if added_methods.include?(stringified_name)
            attribute(:redefined, true, {}, stringified_name)
          end
          added_methods << stringified_name
        end

        # Rolls up all of the test* methods in the fixture into
        # one suite, creating a new instance of the fixture for
        # each method.
        def suite
          suite = TestSuite.new(name, self)
          collect_test_names.each do |test|
            catch(:invalid_test) do
              suite << new(test)
            end
          end
          if suite.empty?
            catch(:invalid_test) do
              suite << new("default_test")
            end
          end
          suite
        end

        # Called before every test case runs. Can be used
        # to set up fixture information used in test case
        # scope.
        #
        # Here is an example test case:
        #   class TestMyClass < Test::Unit::TestCase
        #     class << self
        #       def startup
        #         ...
        #       end
        #     end
        #
        #     def setup
        #       ...
        #     end
        #
        #     def test_my_class1
        #       ...
        #     end
        #
        #     def test_my_class2
        #       ...
        #     end
        #   end
        #
        # Here is a call order:
        # * startup
        # * setup
        # * test_my_class1 (or test_my_class2)
        # * setup
        # * test_my_class2 (or test_my_class1)
        #
        # Note that you should not assume test order. Tests
        # should be worked in any order.
        def startup
        end

        # Called after every test case runs. Can be used to tear
        # down fixture information used in test case scope.
        #
        # Here is an example test case:
        #   class TestMyClass < Test::Unit::TestCase
        #     class << self
        #       def shutdown
        #         ...
        #       end
        #     end
        #
        #     def teardown
        #       ...
        #     end
        #
        #     def test_my_class1
        #       ...
        #     end
        #
        #     def test_my_class2
        #       ...
        #     end
        #   end
        #
        # Here is a call order:
        # * test_my_class1 (or test_my_class2)
        # * teardown
        # * test_my_class2 (or test_my_class1)
        # * teardown
        # * shutdown
        #
        # Note that you should not assume test order. Tests
        # should be worked in any order.
        def shutdown
        end

        @@test_orders = {}

        # Returns the current test order. This returns
        # +:alphabetic+ by default.
        def test_order
          @@test_orders[self] || AVAILABLE_ORDERS.first
        end

        # Sets the current test order.
        #
        # Here are the available _order_:
        # [:alphabetic]
        #   Default. Tests are sorted in alphabetic order.
        # [:random]
        #   Tests are sorted in random order.
        # [:defined]
        #   Tests are sorted in defined order.
        def test_order=(order)
          @@test_orders[self] = order
        end

        # Defines a test in declarative syntax or marks
        # following method as a test method.
        #
        # In declarative syntax usage, the following two
        # test definitions are the almost same:
        #
        #   description "register user"
        #   def test_register_user
        #     ...
        #   end
        #
        #   test "register user" do
        #     ...
        #   end
        #
        # In test method mark usage, the "my_test_method" is
        # treated as a test method:
        #
        #   test
        #   def my_test_method
        #     assert_equal("call me", ...)
        #   end
        def test(*test_description_or_targets, &block)
          if block_given?
            test_description = test_description_or_targets.first
            if test_description.nil?
              raise ArgumentError, "test description is missing"
            end
            n_arguments = test_description_or_targets.size
            if n_arguments > 1
              message = "wrong number of arguments (#{n_arguments} for 1)"
              raise ArgumentError, message
            end
            method_name = test_description
            define_method(method_name, &block)
            description(test_description, method_name)
            attribute(:test, true, {}, method_name)
          else
            targets = test_description_or_targets
            attribute(:test, true, {}, *targets)
          end
        end

        # Describes a test.
        #
        # The following example associates "register a
        # normal user" description with "test_register"
        # test.
        #
        #   description "register a normal user"
        #   def test_register
        #     ...
        #   end
        def description(value, target=nil)
          attribute(:description, value, {}, target || [])
        end

        # :stopdoc:
        private
        def collect_test_names
          method_names = public_instance_methods(true).collect do |name|
            name.to_s
          end
          test_names = method_names.find_all do |method_name|
            method_name =~ /^test./ or get_attribute(method_name, :test)
          end
          send("sort_test_names_in_#{test_order}_order", test_names)
        end

        def sort_test_names_in_alphabetic_order(test_names)
          test_names.sort
        end

        def sort_test_names_in_random_order(test_names)
          test_names.sort_by {rand(test_names.size)}
        end

        def sort_test_names_in_defined_order(test_names)
          added_methods = @@added_methods[self]
          test_names.sort do |test1, test2|
            test1_defined_order = added_methods.index(test1)
            test2_defined_order = added_methods.index(test2)
            if test1_defined_order and test2_defined_order
              test1_defined_order <=> test2_defined_order
            elsif test1_defined_order
              1
            elsif test2_defined_order
              -1
            else
              test1 <=> test2
            end
          end
        end
        # :startdoc:
      end

      attr_reader :method_name, :start_time, :elapsed_time

      # Creates a new instance of the fixture for running the
      # test represented by test_method_name.
      def initialize(test_method_name)
        throw :invalid_test unless respond_to?(test_method_name)
        test_method = method(test_method_name)
        throw :invalid_test if test_method.arity > 0
        owner = Util::MethodOwnerFinder.find(self, test_method_name)
        if owner.class != Module and self.class != owner
          throw :invalid_test
        end
        @method_name = test_method_name
        @test_passed = true
        @interrupted = false
        @start_time = nil
        @elapsed_time = nil
      end

      # Runs the individual test method represented by this
      # instance of the fixture, collecting statistics, failures
      # and errors in result.
      def run(result)
        begin
          @_result = result
          @start_time = Time.now
          yield(STARTED, name)
          yield(STARTED_OBJECT, self)
          begin
            run_setup
            run_test
          rescue Exception
            @interrupted = true
            raise unless handle_exception($!)
          ensure
            begin
              run_teardown
            rescue Exception
              raise unless handle_exception($!)
            end
          end
          @elapsed_time = Time.now - @start_time
          result.add_run
          yield(FINISHED, name)
          yield(FINISHED_OBJECT, self)
        ensure
          # @_result = nil # For test-spec's after_all :<
        end
      end

      # Called before every test method runs. Can be used
      # to set up fixture information.
      #
      # You can add additional setup tasks by the following
      # code:
      #   class TestMyClass < Test::Unit::TestCase
      #     def setup
      #       ...
      #     end
      #
      #     setup
      #     def my_setup1
      #       ...
      #     end
      #
      #     setup
      #     def my_setup2
      #       ...
      #     end
      #
      #     def test_my_class
      #       ...
      #     end
      #   end
      #
      # Here is a call order:
      # * setup
      # * my_setup1
      # * my_setup2
      # * test_my_class
      def setup
      end

      # Called after every test method runs. Can be used to tear
      # down fixture information.
      #
      # You can add additional teardown tasks by the following
      # code:
      #   class TestMyClass < Test::Unit::TestCase
      #     def teardown
      #       ...
      #     end
      #
      #     teardown
      #     def my_teardown1
      #       ...
      #     end
      #
      #     teardown
      #     def my_teardown2
      #       ...
      #     end
      #
      #     def test_my_class
      #       ...
      #     end
      #   end
      #
      # Here is a call order:
      # * test_my_class
      # * my_teardown2
      # * my_teardown1
      # * teardown
      def teardown
      end

      def default_test
        flunk("No tests were specified")
      end

      def size
        1
      end

      # Returns a human-readable name for the specific test that
      # this instance of TestCase represents.
      def name
        "#{@method_name}(#{self.class.name})"
      end

      # Returns a description for the test. A description
      # will be associated by Test::Unit::TestCase.test or
      # Test::Unit::TestCase.description.
      #
      # Returns a name for the test for no description test.
      def description
        self[:description] || name
      end

      # Overridden to return #name.
      def to_s
        name
      end

      # It's handy to be able to compare TestCase instances.
      def ==(other)
        return false unless(other.kind_of?(self.class))
        return false unless(@method_name == other.method_name)
        self.class == other.class
      end

      def interrupted?
        @interrupted
      end

      # Returns whether this individual test passed or
      # not. Primarily for use in teardown so that artifacts
      # can be left behind if the test fails.
      def passed?
        @test_passed
      end

      private
      def current_result
        @_result
      end

      def run_test
        if self.class.get_attribute(@method_name, :redefined)
          notify("#{self.class}\##{@method_name} was redefined")
        end
        __send__(@method_name)
        add_pass
      end

      def handle_exception(exception)
        self.class.exception_handlers.each do |handler|
          return true if send(handler, exception)
        end
        false
      end

      def problem_occurred
        @test_passed = false
      end

      def add_assertion
        current_result.add_assertion
      end

      def add_pass
        current_result.add_pass
      end
    end
  end
end
