class RSpecJUnitFormatter < RSpec::Core::Formatters::BaseFormatter
  RSpec::Core::Formatters.register self,
    :start,
    :stop,
    :dump_summary

  def start(notification)
    @start_notification = notification
    @started = Time.now
    super
  end

  def stop(notification)
    @examples_notification = notification
  end

  def dump_summary(notification)
    @summary_notification = notification
    without_color { xml_dump }
  end

private

  attr_reader :started

  def example_count
    @summary_notification.example_count
  end

  def pending_count
    @summary_notification.pending_count
  end

  def failure_count
    @summary_notification.failure_count
  end

  def duration
    @summary_notification.duration
  end

  def examples
    @examples_notification.notifications
  end

  def result_of(notification)
    notification.example.execution_result.status
  end

  def example_group_file_path_for(notification)
    metadata = notification.example.metadata[:example_group]
    while parent_metadata = metadata[:parent_example_group]
      metadata = parent_metadata
    end
    metadata[:file_path]
  end

  def classname_for(notification)
    fp = example_group_file_path_for(notification)
    fp.sub(%r{\.[^/]*\Z}, "").gsub("/", ".").gsub(%r{\A\.+|\.+\Z}, "")
  end

  def duration_for(notification)
    notification.example.execution_result.run_time
  end

  def description_for(notification)
    notification.example.full_description
  end

  def failure_type_for(example)
    exception_for(example).class.name
  end

  def failure_message_for(example)
    strip_diff_colors(exception_for(example).to_s)
  end

  def failure_for(notification)
    strip_diff_colors(notification.message_lines.join("\n")) << "\n" << notification.formatted_backtrace.join("\n")
  end

  def exception_for(notification)
    notification.example.execution_result.exception
  end

  # rspec makes it really difficult to swap in configuration temporarily due to
  # the way it cascades defaults, command line arguments, and user
  # configuration. This method makes sure configuration gets swapped in
  # correctly, but also that the original state is definitely restored.
  def swap_rspec_configuration(key, value)
    unset = Object.new
    force = RSpec.configuration.send(:value_for, key) { unset }
    if unset.equal?(force)
      previous = RSpec.configuration.send(key)
      RSpec.configuration.send(:"#{key}=", value)
    else
      RSpec.configuration.force({key => value})
    end
    yield
  ensure
    if unset.equal?(force)
      RSpec.configuration.send(:"#{key}=", previous)
    else
      RSpec.configuration.force({key => force})
    end
  end

  # Completely gross hack for absolutely forcing off colorising for the
  # duration of a block.
  if RSpec.configuration.respond_to?(:color_mode=)
    def without_color(&block)
      swap_rspec_configuration(:color_mode, :off, &block)
    end
  elsif RSpec.configuration.respond_to?(:color=)
    def without_color(&block)
      swap_rspec_configuration(:color, false, &block)
    end
  else
    warn 'rspec_junit_formatter cannot prevent colorising due to an unexpected RSpec.configuration format'
    def without_color
      yield
    end
  end
end

# rspec-core 3.0.x forgot to mark this as a module function which causes:
#
#   NoMethodError: undefined method `wrap' for RSpec::Core::Notifications::NullColorizer:Class
#     .../rspec-core-3.0.4/lib/rspec/core/notifications.rb:229:in `add_shared_group_line'
#     .../rspec-core-3.0.4/lib/rspec/core/notifications.rb:157:in `message_lines'
#
if defined?(RSpec::Core::Notifications::NullColorizer) && RSpec::Core::Notifications::NullColorizer.is_a?(Class) && !RSpec::Core::Notifications::NullColorizer.respond_to?(:wrap)
  RSpec::Core::Notifications::NullColorizer.class_eval do
    def self.wrap(*args)
      new.wrap(*args)
    end
  end
end
