# frozen_string_literal: true

module AccessMatchersHelpers
  include Gitlab::Utils::StrongMemoize

  USER_ACCESSOR_METHOD_NAME = 'user'

  def provide_user(role, membership = nil)
    case role
    when :admin
      create(:admin)
    when :auditor
      create(:user, :auditor)
    when :user
      create(:user)
    when :external
      create(:user, :external)
    when :visitor, :anonymous
      nil
    when User
      role
    when *Gitlab::Access.sym_options_with_owner.keys # owner, maintainer, developer, reporter, guest
      raise ArgumentError, "cannot provide #{role} when membership reference is blank" unless membership

      provide_user_by_membership(role, membership)
    else
      raise ArgumentError, "cannot provide user of an unknown role #{role}"
    end
  end

  def provide_user_by_membership(role, membership)
    if role == :owner && membership.owner
      membership.owner
    else
      create(:user).tap do |user|
        membership.public_send(:"add_#{role}", user)
      end
    end
  end

  def raise_if_non_block_expectation!(actual)
    raise ArgumentError, 'This matcher supports block expectations only.' unless actual.is_a?(Proc)
  end

  def update_owner(objects, user)
    return unless objects

    objects.each do |object|
      if object.respond_to?(:owner)
        object.update_attribute(:owner, user)
      elsif object.respond_to?(:user)
        object.update_attribute(:user, user)
      else
        raise ArgumentError, "cannot own this object #{object}"
      end
    end
  end

  def patch_example_group(user)
    return if user.nil? # for anonymous users

    # This call is evaluated in context of ExampleGroup instance in which the matcher is called. Overrides the `user`
    # (or defined by `method_name`) method generated by `let` definition in example group before it's used by `subject`.
    # This override is per concrete example only because the example group class gets re-created for each example.
    instance_eval(<<~CODE, __FILE__, __LINE__ + 1)
      def #{USER_ACCESSOR_METHOD_NAME}
        @#{USER_ACCESSOR_METHOD_NAME} ||= User.find(#{user.id})
      end
    CODE
  end

  def prepare_matcher_environment(role, membership, owned_objects)
    user = provide_user(role, membership)

    if user
      update_owner(owned_objects, user)
      patch_example_group(user)
    end
  end

  def reset_matcher_environment
    instance_eval(<<~CODE, __FILE__, __LINE__ + 1)
      clear_memoization(:#{USER_ACCESSOR_METHOD_NAME})
      undef #{USER_ACCESSOR_METHOD_NAME} if defined? user
    CODE
  end

  def run_matcher(action, role, membership, owned_objects)
    raise_if_non_block_expectation!(action)

    prepare_matcher_environment(role, membership, owned_objects)

    if block_given?
      yield action
    else
      action.call
    end

    reset_matcher_environment
  end
end
