# frozen_string_literal: true

require_relative '../examiner'
require_relative 'smell_matcher'
require_relative '../configuration/app_configuration'

module Reek
  module Spec
    #
    # An rspec matcher that matches when the +actual+ has the specified
    # code smell.
    #
    class ShouldReekOf
      include RSpec::Matchers::Composable

      attr_reader :failure_message, :failure_message_when_negated

      def initialize(smell_type,
                     smell_details = {},
                     configuration = Configuration::AppConfiguration.default)
        @smell_type = smell_type.to_s
        @smell_details = smell_details
        configuration.load_values(DETECTORS_KEY =>
                                    { smell_type => { SmellConfiguration::ENABLED_KEY => true } })
        @configuration = configuration
      end

      def matches?(source)
        @matching_smell_types = nil
        self.examiner = Examiner.new(source,
                                     filter_by_smells: [smell_type],
                                     configuration: configuration)
        set_failure_messages
        matching_smell_details?
      end

      def with_config(config_hash)
        new_configuration = Configuration::AppConfiguration.default
        new_configuration.load_values(DETECTORS_KEY =>
                                        { smell_type => config_hash })
        self.class.new(smell_type, smell_details, new_configuration)
      end

      private

      attr_reader :configuration, :smell_type, :smell_details
      attr_writer :failure_message, :failure_message_when_negated
      attr_accessor :examiner

      def set_failure_messages
        # We set the failure messages for non-matching smell type unconditionally since we
        # need that in any case for "failure_message_when_negated" below.
        # Depending on the existence of matching smell type we check for matching
        # smell details and then overwrite our failure messages conditionally.
        set_failure_messages_for_smell_type
        set_failure_messages_for_smell_details if matching_smell_types? && !matching_smell_details?
      end

      def matching_smell_types
        @matching_smell_types ||= examiner.smells.map { SmellMatcher.new(_1) }
      end

      def matching_smell_types?
        matching_smell_types.any?
      end

      def matching_smell_details?
        matching_smell_types.any? { |warning| warning.matches_attributes?(smell_details) }
      end

      def set_failure_messages_for_smell_type
        self.failure_message = "Expected #{origin} to reek of #{smell_type}, " \
                               'but it didn\'t'
        self.failure_message_when_negated = "Expected #{origin} not to reek " \
                                            "of #{smell_type}, but it did"
      end

      def set_failure_messages_for_smell_details
        self.failure_message =
          "Expected #{origin} to reek of #{smell_type} " \
          "(which it did) with smell details #{smell_details}, which it didn't.\n" \
          "The number of smell details I had to compare with the given one was #{matching_smell_types.count} " \
          "and here they are:\n" \
          "#{all_relevant_smell_details_formatted}"
        self.failure_message_when_negated =
          "Expected #{origin} not to reek of " \
          "#{smell_type} with smell details #{smell_details}, but it did"
      end

      # @quality :reek:FeatureEnvy
      def all_relevant_smell_details_formatted
        matching_smell_types.each_with_object([]).with_index do |(smell, accumulator), index|
          accumulator << "#{index + 1}.)\n"
          warning_as_hash = smell.smell_warning.to_hash
          warning_as_hash.delete('smell_type')
          accumulator << "#{warning_as_hash}\n"
        end.join
      end

      def origin
        examiner.origin
      end
    end
  end
end
