# frozen_string_literal: false
require 'test/unit'
require 'forwardable'

class TestForwardable < Test::Unit::TestCase
  INTEGER = 42
  RECEIVER = BasicObject.new
  RETURNED1 = BasicObject.new
  RETURNED2 = BasicObject.new

  class << RECEIVER
    def delegated1
      RETURNED1
    end

    def delegated2
      RETURNED2
    end

    def delegated1_kw(**kw)
      [RETURNED1, kw]
    end
  end

  def test_def_instance_delegator
    %i[def_delegator def_instance_delegator].each do |m|
      ret = nil
      cls = forwardable_class do
        ret = __send__ m, :@receiver, :delegated1
      end

      assert_same RETURNED1, cls.new.delegated1
      assert_equal :delegated1, ret
    end
  end

  def test_def_instance_delegator_constant
    %i[def_delegator def_instance_delegator].each do |m|
      cls = forwardable_class do
        __send__ m, 'TestForwardable::INTEGER', :to_i
      end

      assert_equal 42, cls.new.to_i
    end
  end

  def test_def_instance_delegator_kw
    %i[def_delegator def_instance_delegator].each do |m|
      cls = forwardable_class do
        __send__ m, :@receiver, :delegated1_kw
      end

      ary = cls.new.delegated1_kw b: 1
      assert_same RETURNED1, ary[0]
      assert_equal({b: 1}, ary[1])
    end
  end

  def test_def_instance_delegator_using_args_method_as_receiver
    %i[def_delegator def_instance_delegator].each do |m|
      cls = forwardable_class(
        receiver_name: :args,
        type: :method,
        visibility: :private
      ) do
        __send__ m, :args, :delegated1
      end

      assert_same RETURNED1, cls.new.delegated1
    end
  end

  def test_def_instance_delegator_using_block_method_as_receiver
    %i[def_delegator def_instance_delegator].each do |m|
      cls = forwardable_class(
        receiver_name: :block,
        type: :method,
        visibility: :private
      ) do
        __send__ m, :block, :delegated1
      end

      assert_same RETURNED1, cls.new.delegated1
    end
  end

  def test_def_instance_delegators
    %i[def_delegators def_instance_delegators].each do |m|
      cls = forwardable_class do
        __send__ m, :@receiver, :delegated1, :delegated2
      end

      assert_same RETURNED1, cls.new.delegated1
      assert_same RETURNED2, cls.new.delegated2
    end
  end

  def test_def_instance_delegators_using_args_method_as_receiver
    %i[def_delegators def_instance_delegators].each do |m|
      cls = forwardable_class(
        receiver_name: :args,
        type: :method,
        visibility: :private
      ) do
        __send__ m, :args, :delegated1, :delegated2
      end

      assert_same RETURNED1, cls.new.delegated1
      assert_same RETURNED2, cls.new.delegated2
    end
  end

  def test_def_instance_delegators_using_block_method_as_receiver
    %i[def_delegators def_instance_delegators].each do |m|
      cls = forwardable_class(
        receiver_name: :block,
        type: :method,
        visibility: :private
      ) do
        __send__ m, :block, :delegated1, :delegated2
      end

      assert_same RETURNED1, cls.new.delegated1
      assert_same RETURNED2, cls.new.delegated2
    end
  end

  def test_def_instance_delegators_send_id
    %i[def_delegators def_instance_delegators].each do |m|
      cls = forwardable_class do
        attr_reader :receiver
        __send__ m, :@receiver, :__send__, :__id__
      end

      assert_not_equal cls.new.__id__, cls.new.receiver.__id__
      assert_not_equal cls.new.__send__(:__id__), cls.new.receiver.__send__(:__id__)
    end
  end

  def test_instance_delegate
    %i[delegate instance_delegate].each do |m|
      cls = forwardable_class do
        __send__ m, delegated1: :@receiver, delegated2: :@receiver
      end

      assert_same RETURNED1, cls.new.delegated1
      assert_same RETURNED2, cls.new.delegated2

      cls = forwardable_class do
        __send__ m, %i[delegated1 delegated2] => :@receiver
      end

      assert_same RETURNED1, cls.new.delegated1
      assert_same RETURNED2, cls.new.delegated2
    end
  end

  def test_def_instance_delegate_using_args_method_as_receiver
    %i[delegate instance_delegate].each do |m|
      cls = forwardable_class(
        receiver_name: :args,
        type: :method,
        visibility: :private
      ) do
        __send__ m, delegated1: :args, delegated2: :args
      end

      assert_same RETURNED1, cls.new.delegated1
      assert_same RETURNED2, cls.new.delegated2
    end
  end

  def test_def_instance_delegate_using_block_method_as_receiver
    %i[delegate instance_delegate].each do |m|
      cls = forwardable_class(
        receiver_name: :block,
        type: :method,
        visibility: :private
      ) do
        __send__ m, delegated1: :block, delegated2: :block
      end

      assert_same RETURNED1, cls.new.delegated1
      assert_same RETURNED2, cls.new.delegated2
    end
  end

  def test_class_single_delegator
    %i[def_delegator def_single_delegator].each do |m|
      ret = nil
      cls = single_forwardable_class do
        ret = __send__ m, :@receiver, :delegated1
      end

      assert_same RETURNED1, cls.delegated1
      assert_equal :delegated1, ret
    end
  end

  def test_class_single_delegators
    %i[def_delegators def_single_delegators].each do |m|
      cls = single_forwardable_class do
        __send__ m, :@receiver, :delegated1, :delegated2
      end

      assert_same RETURNED1, cls.delegated1
      assert_same RETURNED2, cls.delegated2
    end
  end

  def test_class_single_delegate
    %i[delegate single_delegate].each do |m|
      cls = single_forwardable_class do
        __send__ m, delegated1: :@receiver, delegated2: :@receiver
      end

      assert_same RETURNED1, cls.delegated1
      assert_same RETURNED2, cls.delegated2

      cls = single_forwardable_class do
        __send__ m, %i[delegated1 delegated2] => :@receiver
      end

      assert_same RETURNED1, cls.delegated1
      assert_same RETURNED2, cls.delegated2
    end
  end

  def test_obj_single_delegator
    %i[def_delegator def_single_delegator].each do |m|
      obj = single_forwardable_object do
        __send__ m, :@receiver, :delegated1
      end

      assert_same RETURNED1, obj.delegated1
    end
  end

  def test_obj_single_delegators
    %i[def_delegators def_single_delegators].each do |m|
      obj = single_forwardable_object do
        __send__ m, :@receiver, :delegated1, :delegated2
      end

      assert_same RETURNED1, obj.delegated1
      assert_same RETURNED2, obj.delegated2
    end
  end

  def test_obj_single_delegators_send_id
    %i[def_delegators def_single_delegators].each do |m|
      obj = single_forwardable_object do
        singleton_class.__send__ :attr_reader, :receiver
        __send__ m, :@receiver, :__send__, :__id__
      end

      assert_not_equal obj.__id__, obj.receiver.__id__
      assert_not_equal obj.__send__(:__id__), obj.receiver.__send__(:__id__)
    end
  end

  def test_obj_single_delegate
    %i[delegate single_delegate].each do |m|
      obj = single_forwardable_object do
        __send__ m, delegated1: :@receiver, delegated2: :@receiver
      end

      assert_same RETURNED1, obj.delegated1
      assert_same RETURNED2, obj.delegated2

      obj = single_forwardable_object do
        __send__ m, %i[delegated1 delegated2] => :@receiver
      end

      assert_same RETURNED1, obj.delegated1
      assert_same RETURNED2, obj.delegated2
    end
  end

  class Foo
    extend Forwardable

    attr_accessor :bar
    def_delegator :bar, :baz
    def_delegator :caller, :itself, :c
  end

  def test_backtrace_adjustment
    obj = Foo.new
    def (obj.bar = Object.new).baz
      foo
    end
    e = assert_raise(NameError) {
      obj.baz
    }
    assert_not_match(/\/forwardable\.rb/, e.backtrace[0],
                     proc {RubyVM::InstructionSequence.of(obj.method(:baz)).disassemble})
    assert_equal(caller(0, 1)[0], Foo.new.c[0])
  end

  class Foo2 < BasicObject
    extend ::Forwardable

    def_delegator :bar, :baz
  end

  def test_basicobject_subclass
    bug11616 = '[ruby-core:71176] [Bug #11616]'
    assert_raise_with_message(NameError, /`bar'/, bug11616) {
      Foo2.new.baz
    }
  end

  def test_aref
    obj = Object.new.extend SingleForwardable
    obj.instance_variable_set("@h", {foo: 42})
    obj.def_delegator("@h", :[])
    assert_equal(42, obj[:foo])
  end

  def test_aset
    obj = Object.new.extend SingleForwardable
    obj.instance_variable_set("@h", h = {foo: 0})
    obj.def_delegator("@h", :[]=)
    obj[:foo] = 42
    assert_equal(42, h[:foo])
  end

  def test_binop
    obj = Object.new.extend SingleForwardable
    obj.instance_variable_set("@h", 40)
    obj.def_delegator("@h", :+)
    assert_equal(42, obj+2)
  end

  def test_uniop
    obj = Object.new.extend SingleForwardable
    obj.instance_variable_set("@h", -42)
    obj.def_delegator("@h", :-@)
    assert_equal(42, -obj)
  end

  def test_on_private_method
    cls = Class.new do
      private def foo; :foo end
      extend Forwardable
      def_delegator :itself, :foo, :bar
    end
    assert_warn(/forwarding to private method/) do
      assert_equal(:foo, cls.new.bar)
    end
  end

  def test_non_module
    str = String.new
    str.extend Forwardable
    str.instance_variable_set("@h", 42)
    str.def_delegator("@h", :to_s, :forty_two)
    assert_equal("42", str.forty_two)
  end

  private

  def forwardable_class(
    receiver_name: :receiver,
    type: :ivar,
    visibility: :public,
    &block
  )
    Class.new do
      extend Forwardable

      define_method(:initialize) do
        instance_variable_set("@#{receiver_name}", RECEIVER)
      end

      if type == :method
        attr_reader(receiver_name)
        __send__(visibility, receiver_name)
      end

      class_exec(&block)
    end
  end

  def single_forwardable_class(&block)
    Class.new do
      extend SingleForwardable

      @receiver = RECEIVER

      class_exec(&block)
    end
  end

  def single_forwardable_object(&block)
    obj = Object.new.extend SingleForwardable
    obj.instance_variable_set(:@receiver, RECEIVER)
    obj.instance_eval(&block)
    obj
  end
end