require 'rubygems'
require 'hpricot'

#
# have_xpath matcher using hpricot
#
module Spec
  module Matchers

    class HaveXpath
      def initialize(xpath, inner_text_or_options, options)
        @xpath = xpath
        if Hash === inner_text_or_options
          @inner_text = nil
          @options = inner_text_or_options
        else
          @inner_text = inner_text_or_options
          @options = options
        end
      end

      def matches?(actual)
        @actual= actual
        @hdoc = @actual.is_a?(String) ? Hpricot.XML(@actual) : @actual
        m= @hdoc.search(@xpath)
        return @options[:count] == 0 if m.empty?

        if @inner_text
          m = filter_on_inner_text(m)
        end

        @actual_count = m.length
        return false if not acceptable_count?(@actual_count)

        not m.empty?
      end

      def failure_message
        explanation = @actual_count ? "but found #{@actual_count}" : "but did not"
        "expected\n#{@hdoc.to_s}\nto have #{failure_count_phrase} #{failure_selector_phrase}, #{explanation}"
      end

      def negative_failure_message
        explanation = @actual_count ? "but found #{@actual_count}" : "but did"
        "expected\n#{@hdoc.to_s}\nnot to have #{failure_count_phrase} #{failure_selector_phrase}, #{explanation}"
      end

      def description
        "match the xpath expression #{@xpath}"
      end

      private

      def filter_on_inner_text(elements)
        elements.select do |el|
          next(el.inner_text =~ @inner_text) if @inner_text.is_a?(Regexp)
          el.inner_text == @inner_text
        end
      end

      def acceptable_count?(actual_count)
        if @options[:count]
          return false unless @options[:count] === actual_count
        end
        if @options[:minimum]
          return false unless actual_count >= @options[:minimum]
        end
        if @options[:maximum]
          return false unless actual_count <= @options[:maximum]
        end
        true
      end

      def failure_count_phrase
        if @options[:count]
          "#{@options[:count]} elements matching"
        elsif @options[:minimum] || @options[:maximum]
          count_explanations = []
          count_explanations << "at least #{@options[:minimum]}" if @options[:minimum]
          count_explanations << "at most #{@options[:maximum]}" if @options[:maximum]
          "#{count_explanations.join(' and ')} elements matching"
        else
          "an element matching"
        end
      end

      def failure_selector_phrase
        phrase = @xpath.inspect
        phrase << (@inner_text ? " with inner text #{@inner_text.inspect}" : "")
      end

    end

    def have_xpath(xpath, inner_text_or_options=nil, options={})
      HaveXpath.new(xpath, inner_text_or_options, options)
    end

  end
end
