module EmailSpec
  module Matchers
    class EmailMatcher
      def address_array
        if @email.perform_deliveries
          Array(yield)
        else
          []
        end
      end
    end

    class ReplyTo
      def initialize(email)
        @expected_reply_to = Mail::ReplyToField.new(email).addrs.first
      end

      def description
        "have reply to as #{@expected_reply_to.address}"
      end

      def matches?(email)
        @email = email
        @actual_reply_to = (email.reply_to || []).first
        !@actual_reply_to.nil? &&
          @actual_reply_to == @expected_reply_to.address
      end

      def failure_message
        "expected #{@email.inspect} to reply to #{@expected_reply_to.address.inspect}, but it replied to #{@actual_reply_to.inspect}"
      end

      def failure_message_when_negated
        "expected #{@email.inspect} not to deliver to #{@expected_reply_to.address.inspect}, but it did"
      end
      alias negative_failure_message failure_message_when_negated
    end

    def reply_to(email)
      ReplyTo.new(email)
    end

    alias :have_reply_to :reply_to

    class DeliverTo < EmailMatcher
      def initialize(expected_email_addresses_or_objects_that_respond_to_email)
        emails = expected_email_addresses_or_objects_that_respond_to_email.map do |email_or_object|
          email_or_object.kind_of?(String) ? email_or_object : email_or_object.email
        end

        @expected_recipients = Mail::ToField.new(emails).addrs.map(&:to_s).sort
      end

      def description
        "be delivered to #{@expected_recipients.inspect}"
      end

      def matches?(email)
        @email = email
        @actual_recipients = address_array{ email.header[:to].addrs }.map(&:to_s).sort
        @actual_recipients == @expected_recipients
      end

      def failure_message
        "expected #{@email.inspect} to deliver to #{@expected_recipients.inspect}, but it delivered to #{@actual_recipients.inspect}"
      end

      def failure_message_when_negated
        "expected #{@email.inspect} not to deliver to #{@expected_recipients.inspect}, but it did"
      end
      alias negative_failure_message failure_message_when_negated
    end

    def deliver_to(*expected_email_addresses_or_objects_that_respond_to_email)
      DeliverTo.new(expected_email_addresses_or_objects_that_respond_to_email.flatten)
    end

    alias :be_delivered_to :deliver_to

    class DeliverFrom < EmailMatcher

      def initialize(email)
        @expected_sender = Mail::FromField.new(email).addrs.first
      end

      def description
        "be delivered from #{@expected_sender}"
      end

      def matches?(email)
        @email = email
        @actual_sender = address_array{ email.header[:from].addrs }.first

        !@actual_sender.nil? &&
          @actual_sender.to_s == @expected_sender.to_s
      end

      def failure_message
        %(expected #{@email.inspect} to deliver from "#{@expected_sender.to_s}", but it delivered from "#{@actual_sender.to_s}")
      end

      def failure_message_when_negated
        %(expected #{@email.inspect} not to deliver from "#{@expected_sender.to_s}", but it did)
      end
      alias negative_failure_message failure_message_when_negated
    end

    def deliver_from(email)
      DeliverFrom.new(email)
    end

    alias :be_delivered_from :deliver_from

    class BccTo < EmailMatcher

      def initialize(expected_email_addresses_or_objects_that_respond_to_email)
        emails = expected_email_addresses_or_objects_that_respond_to_email.map do |email_or_object|
          email_or_object.kind_of?(String) ? email_or_object : email_or_object.email
        end

        @expected_email_addresses = emails.sort
      end

      def description
        "be bcc'd to #{@expected_email_addresses.inspect}"
      end

      def matches?(email)
        @email = email
        @actual_recipients = address_array{ email.bcc }.sort
        @actual_recipients == @expected_email_addresses
      end

      def failure_message
        "expected #{@email.inspect} to bcc to #{@expected_email_addresses.inspect}, but it was bcc'd to #{@actual_recipients.inspect}"
      end

      def failure_message_when_negated
        "expected #{@email.inspect} not to bcc to #{@expected_email_addresses.inspect}, but it did"
      end
      alias negative_failure_message failure_message_when_negated
    end

    def bcc_to(*expected_email_addresses_or_objects_that_respond_to_email)
      BccTo.new(expected_email_addresses_or_objects_that_respond_to_email.flatten)
    end

    class CcTo < EmailMatcher

      def initialize(expected_email_addresses_or_objects_that_respond_to_email)
        emails = expected_email_addresses_or_objects_that_respond_to_email.map do |email_or_object|
          email_or_object.kind_of?(String) ? email_or_object : email_or_object.email
        end

        @expected_email_addresses = emails.sort
      end

      def description
        "be cc'd to #{@expected_email_addresses.inspect}"
      end

      def matches?(email)
        @email = email
        @actual_recipients = address_array { email.cc }.sort
        @actual_recipients == @expected_email_addresses
      end

      def failure_message
        "expected #{@email.inspect} to cc to #{@expected_email_addresses.inspect}, but it was cc'd to #{@actual_recipients.inspect}"
      end

      def failure_message_when_negated
        "expected #{@email.inspect} not to cc to #{@expected_email_addresses.inspect}, but it did"
      end
      alias negative_failure_message failure_message_when_negated
    end

    def cc_to(*expected_email_addresses_or_objects_that_respond_to_email)
      CcTo.new(expected_email_addresses_or_objects_that_respond_to_email.flatten)
    end

    class HaveSubject

      def initialize(subject)
        @expected_subject = subject
      end

      def description
        if @expected_subject.is_a?(String)
          "have subject of #{@expected_subject.inspect}"
        else
          "have subject matching #{@expected_subject.inspect}"
        end
      end

      def matches?(email)
        @given_subject = email.subject
        if @expected_subject.is_a?(String)
          @given_subject == @expected_subject
        else
          !!(@given_subject =~ @expected_subject)
        end
      end

      def failure_message
        if @expected_subject.is_a?(String)
          "expected the subject to be #{@expected_subject.inspect} but was #{@given_subject.inspect}"
        else
          "expected the subject to match #{@expected_subject.inspect}, but did not.  Actual subject was: #{@given_subject.inspect}"
        end
      end

      def failure_message_when_negated
        if @expected_subject.is_a?(String)
          "expected the subject not to be #{@expected_subject.inspect} but was"
        else
          "expected the subject not to match #{@expected_subject.inspect} but #{@given_subject.inspect} does match it."
        end
      end
      alias negative_failure_message failure_message_when_negated
    end

    def have_subject(subject)
      HaveSubject.new(subject)
    end

    class IncludeEmailWithSubject

      def initialize(subject)
        @expected_subject = subject
      end

      def description
        if @expected_subject.is_a?(String)
          "include email with subject of #{@expected_subject.inspect}"
        else
          "include email with subject matching #{@expected_subject.inspect}"
        end
      end

      def matches?(emails)
        @given_emails = emails
        if @expected_subject.is_a?(String)
          @given_emails.map(&:subject).include?(@expected_subject)
        else
          !!(@given_emails.any?{ |mail| mail.subject =~ @expected_subject })
        end
      end

      def failure_message
        if @expected_subject.is_a?(String)
          "expected at least one email to have the subject #{@expected_subject.inspect} but none did. Subjects were #{@given_emails.map(&:subject).inspect}"
        else
          "expected at least one email to have a subject matching #{@expected_subject.inspect}, but none did. Subjects were #{@given_emails.map(&:subject).inspect}"
        end
      end

      def failure_message_when_negated
        if @expected_subject.is_a?(String)
          "expected no email with the subject #{@expected_subject.inspect} but found at least one. Subjects were #{@given_emails.map(&:subject).inspect}"
        else
          "expected no email to have a subject matching #{@expected_subject.inspect} but found at least one. Subjects were #{@given_emails.map(&:subject).inspect}"
        end
      end
      alias negative_failure_message failure_message_when_negated
    end

    def include_email_with_subject(*emails)
      IncludeEmailWithSubject.new(emails.flatten.first)
    end

    class HaveBodyText

      def initialize(text)
        @expected_text = text
      end

      def description
        if @expected_text.is_a?(String)
          "have body including #{@expected_text.inspect}"
        else
          "have body matching #{@expected_text.inspect}"
        end
      end

      def matches?(email)
        if @expected_text.is_a?(String)
          @given_text = email.default_part_body.to_s.gsub(/\s+/, " ")
          @expected_text = @expected_text.gsub(/\s+/, " ")
          @given_text.include?(@expected_text)
        else
          @given_text = email.default_part_body.to_s
          !!(@given_text =~ @expected_text)
        end
      end

      def failure_message
        if @expected_text.is_a?(String)
          "expected the body to contain #{@expected_text.inspect} but was #{@given_text.inspect}"
        else
          "expected the body to match #{@expected_text.inspect}, but did not.  Actual body was: #{@given_text.inspect}"
        end
      end

      def failure_message_when_negated
        if @expected_text.is_a?(String)
          "expected the body not to contain #{@expected_text.inspect} but was #{@given_text.inspect}"
        else
          "expected the body not to match #{@expected_text.inspect} but #{@given_text.inspect} does match it."
        end
      end
      alias negative_failure_message failure_message_when_negated
    end

    def have_body_text(text)
      HaveBodyText.new(text)
    end

    class HaveHeader

      def initialize(name, value)
        @expected_name, @expected_value = name, value
      end

      def description
        if @expected_value.is_a?(String)
          "have header #{@expected_name}: #{@expected_value}"
        else
          "have header #{@expected_name} with value matching #{@expected_value.inspect}"
        end
      end

      def matches?(email)
        @given_header = email.header

        if @expected_value.is_a?(String)
          @given_header[@expected_name].to_s == @expected_value
        else
          @given_header[@expected_name].to_s =~ @expected_value
        end      end

      def failure_message
        if @expected_value.is_a?(String)
          "expected the headers to include '#{@expected_name}: #{@expected_value}' but they were #{mail_headers_hash(@given_header).inspect}"
        else
          "expected the headers to include '#{@expected_name}' with a value matching #{@expected_value.inspect} but they were #{mail_headers_hash(@given_header).inspect}"
        end
      end

      def failure_message_when_negated
        if @expected_value.is_a?(String)
          "expected the headers not to include '#{@expected_name}: #{@expected_value}' but they were #{mail_headers_hash(@given_header).inspect}"
        else
          "expected the headers not to include '#{@expected_name}' with a value matching #{@expected_value.inspect} but they were #{mail_headers_hash(@given_header).inspect}"
        end
      end
      alias negative_failure_message failure_message_when_negated

      def mail_headers_hash(email_headers)
        email_headers.fields.inject({}) { |hash, field| hash[field.field.class::FIELD_NAME] = field.to_s; hash }
      end
    end

    def have_header(name, value)
      HaveHeader.new(name, value)
    end

    def self.included base
      if base.respond_to? :register_matcher
        instance_methods.each do |name|
          base.register_matcher name, name
        end
      end
    end
  end
end
