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

require 'test/unit/attribute'
require 'test/unit/fixture'
require 'test/unit/exception-handler'
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/data'
require 'test/unit/testsuite'
require 'test/unit/test-suite-creator'
require 'test/unit/assertion-failed-error'
require 'test/unit/auto-runner-loader'
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 cleanup
    #       ...
    #     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
    # * cleanup
    # * teardown
    # * setup
    # * test_my_method2
    # * cleanup
    # * teardown
    # * shutdown
    class TestCase
      include Attribute
      include Fixture
      include ExceptionHandler
      include ErrorHandler
      include FailureHandler
      include TestCasePendingSupport
      include TestCaseOmissionSupport
      include TestCaseNotificationSupport
      include Priority
      include Data
      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
          super
        end

        @@added_method_names = {}
        def method_added(name) # :nodoc:
          super
          added_method_names = (@@added_method_names[self] ||= {})
          stringified_name = name.to_s
          if added_method_names.key?(stringified_name)
            attribute(:redefined, {:backtrace => caller}, {}, stringified_name)
          end
          source_location = find_attribute(stringified_name, :source_location)
          if source_location
            path, line = source_location
          elsif respond_to?(:caller_locations, true)
            location = caller_locations(1, 1)[0]
            path = location.absolute_path || location.path
            line = location.lineno
          else
            # TODO: Remove me when Ruby 1.9 support is dropped
            path, line, = caller[0].split(/:(\d+)/, 2)
            line = line.to_i if line
          end
          method_locations << {
            :method_name => stringified_name,
            :path => File.expand_path(path),
            :line => line,
          }
          added_method_names[stringified_name] = true
          AutoRunnerLoader.check(self, stringified_name)
        end

        def added_method_names # :nodoc:
          (@@added_method_names[self] ||= {}).keys
        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_creator = TestSuiteCreator.new(self)
          suite_creator.create
        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: #{test_description}"
            description(test_description, method_name)
            attribute(:test, true, {}, method_name)
            if block.respond_to?(:source_location)
              attribute(:source_location, block.source_location, {}, method_name)
            end
            define_method(method_name, &block)
          else
            targets = test_description_or_targets
            attribute(:test, true, {}, *targets)
            targets.each do |target|
              AutoRunnerLoader.check(self, target)
            end
          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)
          targets = [target].compact
          attribute(:description, value, {}, *targets)
        end

        # Defines a sub test case.
        #
        # This is a syntax sugar. The both of the following codes are
        # the same in meaning:
        #
        # Standard:
        #   class TestParent < Test::UnitTestCase
        #     class TestChild < self
        #       def test_in_child
        #       end
        #     end
        #   end
        #
        # Syntax sugar:
        #   class TestParent < Test::UnitTestCase
        #     sub_test_case("TestChild") do
        #       def test_in_child
        #       end
        #     end
        #   end
        #
        # The difference of them are the following:
        #
        # * Test case created by {sub_test_case} is an anonymous class.
        #   So you can't refer the test case by name.
        # * The class name of class style must follow
        #   constant naming rule in Ruby. But the name of test case
        #   created by {sub_test_case} doesn't need to follow the rule.
        #   For example, you can use a space in name such as "child test".
        #
        # @param name [String] The name of newly created sub test case.
        # @yield
        #   The block is evaluated under the newly created sub test
        #   case class context.
        # @return [Test::Unit::TestCase] Created sub test case class.
        def sub_test_case(name, &block)
          parent_test_case = self
          sub_test_case = Class.new(self) do
            singleton_class = class << self; self; end
            singleton_class.__send__(:define_method, :name) do
              [parent_test_case.name, name].compact.join("::")
            end
          end
          sub_test_case.class_eval(&block)
          sub_test_case
        end

        # Checks whether a test that is matched the query is
        # defined.
        #
        # @option query [String] :path (nil)
        #   the path where a test is defined in.
        # @option query [Numeric] :line (nil)
        #   the line number where a test is defined at.
        # @option query [String] :method_name (nil)
        #   the method name for a test.
        def test_defined?(query)
          locations = find_locations(query)
          not locations.empty?
        end

        def find_locations(query)
          query_path = query[:path]
          query_line = query[:line]
          query_method_name = query[:method_name]

          available_locations = target_method_locations(query_path)
          if query_line
            available_locations = available_locations.sort_by do |location|
              -location[:line]
            end
            available_location = available_locations.find do |location|
              query_line >= location[:line]
            end
            return [] if available_location.nil?
            return [] if available_location[:test_case] != self
            available_locations = [available_location]
          end
          if query_method_name
            available_location = available_locations.find do |location|
              query_method_name == location[:method_name]
            end
            return [] if available_location.nil?
            available_locations = [available_location]
          end

          available_locations
        end

        private
        # @private
        @@method_locations = {}
        # @private
        def method_locations
          @@method_locations[self] ||= []
        end

        # @private
        def target_method_locations(path)
          if path.nil?
            self_location = method_locations.first
            path = self_location[:path] if self_location
          end
          return [] if path.nil?

          target_locations = []
          @@method_locations.each do |test_case, locations|
            locations.each do |location|
              absolete_path = File.expand_path(path)
              location_path = location[:path]
              location_basename = File.basename(location_path)
              if location_path == absolete_path or location_basename == path
                target_locations << location.merge(:test_case => test_case)
              end
            end
          end
          target_locations
        end
      end

      attr_reader :method_name

      # Creates a new instance of the fixture for running the
      # test represented by test_method_name.
      def initialize(test_method_name)
        @method_name = test_method_name
        @internal_data = InternalData.new
      end

      # Assigns test data to the test. It is used in internal.
      def assign_test_data(label, data) # :nodoc:
        @internal_data.assign_test_data(label, data)
      end

      # Returns the test is valid test. It is used in internal.
      def valid? # :nodoc:
        return false unless respond_to?(@method_name)
        test_method = method(@method_name)
        unless @internal_data.have_test_data?
          return false unless test_method.arity <= 0
        end
        owner = Util::MethodOwnerFinder.find(self, @method_name)
        if owner.class != Module and self.class != owner
          return false
        end
        true
      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
          @internal_data.test_started
          yield(STARTED, name)
          yield(STARTED_OBJECT, self)
          processed_exception_in_setup = false
          begin
            catch do |tag|
              run_setup do
                begin
                  run_test
                  run_cleanup
                  add_pass
                rescue Exception
                  @internal_data.interrupted
                  unless handle_exception($!)
                    processed_exception_in_setup = true
                    raise
                  end
                  throw(tag)
                end
              end
            end
          rescue Exception
            if processed_exception_in_setup
              raise
            else
              @internal_data.interrupted
              raise unless handle_exception($!)
            end
          ensure
            begin
              run_teardown
            rescue Exception
              raise unless handle_exception($!)
            end
          end
          @internal_data.test_finished
          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 do
      #       ... # setup callback1
      #     end
      #
      #     setup
      #     def my_setup2
      #       ...
      #     end
      #
      #     setup do
      #       ... # setup callback2
      #     end
      #
      #     def test_my_class
      #       ...
      #     end
      #   end
      #
      # Here is a call order:
      # * setup
      # * my_setup1
      # * setup callback1
      # * my_setup2
      # * setup callback2
      # * test_my_class
      def setup
      end

      # Called after every test method runs but the test
      # method isn't marked as 'passed'. Can be used to
      # clean up and/or verify tested condition.
      # e.g. Can be used to verify mock.
      #
      # You can add additional cleanup tasks by the following
      # code:
      #   class TestMyClass < Test::Unit::TestCase
      #     def cleanup
      #       ...
      #     end
      #
      #     cleanup
      #     def my_cleanup1
      #       ...
      #     end
      #
      #     cleanup do
      #       ... # cleanup callback1
      #     end
      #
      #     cleanup
      #     def my_cleanup2
      #       ...
      #     end
      #
      #     cleanup do
      #       ... # cleanup callback2
      #     end
      #
      #     def test_my_class
      #       ...
      #     end
      #   end
      #
      # Here is a call order:
      # * test_my_class
      # * cleanup callback2
      # * my_cleanup2
      # * cleanup callback1
      # * my_cleanup1
      # * cleanup
      def cleanup
      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 do
      #       ... # teardown callback1
      #     end
      #
      #     teardown
      #     def my_teardown2
      #       ...
      #     end
      #
      #     teardown do
      #       ... # teardown callback2
      #     end
      #
      #     def test_my_class
      #       ...
      #     end
      #   end
      #
      # Here is a call order:
      # * test_my_class
      # * teardown callback2
      # * my_teardown2
      # * teardown callback1
      # * my_teardown1
      # * teardown
      def teardown
      end

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

      def size
        1
      end

      # Returns a label of test data for the test. If the
      # test isn't associated with any test data, it returns
      # +nil+.
      def data_label
        @internal_data.test_data_label
      end

      # Returns a human-readable name for the specific test that
      # this instance of TestCase represents.
      def name
        if @internal_data.have_test_data?
          "#{@method_name}[#{data_label}](#{self.class.name})"
        else
          "#{@method_name}(#{self.class.name})"
        end
      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
        return false unless data_label == other.data_label
        self.class == other.class
      end

      # Returns a Time at the test was started.
      def start_time
        @internal_data.start_time
      end

      # Returns elapsed time for the test was ran.
      def elapsed_time
        @internal_data.elapsed_time
      end

      # Returns whether the test is interrupted.
      def interrupted?
        @internal_data.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?
        @internal_data.passed?
      end

      # Notify that a problem is occurred in the test. It means that
      # the test is a failed test. If any failed tests exist in test
      # suites, the test process exits with failure exit status.
      #
      # This is a public API for developers who extend test-unit.
      #
      # @return [void]
      def problem_occurred
        @internal_data.problem_occurred
      end

      # Notify that the test is passed. Normally, it is not needed
      # because #run calls it automatically. If you want to override
      # #run, it is not a good idea. Please contact test-unit
      # developers. We will help you without your custom #run. For
      # example, we may add a new hook in #run.
      #
      # This is a public API for developers who extend test-unit.
      #
      # @return [void]
      def add_pass
        current_result.add_pass
      end

      private
      def current_result
        @_result
      end

      def run_test
        signature = "#{self.class}\##{@method_name}"
        redefined_info = self[:redefined]
        if redefined_info
          notify("#{signature} was redefined",
                 :backtrace => redefined_info[:backtrace])
        end
        if @internal_data.have_test_data?
          test_method = method(@method_name)
          if test_method.arity == 1 or test_method.arity < 0
            __send__(@method_name, @internal_data.test_data)
          else
            locations = self.class.find_locations(:method_name => @method_name)
            backtrace = locations.collect do |location|
              "#{location[:path]}:#{location[:line]}"
            end
            notify("#{signature} misses a parameter to take test data",
                   :backtrace => backtrace)
          end
        else
          __send__(@method_name)
        end
      end

      def handle_exception(exception)
        self.class.exception_handlers.each do |handler|
          if handler.respond_to?(:call)
            handled = handler.call(self, exception)
          else
            handled = __send__(handler, exception)
          end
          return true if handled
        end
        false
      end

      def add_assertion
        current_result.add_assertion
      end

      class InternalData
        attr_reader :start_time, :elapsed_time
        attr_reader :test_data_label, :test_data
        def initialize
          @start_time = nil
          @elapsed_time = nil
          @passed = true
          @interrupted = false
          @test_data_label = nil
          @test_data = nil
        end

        def passed?
          @passed
        end

        def interrupted?
          @interrupted
        end

        def assign_test_data(label, data)
          @test_data_label = label
          @test_data = data
        end

        def have_test_data?
          not @test_data_label.nil?
        end

        def test_started
          @start_time = Time.now
        end

        def test_finished
          @elapsed_time = Time.now - @start_time
        end

        def problem_occurred
          @passed = false
        end

        def interrupted
          @interrupted = true
        end
      end
    end
  end
end
