RSpec::Matchers.define :have_permissions do |repo, attrs|
  match do |user|
    const = attrs && attrs.any? ? ::Matchers::HaveAllPermissions : ::Matchers::HaveAnyPermissions
    @matcher = const.new(user, repo, attrs)
    @matcher.result
  end

  failure_message { |*| @matcher.failure_message }
  failure_message_when_negated { |*| @matcher.failure_message_when_negated }
end

module Matchers
  class HaveAllPermissions < Struct.new(:user, :repo, :attrs)
    MSGS = {
      positive: "User id=%d was expected to have the following permissions on repo %s but did not:\n\n%s\n\n%s\n\n%s",
      negative: "User id=%d was expected to not have the following permissions on repo %s but did:\n\n%s\n\n%s",
      error:    "%s (actual: %s, expected: %s)"
    }

    def result
      attrs.each { |key, value| match(key, value) }
      errors.empty?
    end

    def failure_message
      MSGS[:positive] % [user.id, repo.slug.inspect, formatted_errors, permissions.inspect, { user: user.login, repo: repo.name }.merge(attrs).inspect]
    end

    def failure_message_when_negated
      MSGS[:negative] % [user.id, repo.slug.inspect, permissions.inspect, { user_id: user.id, repo_id: repo.id }.merge(attrs).inspect]
    end

    private

      def match(key, value)
        match = permissions && permissions.send(key) == value
        errors << key unless match
        match
      end

      def permissions
        @permissions ||= model(:permission).first(user_id: user.id, repository_id: repo.id)
      end

      def errors
        @errors ||= []
      end

      def formatted_errors
        errors.map { |attr| format_error(attr) }.join("\n")
      end

      def format_error(attr)
        MSGS[:error] % [attr, permissions ? permissions.send(attr) : nil, attrs[attr]].map(&:inspect)
      end
  end

  class HaveAnyPermissions < Struct.new(:user, :repo, :attrs)
    MSGS = {
      positive: "User id=%d was expected to have permissions on repo %s but did not:\n\n%s",
      negative: "User id=%d was expected to not have any permissions on repo %s but did:\n\n%s\n\n%s",
    }

    def result
      !!permissions
    end

    def failure_message
      MSGS[:positive] % [repo.id, repo.slug, formatted_errors, repo.inspect]
    end

    def failure_message_when_negated
      MSGS[:negative] % [repo.id, repo.slug, formatted_errors, repo.inspect, permissions.inspect]
    end

    private

      def match(key, value)
        match = permissions.send(key) == value
        errors << key unless match
        match
      end

      def permissions
        @permissions ||= model(:permission).where(user_id: user.id, repository_id: repo.id).first
      end

      def errors
        @errors ||= []
      end

      def formatted_errors
        errors.map { |attr| format_error(attr) }.join("\n")
      end

      def format_error(attr)
        MSGS[:error] % [attr, permissions.send(attr), attrs[attr]].map(&:inspect)
      end
  end
end
