class RSpecHtmlMatchers::HaveTag

rubocop:disable Metrics/ClassLength
@private
@api

def classes_to_selector classes

def classes_to_selector classes
  case classes
  when Array
    classes.join('.')
  when String
    classes.gsub(/\s+/, '.')
  end
end

def count_is_range_but_no_min?

def count_is_range_but_no_min?
  options[:count].is_a?(Range) &&
    (options[:count].min.nil? || (options[:count].min < 0))
end

def count_right? # rubocop:disable Metrics/AbcSize, Metrics/MethodLength

rubocop:disable Metrics/AbcSize, Metrics/MethodLength
def count_right? # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
  case options[:count]
  when Integer
    if @count == options[:count]
      match_succeeded! :unexpected_count, document, @count, tag
    else
      match_failed! :expected_count, document, options[:count], tag, @count
    end
  when Range
    if options[:count].member? @count
      match_succeeded! :unexpected_btw_count, document, options[:count].min, options[:count].max, tag, @count
    else
      match_failed! :expected_btw_count, document, options[:count].min, options[:count].max, tag, @count
    end
  when nil
    if options[:maximum]
      if @count <= options[:maximum]
        match_succeeded! :unexpected_at_most, document, options[:maximum], tag, @count
      else
        match_failed! :expected_at_most, document, options[:maximum], tag, @count
      end
    elsif options[:minimum]
      if @count >= options[:minimum]
        match_succeeded! :unexpected_at_least, document, options[:minimum], tag, @count
      else
        match_failed! :expected_at_least, document, options[:minimum], tag, @count
      end
    else
      true
    end
  end
end

def description

def description
  # TODO: should it be more complicated?
  if options.key?(:count)
    format(DESCRIPTIONS[:have_n], options[:count], tag)
  else
    DESCRIPTIONS[:have_at_least_1] % tag
  end
end

def initialize tag, options = {}, &block

def initialize tag, options = {}, &block
  @tag = tag.to_s
  @options = options
  @block = block
  if with_attrs = @options.delete(:with)
    if classes = with_attrs.delete(:class)
      @tag += '.' + classes_to_selector(classes)
    end
    selector = with_attrs.inject('') do |html_attrs_string, (k, v)|
      html_attrs_string += "[#{k}='#{v}']"
      html_attrs_string
    end
    @tag += selector
  end
  if without_attrs = @options.delete(:without)
    if classes = without_attrs.delete(:class)
      @tag += ":not(.#{classes_to_selector(classes)})"
    end
  end
  validate_options!
  organize_options!
end

def match_failed! message, *args

def match_failed! message, *args
  @failure_message = format MESSAGES[message], *args
  false
end

def match_succeeded! message, *args

def match_succeeded! message, *args
  @failure_message_when_negated = format MESSAGES[message], *args
  true
end

def matches? src, &block

def matches? src, &block
  @block = block if block
  src = src.html if defined?(Capybara::Session) && src.is_a?(Capybara::Session)
  case src
  when String
    parent_scope = Nokogiri::HTML(src)
    @document    = src
  else
    parent_scope  = src.current_scope
    @document     = parent_scope.to_html
  end
  @current_scope = begin
                     parent_scope.css(tag)
                     # on jruby this produce exception if css was not found:
                     # undefined method `decorate' for nil:NilClass
                   rescue NoMethodError
                     Nokogiri::XML::NodeSet.new(Nokogiri::XML::Document.new)
                   end
  if tag_presents? && proper_content? && count_right?
    @block.call(self) if @block
    true
  else
    false
  end
end

def maybe_empty?

def maybe_empty?
  if options[:blank] && current_scope.children.empty?
    match_succeeded! :unexpected_blank, tag, document
  else
    match_failed! :expected_blank, tag, document
  end
end

def organize_options!

def organize_options!
  @options[:minimum] ||= @options.delete(:min)
  @options[:maximum] ||= @options.delete(:max)
  @options[:text] = @options[:text].to_s if @options.key?(:text) && !@options[:text].is_a?(Regexp)
  if @options.key?(:seen) && !@options[:seen].is_a?(Regexp) # rubocop:disable Style/GuardClause
    @options[:text] = @options[:seen].to_s
    @options[:squeeze_text] = true
  end
end

def proper_content?

def proper_content?
  if options.key?(:blank)
    maybe_empty?
  else
    text_right?
  end
end

def tag_presents?

def tag_presents?
  if current_scope.first
    @count = current_scope.count
    match_succeeded! :unexpected_tag, document, tag, @count
  else
    match_failed! :expected_tag, document, tag
  end
end

def text_right?

def text_right?
  return true unless options[:text]
  case text = options[:text]
  when Regexp
    new_scope = current_scope.css(':regexp()', NokogiriRegexpHelper.new(text))
    if new_scope.empty?
      match_failed! :expected_regexp, text.inspect, tag, document
    else
      @count = new_scope.count
      match_succeeded! :unexpected_regexp, text.inspect, tag, document
    end
  else
    new_scope = current_scope.css(':content()', NokogiriTextHelper.new(text, options[:squeeze_text]))
    if new_scope.empty?
      match_failed! :expected_text, text, tag, document
    else
      @count = new_scope.count
      match_succeeded! :unexpected_text, text, tag, document
    end
  end
end

def validate_count_presence!

def validate_count_presence!
  raise 'wrong :count specified' unless [Range, NilClass].include?(options[:count].class) || options[:count].is_a?(Integer)
  [:min, :minimum, :max, :maximum].each do |key|
    raise MESSAGES[:wrong_count_error] if options.key?(key) && options.key?(:count)
  end
end

def validate_count_when_set_min_max!

def validate_count_when_set_min_max!
  raise MESSAGES[:min_max_error] if options[:minimum] > options[:maximum]
rescue NoMethodError # nil > 4 # rubocop:disable Lint/HandleExceptions
rescue ArgumentError # 2 < nil # rubocop:disable Lint/HandleExceptions
end

def validate_count_when_set_range!

def validate_count_when_set_range!
  begin
    raise format(MESSAGES[:bad_range_error], options[:count].to_s) if count_is_range_but_no_min?
  rescue ArgumentError, 'comparison of String with' # if options[:count] == 'a'..'z' # rubocop:disable Lint/RescueType
    raise format(MESSAGES[:bad_range_error], options[:count].to_s)
  end
rescue TypeError # fix for 1.8.7 for 'rescue ArgumentError, "comparison of String with"' stroke
  raise format(MESSAGES[:bad_range_error], options[:count].to_s)
end

def validate_html_body_tags!

=> []
irb(main):012:0> Nokogiri::HTML('

asd

').xpath('//a')
=> [#]>]
irb(main):011:0> Nokogiri::HTML('

asd

').xpath('//p')
=> [#]>]>]
irb(main):010:0> Nokogiri::HTML('

asd

').xpath('//body')
=> [#]>]>]>]
irb(main):009:0> Nokogiri::HTML('

asd

').xpath('//html')
here is a demo:
def validate_html_body_tags!
  if %w[html body].include?(tag) && options.empty?
    raise ArgumentError, 'matching <html> and <body> tags without specifying additional options does not work, see: https://github.com/kucaahbe/rspec-html-matchers/pull/75'
  end
end

def validate_options!

def validate_options!
  validate_html_body_tags!
  validate_text_options!
  validate_count_presence!
  validate_count_when_set_min_max!
  validate_count_when_set_range!
end

def validate_text_options!

def validate_text_options!
  # TODO: test these options validations
  if options.key?(:blank) && options[:blank] && options.key?(:text) # rubocop:disable Style/GuardClause, Style/IfUnlessModifier
    raise ':text option is not accepted when :blank => true'
  end
end