# frozen_string_literal: true
# rubocop:todo all

require "mongoid/validatable/macros"
require "mongoid/validatable/localizable"
require "mongoid/validatable/associated"
require "mongoid/validatable/format"
require "mongoid/validatable/length"
require "mongoid/validatable/numericality"
require "mongoid/validatable/queryable"
require "mongoid/validatable/presence"
require "mongoid/validatable/uniqueness"

module Mongoid

  # This module provides additional validations that ActiveModel does not
  # provide: validates_associated and validates_uniqueness_of.
  module Validatable
    extend ActiveSupport::Concern

    included do
      extend Macros
      include Macros
    end

    # Begin the associated validation.
    #
    # @example Begin validation.
    #   document.begin_validate
    def begin_validate
      Threaded.begin_validate(self)
    end

    # Exit the associated validation.
    #
    # @example Exit validation.
    #   document.exit_validate
    def exit_validate
      Threaded.exit_validate(self)
    end

    # Perform a validation within the associated block.
    def validating
      begin_validate
      yield
    ensure
      exit_validate
    end

    # Given the provided options, are we performing validations?
    #
    # @example Are we performing validations?
    #   document.performing_validations?(validate: true)
    #
    # @param [ Hash ] options The options to check.
    #
    # @option options [ true | false ] :validate Whether or not to validate.
    #
    # @return [ true | false ] If we are validating.
    def performing_validations?(options = {})
      options[:validate].nil? ? true : options[:validate]
    end

    # Overrides the default ActiveModel behavior since we need to handle
    # validations of associations slightly different than just calling the
    # getter.
    #
    # @example Read the value.
    #   person.read_attribute_for_validation(:addresses)
    #
    # @param [ Symbol ] attr The name of the field or association.
    #
    # @return [ Object ] The value of the field or the association.
    def read_attribute_for_validation(attr)
      attribute = database_field_name(attr)
      if relations.key?(attribute)
        begin_validate
        relation = without_autobuild { send(attr) }
        exit_validate
        relation.try(:in_memory) || relation
      elsif fields[attribute].try(:localized?)
        attributes[attribute]
      else
        send(attr)
      end
    end

    # Determine if the document is valid.
    #
    # @example Is the document valid?
    #   person.valid?
    #
    # @example Is the document valid in a context?
    #   person.valid?(:create)
    #
    # @param [ Symbol ] context The optional validation context.
    #
    # @return [ true | false ] True if valid, false if not.
    def valid?(context = nil)
      super context ? context : (new_record? ? :create : :update)
    end

    # Used to prevent infinite loops in associated validations.
    #
    # @example Is the document validated?
    #   document.validated?
    #
    # @return [ true | false ] Has the document already been validated?
    def validated?
      Threaded.validated?(self)
    end

    # Are we currently performing a validation that has a query?
    #
    # @example Are we validating with a query?
    #   document.validating_with_query?
    #
    # @return [ true | false ] If we are validating with a query.
    def validating_with_query?
      self.class.validating_with_query?
    end

    module ClassMethods

      # Adds an associated validator for the association if the validate option
      # was not provided or set to true.
      #
      # @example Set up validation.
      #   Person.validates_relation(association)
      #
      # @param [ Mongoid::Association::Relatable ] association The association metadata.
      def validates_relation(association)
        if association.validate?
          validates_associated(association.name)
        end
      end

      # Add validation with the supplied validators for the provided fields
      # with options.
      #
      # @example Validate with a specific validator.
      #   validates_with MyValidator, on: :create
      #
      # @param [ ActiveModel::Validator..., Hash ] *args The validator classes
      #   and options hash.
      #
      # @note See ActiveModel::Validations::With for full options. This is
      #   overridden to add autosave functionality when presence validation is
      #   added.
      def validates_with(*args, &block)
        if args.first == PresenceValidator
          args.last[:attributes].each do |name|
            association = relations[name.to_s]
            if association && association.autosave?
              Association::Referenced::AutoSave.define_autosave!(association)
            end
          end
        end
        super
      end

      # Are we currently performing a validation that has a query?
      #
      # @example Are we validating with a query?
      #   Model.validating_with_query?
      #
      # @return [ true | false ] If we are validating with a query.
      def validating_with_query?
        Threaded.executing?("#{name}-validate-with-query")
      end
    end
  end
end
