module FactoryGirl
  class DefinitionProxy
    UNPROXIED_METHODS = %w(__send__ __id__ nil? send object_id extend instance_eval initialize block_given? raise caller method)

    (instance_methods + private_instance_methods).each do |method|
      undef_method(method) unless UNPROXIED_METHODS.include?(method.to_s)
    end

    delegate :before, :after, :callback, to: :@definition

    attr_reader :child_factories

    def initialize(definition, ignore = false)
      @definition      = definition
      @ignore          = ignore
      @child_factories = []
    end

    def singleton_method_added(name)
      message = "Defining methods in blocks (trait or factory) is not supported (#{name})"
      raise FactoryGirl::MethodDefinitionError, message
    end

    # Adds an attribute that should be assigned on generated instances for this
    # factory.
    #
    # This method should be called with either a value or block, but not both. If
    # called with a block, the attribute will be generated "lazily," whenever an
    # instance is generated. Lazy attribute blocks will not be called if that
    # attribute is overridden for a specific instance.
    #
    # When defining lazy attributes, an instance of FactoryGirl::Strategy will
    # be yielded, allowing associations to be built using the correct build
    # strategy.
    #
    # Arguments:
    # * name: +Symbol+ or +String+
    #   The name of this attribute. This will be assigned using "name=" for
    #   generated instances.
    # * value: +Object+
    #   If no block is given, this value will be used for this attribute.
    def add_attribute(name, value = nil, &block)
      raise AttributeDefinitionError, 'Both value and block given' if value && block_given?

      declaration = if block_given?
        Declaration::Dynamic.new(name, @ignore, block)
      else
        Declaration::Static.new(name, value, @ignore)
      end

      @definition.declare_attribute(declaration)
    end

    def ignore(&block)
      ActiveSupport::Deprecation.warn "`#ignore` is deprecated and will be "\
        "removed in 5.0. Please use `#transient` instead."
      transient &block
    end

    def transient(&block)
      proxy = DefinitionProxy.new(@definition, true)
      proxy.instance_eval(&block)
    end

    # Calls add_attribute using the missing method name as the name of the
    # attribute, so that:
    #
    #   factory :user do
    #     name 'Billy Idol'
    #   end
    #
    # and:
    #
    #   factory :user do
    #     add_attribute :name, 'Billy Idol'
    #   end
    #
    # are equivalent.
    #
    # If no argument or block is given, factory_girl will look for a sequence
    # or association with the same name. This means that:
    #
    #   factory :user do
    #     email { create(:email) }
    #     association :account
    #   end
    #
    # and:
    #
    #   factory :user do
    #     email
    #     account
    #   end
    #
    # are equivalent.
    def method_missing(name, *args, &block)
      if args.empty? && block.nil?
        @definition.declare_attribute(Declaration::Implicit.new(name, @definition, @ignore))
      elsif args.first.respond_to?(:has_key?) && args.first.has_key?(:factory)
        association(name, *args)
      else
        add_attribute(name, *args, &block)
      end
    end

    # Adds an attribute that will have unique values generated by a sequence with
    # a specified format.
    #
    # The result of:
    #   factory :user do
    #     sequence(:email) { |n| "person#{n}@example.com" }
    #   end
    #
    # Is equal to:
    #   sequence(:email) { |n| "person#{n}@example.com" }
    #
    #   factory :user do
    #     email { FactoryGirl.create(:email) }
    #   end
    #
    # Except that no globally available sequence will be defined.
    def sequence(name, *args, &block)
      sequence = Sequence.new(name, *args, &block)
      add_attribute(name) { increment_sequence(sequence) }
    end

    # Adds an attribute that builds an association. The associated instance will
    # be built using the same build strategy as the parent instance.
    #
    # Example:
    #   factory :user do
    #     name 'Joey'
    #   end
    #
    #   factory :post do
    #     association :author, factory: :user
    #   end
    #
    # Arguments:
    # * name: +Symbol+
    #   The name of this attribute.
    # * options: +Hash+
    #
    # Options:
    # * factory: +Symbol+ or +String+
    #    The name of the factory to use when building the associated instance.
    #    If no name is given, the name of the attribute is assumed to be the
    #    name of the factory. For example, a "user" association will by
    #    default use the "user" factory.
    def association(name, *options)
      @definition.declare_attribute(Declaration::Association.new(name, *options))
    end

    def to_create(&block)
      @definition.to_create(&block)
    end

    def skip_create
      @definition.skip_create
    end

    def factory(name, options = {}, &block)
      @child_factories << [name, options, block]
    end

    def trait(name, &block)
      @definition.define_trait(Trait.new(name, &block))
    end

    def initialize_with(&block)
      @definition.define_constructor(&block)
    end
  end
end