require 'rspec/expectations'
require 'rspec/core/version'

module StatsD::Instrument::Matchers
  CUSTOM_MATCHERS = {
    increment: :c,
    measure: :ms,
    gauge: :g,
    histogram: :h,
    distribution: :d,
    set: :s,
    key_value: :kv
  }

  class Matcher
    include RSpec::Matchers::Composable if RSpec::Core::Version::STRING.start_with?('3')
    include StatsD::Instrument::Helpers

    def initialize(metric_type, metric_name, options = {})
      @metric_type = metric_type
      @metric_name = metric_name
      @options = options
    end

    def matches?(block)
      begin
        expect_statsd_call(@metric_type, @metric_name, @options, &block)
      rescue RSpec::Expectations::ExpectationNotMetError => e
        @message = e.message

        false
      end
    end

    def failure_message
      @message
    end

    def failure_message_when_negated
      "No StatsD calls for metric #{@metric_name} expected."
    end

    def supports_block_expectations?
      true
    end

    private

    def expect_statsd_call(metric_type, metric_name, options, &block)
      metrics = capture_statsd_calls(&block)
      metrics = metrics.select { |m| m.type == metric_type && m.name == metric_name }

      raise RSpec::Expectations::ExpectationNotMetError, "No StatsD calls for metric #{metric_name} were made." if metrics.empty?
      raise RSpec::Expectations::ExpectationNotMetError, "The numbers of StatsD calls for metric #{metric_name} was unexpected. Expected #{options[:times].inspect}, got #{metrics.length}" if options[:times] && options[:times] != metrics.length

      [:sample_rate, :value, :tags].each do |expectation|
        next unless options[expectation]

        num_matches = metrics.count do |m|
          matcher = RSpec::Matchers::BuiltIn::Match.new(options[expectation])
          matcher.matches?(m.public_send(expectation))
        end

        found = options[:times] ? num_matches == options[:times] : num_matches > 0

        if !found
          message = metric_information(metric_name, options, metrics, expectation)
          raise RSpec::Expectations::ExpectationNotMetError, message
        end
      end

      true
    end

    def metric_information(metric_name, options, metrics, expectation)
      message = "expected StatsD #{expectation.inspect} for metric '#{metric_name}' to be called"

      message += "\n  "
      message += options[:times] ? "exactly #{options[:times]} times" : "at least once"
      message += " with: #{options[expectation]}"

      message += "\n  captured metric values: #{metrics.map(&expectation).join(', ')}"

      message
    end
  end

  CUSTOM_MATCHERS.each do |method_name, metric_type|
    klass = Class.new(Matcher)

    define_method "trigger_statsd_#{method_name}" do |metric_name, options = {}|
      klass.new(metric_type, metric_name, options)
    end

    StatsD::Instrument::Matchers.const_set(method_name.capitalize, klass)
  end
end