module Spec
  module Example

    module ExampleGroupMethods
      class << self
        attr_accessor :matcher_class

        def build_description_from(*args)
          text = args.inject("") do |description, arg|
            description << " " unless (description == "" || arg.to_s =~ /^(\s|\.|#)/)
            description << arg.to_s
          end
          text == "" ? nil : text
        end
      end

      include Spec::Example::BeforeAndAfterHooks
      include Spec::Example::Subject::ExampleGroupMethods
      include Spec::Example::PredicateMatchers
      include Spec::Example::ArgsAndOptions

      attr_reader :location

      def options # :nodoc:
        @options ||= {}
      end

      def inherited(klass) # :nodoc:
        super
        ExampleGroupFactory.register_example_group(klass)
      end

      # Makes the describe/it syntax available from a class. For example:
      #
      #   class StackSpec < Spec::ExampleGroup
      #     describe Stack, "with no elements"
      #
      #     before
      #       @stack = Stack.new
      #     end
      #
      #     it "should raise on pop" do
      #       lambda{ @stack.pop }.should raise_error
      #     end
      #   end
      #
      def describe(*args, &example_group_block)
        raise Spec::Example::NoDescriptionError.new("example group", caller(0)[1]) if args.empty?
        if example_group_block
          options = add_options(args)
          set_location(options, caller(0)[1])
          if options[:shared]
            ExampleGroupFactory.create_shared_example_group(*args, &example_group_block)
          else
            subclass(*args, &example_group_block)
          end
        else
          set_description(*args)
        end
      end
      alias :context :describe

      # Use this to pull in examples from shared example groups.
      def it_should_behave_like(*shared_example_groups)
        shared_example_groups.each do |group|
          include_shared_example_group(group)
        end
      end

      # Creates an instance of the current example group class and adds it to
      # a collection of examples of the current example group.
      def example(description=nil, options={}, backtrace=nil, &implementation)
        example_proxy = ExampleProxy.new(description, options, backtrace || caller(0)[1])
        example_proxies << example_proxy
        example_implementations[example_proxy] = implementation || pending_implementation
        example_proxy
      end

      def pending_implementation
        lambda {|*args| raise(Spec::Example::NotYetImplementedError) }
      end

      alias_method :it, :example
      alias_method :specify, :example

      # Use this to temporarily disable an example.
      def xexample(description=nil, opts={}, &block)
        Kernel.warn("Example disabled: #{description}")
      end

      alias_method :xit, :xexample
      alias_method :xspecify, :xexample

      def run(run_options)
        examples = examples_to_run(run_options)
        notify(run_options.reporter) unless examples.empty?
        return true if examples.empty?
        return dry_run(examples, run_options) if run_options.dry_run?

        define_methods_from_predicate_matchers

        success, before_all_instance_variables = run_before_all(run_options)
        success, after_all_instance_variables  = run_examples(success, before_all_instance_variables, examples, run_options)
        success                                = run_after_all(success, after_all_instance_variables, run_options)
      end

      def set_description(*args)
        @description_args, @options = args_and_options(*args)
        @backtrace = caller(1)
        @location = File.expand_path(options[:location]) if options[:location]
        self
      end

      def notify(reporter) # :nodoc:
        reporter.example_group_started(ExampleGroupProxy.new(self))
      end

      def description
        @description ||= ExampleGroupMethods.build_description_from(*description_parts) || to_s
      end

      def described_type
        @described_type ||= description_parts.reverse.find {|part| part.is_a?(Module)}
      end

      def described_class
        @described_class ||= Class === described_type ? described_type : nil
      end

      def description_args
        @description_args ||= []
      end

      def description_parts #:nodoc:
        @description_parts ||= example_group_hierarchy.inject([]) do |parts, example_group_class|
          [parts << example_group_class.description_args].flatten
        end
      end

      def example_proxies # :nodoc:
        @example_proxies ||= []
      end

      def example_implementations # :nodoc:
        @example_implementations ||= {}
      end

      def examples(run_options=nil) #:nodoc:
        (run_options && run_options.reverse) ? example_proxies.reverse : example_proxies
      end

      def number_of_examples #:nodoc:
        example_proxies.length
      end

      def example_group_hierarchy
        @example_group_hierarchy ||= ExampleGroupHierarchy.new(self)
      end

      def nested_descriptions
        example_group_hierarchy.nested_descriptions
      end

      def include_constants_in(mod)
        include mod if (Spec::Ruby.version.to_f >= 1.9) & (Module === mod) & !(Class === mod)
      end

      def let(name, &block)
        define_method name do
          @assignments ||= {}
          @assignments[name] ||= instance_eval(&block)
        end
      end

      def let!(name, &block)
        let(name, &block)
        before { __send__(name) }
      end

    private

      def subclass(*args, &example_group_block)
        @class_count ||= 0
        @class_count += 1
        klass = const_set("Subclass_#{@class_count}", Class.new(self))
        klass.set_description(*args)
        klass.include_constants_in(args.last[:scope])
        klass.module_eval(&example_group_block)
        klass
      end

      def dry_run(examples, run_options)
        examples.each do |example|
          run_options.reporter.example_started(example)
          run_options.reporter.example_finished(example)
        end
      end

      def run_before_all(run_options)
        return [true,{}] if example_group_hierarchy.before_all_parts.empty?
        example_proxy = ExampleProxy.new("before(:all)")
        before_all = new(example_proxy)
        begin
          example_group_hierarchy.run_before_all(before_all)
          return [true, before_all.instance_variable_hash]
        rescue Exception => e
          run_options.reporter.example_failed(example_proxy, e)
          return [false, before_all.instance_variable_hash]
        end
      end

      def run_examples(success, instance_variables, examples, run_options)
        return [success, instance_variables] unless success

        after_all_instance_variables = instance_variables

        examples.each do |example|
          example_group_instance = new(example, &example_implementations[example])
          success &= example_group_instance.execute(run_options, instance_variables)
          after_all_instance_variables = example_group_instance.instance_variable_hash
        end

        return [success, after_all_instance_variables]
      end

      def run_after_all(success, instance_variables, run_options)
        return success if example_group_hierarchy.after_all_parts.empty?
        example_proxy = ExampleProxy.new("after(:all)")
        after_all = new(example_proxy)
        after_all.set_instance_variables_from_hash(instance_variables)
        example_group_hierarchy.run_after_all(after_all)
        success
      rescue Exception => e
        run_options.reporter.example_failed(example_proxy, e)
        false
      end

      def examples_to_run(run_options)
        return example_proxies unless examples_were_specified?(run_options)
        if run_options.line_number_requested?
          if location =~ /:#{run_options.example_line}:?/
            example_proxies
          else
            example_proxies.select {|proxy| proxy.location =~ /:#{run_options.example_line}:?/}
          end
        else
          example_proxies.reject do |proxy|
            matcher = ExampleGroupMethods.matcher_class.
              new(description.to_s, proxy.description)
            !matcher.matches?(run_options.examples)
          end
        end
      end

      def examples_were_specified?(run_options)
        !run_options.examples.empty?
      end

      def method_added(name) # :nodoc:
        example(name.to_s, {}, caller(0)[1]) {__send__ name.to_s} if example_method?(name.to_s)
      end

      def example_method?(method_name)
        should_method?(method_name)
      end

      def should_method?(method_name)
        !(method_name =~ /^should(_not)?$/) &&
        method_name =~ /^should/ &&
        instance_method(method_name).arity < 1
      end

      def include_shared_example_group(shared_example_group)
        case shared_example_group
        when SharedExampleGroup
          include shared_example_group
        else
          unless example_group = SharedExampleGroup.find(shared_example_group)
            raise RuntimeError.new("Shared Example Group '#{shared_example_group}' can not be found")
          end
          include(example_group)
        end
      end
    end

  end
end
