module Shoulda
  module Matchers
    module ActiveModel
      # The `have_secure_password` matcher tests usage of the
      # `has_secure_password` macro.
      #
      # #### Example
      #
      #     class User
      #       include ActiveModel::Model
      #       include ActiveModel::SecurePassword
      #       attr_accessor :password
      #
      #       has_secure_password
      #     end
      #
      #     # RSpec
      #     describe User do
      #       it { should have_secure_password }
      #     end
      #
      #     # Minitest (Shoulda)
      #     class UserTest < ActiveSupport::TestCase
      #       should have_secure_password
      #     end
      #
      # @return [HaveSecurePasswordMatcher]
      #
      def have_secure_password
        HaveSecurePasswordMatcher.new
      end

      # @private
      class HaveSecurePasswordMatcher
        attr_reader :failure_message

        CORRECT_PASSWORD = "aBcDe12345"
        INCORRECT_PASSWORD = "password"

        EXPECTED_METHODS = [
          :authenticate,
          :password=,
          :password_confirmation=,
          :password_digest,
          :password_digest=,
        ]

        MESSAGES = {
          authenticated_incorrect_password: "expected %{subject} to not authenticate an incorrect password",
          did_not_authenticate_correct_password: "expected %{subject} to authenticate the correct password",
          method_not_found: "expected %{subject} to respond to %{methods}"
        }

        def description
          "have a secure password"
        end

        def matches?(subject)
          @subject = subject

          if failure = validate
            key, params = failure
            @failure_message = MESSAGES[key] % { subject: subject.class }.merge(params)
          end

          failure.nil?
        end

        protected

        attr_reader :subject

        def validate
          missing_methods = EXPECTED_METHODS.select {|m| !subject.respond_to?(m) }

          if missing_methods.present?
            [:method_not_found, { methods: missing_methods.to_sentence }]
          else
            subject.password = CORRECT_PASSWORD
            subject.password_confirmation = CORRECT_PASSWORD

            if not subject.authenticate(CORRECT_PASSWORD)
              [:did_not_authenticate_correct_password, {}]
            elsif subject.authenticate(INCORRECT_PASSWORD)
              [:authenticated_incorrect_password, {}]
            end
          end
        end
      end
    end
  end
end
