module FactoryGirl
  module Strategy
    class Stub
      @@next_id = 1000

      DISABLED_PERSISTENCE_METHODS = [
        :connection,
        :decrement!,
        :decrement,
        :delete,
        :destroy!,
        :destroy,
        :destroyed?,
        :increment!,
        :increment,
        :reload,
        :save!,
        :save,
        :toggle!,
        :toggle,
        :touch,
        :update!,
        :update,
        :update_attribute,
        :update_attributes!,
        :update_attributes,
        :update_column,
        :update_columns,
      ].freeze

      def association(runner)
        runner.run(:build_stubbed)
      end

      def result(evaluation)
        evaluation.object.tap do |instance|
          stub_database_interaction_on_result(instance)
          clear_changed_attributes_on_result(instance)
          evaluation.notify(:after_stub, instance)
        end
      end

      private

      def next_id
        @@next_id += 1
      end

      def stub_database_interaction_on_result(result_instance)
        result_instance.id ||= next_id

        result_instance.instance_eval do
          def persisted?
            !new_record?
          end

          def new_record?
            id.nil?
          end

          DISABLED_PERSISTENCE_METHODS.each do |write_method|
            define_singleton_method(write_method) do |*args|
              raise "stubbed models are not allowed to access the database - #{self.class}##{write_method}(#{args.join(",")})"
            end
          end
        end

        created_at_missing_default = result_instance.respond_to?(:created_at) && !result_instance.created_at
        result_instance_missing_created_at = !result_instance.respond_to?(:created_at)

        if created_at_missing_default || result_instance_missing_created_at
          result_instance.instance_eval do
            def created_at
              @created_at ||= Time.now.in_time_zone
            end
          end
        end

        has_updated_at = result_instance.respond_to?(:updated_at)
        updated_at_no_default = has_updated_at && !result_instance.updated_at
        result_instance_missing_updated_at = !has_updated_at

        if updated_at_no_default || result_instance_missing_updated_at
          result_instance.instance_eval do
            def updated_at
              @updated_at ||= Time.current
            end
          end
        end
      end

      def clear_changed_attributes_on_result(result_instance)
        unless result_instance.respond_to?(:clear_changes_information)
          result_instance.extend ActiveModelDirtyBackport
        end

        result_instance.clear_changes_information
      end
    end

    module ActiveModelDirtyBackport
      def clear_changes_information
        @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
        @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
      end
    end
  end
end
