require 'test/unit'
require "securerandom"

class TestObjectId < Test::Unit::TestCase
  def setup
    @obj = Object.new
  end

  def test_dup_new_id
    id = @obj.object_id
    refute_equal id, @obj.dup.object_id
  end

  def test_dup_with_ivar_and_id
    id = @obj.object_id
    @obj.instance_variable_set(:@foo, 42)

    copy = @obj.dup
    refute_equal id, copy.object_id
    assert_equal 42, copy.instance_variable_get(:@foo)
  end

  def test_dup_with_id_and_ivar
    @obj.instance_variable_set(:@foo, 42)
    id = @obj.object_id

    copy = @obj.dup
    refute_equal id, copy.object_id
    assert_equal 42, copy.instance_variable_get(:@foo)
  end

  def test_dup_with_id_and_ivar_and_frozen
    @obj.instance_variable_set(:@foo, 42)
    @obj.freeze
    id = @obj.object_id

    copy = @obj.dup
    refute_equal id, copy.object_id
    assert_equal 42, copy.instance_variable_get(:@foo)
    refute_predicate copy, :frozen?
  end

  def test_clone_new_id
    id = @obj.object_id
    refute_equal id, @obj.clone.object_id
  end

  def test_clone_with_ivar_and_id
    id = @obj.object_id
    @obj.instance_variable_set(:@foo, 42)

    copy = @obj.clone
    refute_equal id, copy.object_id
    assert_equal 42, copy.instance_variable_get(:@foo)
  end

  def test_clone_with_id_and_ivar
    @obj.instance_variable_set(:@foo, 42)
    id = @obj.object_id

    copy = @obj.clone
    refute_equal id, copy.object_id
    assert_equal 42, copy.instance_variable_get(:@foo)
  end

  def test_clone_with_id_and_ivar_and_frozen
    @obj.instance_variable_set(:@foo, 42)
    @obj.freeze
    id = @obj.object_id

    copy = @obj.clone
    refute_equal id, copy.object_id
    assert_equal 42, copy.instance_variable_get(:@foo)
    assert_predicate copy, :frozen?
  end

  def test_marshal_new_id
    return pass if @obj.is_a?(Module)

    id = @obj.object_id
    refute_equal id, Marshal.load(Marshal.dump(@obj)).object_id
  end

  def test_marshal_with_ivar_and_id
    return pass if @obj.is_a?(Module)

    id = @obj.object_id
    @obj.instance_variable_set(:@foo, 42)

    copy = Marshal.load(Marshal.dump(@obj))
    refute_equal id, copy.object_id
    assert_equal 42, copy.instance_variable_get(:@foo)
  end

  def test_marshal_with_id_and_ivar
    return pass if @obj.is_a?(Module)

    @obj.instance_variable_set(:@foo, 42)
    id = @obj.object_id

    copy = Marshal.load(Marshal.dump(@obj))
    refute_equal id, copy.object_id
    assert_equal 42, copy.instance_variable_get(:@foo)
  end

  def test_marshal_with_id_and_ivar_and_frozen
    return pass if @obj.is_a?(Module)

    @obj.instance_variable_set(:@foo, 42)
    @obj.freeze
    id = @obj.object_id

    copy = Marshal.load(Marshal.dump(@obj))
    refute_equal id, copy.object_id
    assert_equal 42, copy.instance_variable_get(:@foo)
    refute_predicate copy, :frozen?
  end

  def test_object_id_need_resize
    (3 - @obj.instance_variables.size).times do |i|
      @obj.instance_variable_set("@a_#{i}", "[Bug #21445]")
    end
    @obj.object_id
    GC.start
  end
end

class TestObjectIdClass < TestObjectId
  def setup
    @obj = Class.new
  end
end

class TestObjectIdGeneric < TestObjectId
  def setup
    @obj = Array.new
  end
end

class TestObjectIdTooComplex < TestObjectId
  class TooComplex
    def initialize
      @too_complex_obj_id_test = 1
    end
  end

  def setup
    if defined?(RubyVM::Shape::SHAPE_MAX_VARIATIONS)
      assert_equal 8, RubyVM::Shape::SHAPE_MAX_VARIATIONS
    end
    8.times do |i|
      TooComplex.new.instance_variable_set("@TestObjectIdTooComplex#{i}", 1)
    end
    @obj = TooComplex.new
    @obj.instance_variable_set("@a#{rand(10_000)}", 1)

    if defined?(RubyVM::Shape)
      assert_predicate(RubyVM::Shape.of(@obj), :too_complex?)
    end
  end
end

class TestObjectIdTooComplexClass < TestObjectId
  class TooComplex < Module
  end

  def setup
    if defined?(RubyVM::Shape::SHAPE_MAX_VARIATIONS)
      assert_equal 8, RubyVM::Shape::SHAPE_MAX_VARIATIONS
    end

    @obj = TooComplex.new

    @obj.instance_variable_set("@___#{SecureRandom.hex}", 1)

    8.times do |i|
      @obj.instance_variable_set("@TestObjectIdTooComplexClass#{i}", 1)
      @obj.remove_instance_variable("@TestObjectIdTooComplexClass#{i}")
    end

    @obj.instance_variable_set("@test", 1)

    if defined?(RubyVM::Shape)
      assert_predicate(RubyVM::Shape.of(@obj), :too_complex?)
    end
  end
end

class TestObjectIdTooComplexGeneric < TestObjectId
  class TooComplex < Array
  end

  def setup
    if defined?(RubyVM::Shape::SHAPE_MAX_VARIATIONS)
      assert_equal 8, RubyVM::Shape::SHAPE_MAX_VARIATIONS
    end
    8.times do |i|
      TooComplex.new.instance_variable_set("@TestObjectIdTooComplexGeneric#{i}", 1)
    end
    @obj = TooComplex.new
    @obj.instance_variable_set("@a#{rand(10_000)}", 1)
    @obj.instance_variable_set("@a#{rand(10_000)}", 1)

    if defined?(RubyVM::Shape)
      assert_predicate(RubyVM::Shape.of(@obj), :too_complex?)
    end
  end
end

class TestObjectIdRactor < Test::Unit::TestCase
  def test_object_id_race_free
    assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}")
    begin;
      Warning[:experimental] = false
      class MyClass
        attr_reader :a, :b, :c
        def initialize
          @a = @b = @c = nil
        end
      end
      N = 10_000
      objs = Ractor.make_shareable(N.times.map { MyClass.new })
      results = 4.times.map{
        Ractor.new(objs) { |objs|
          vars = []
          ids = []
          objs.each do |obj|
            vars << obj.a << obj.b << obj.c
            ids << obj.object_id
          end
          [vars, ids]
        }
      }.map(&:value)
      assert_equal 1, results.uniq.size
    end;
  end

  def test_external_object_id_ractor_move
    assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}")
    begin;
      Warning[:experimental] = false
      class MyClass
        attr_reader :a, :b, :c
        def initialize
          @a = @b = @c = nil
        end
      end
      obj = Ractor.make_shareable(MyClass.new)
      object_id = obj.object_id
      obj = Ractor.new { Ractor.receive }.send(obj, move: true).value
      assert_equal object_id, obj.object_id
    end;
  end
end

class TestObjectIdStruct < TestObjectId
  EmbeddedStruct = Struct.new(:embedded_field)

  def setup
    @obj = EmbeddedStruct.new
  end
end

class TestObjectIdStructGenIvar < TestObjectId
  GenIvarStruct = Struct.new(:a, :b, :c)

  def setup
    @obj = GenIvarStruct.new
  end
end

class TestObjectIdStructNotEmbed < TestObjectId
  MANY_IVS = 80

  StructNotEmbed = Struct.new(*MANY_IVS.times.map { |i| :"field_#{i}" })

  def setup
    @obj = StructNotEmbed.new
  end
end

class TestObjectIdStructTooComplex < TestObjectId
  StructTooComplex = Struct.new(:a) do
    def initialize
      @too_complex_obj_id_test = 1
    end
  end

  def setup
    if defined?(RubyVM::Shape::SHAPE_MAX_VARIATIONS)
      assert_equal 8, RubyVM::Shape::SHAPE_MAX_VARIATIONS
    end
    8.times do |i|
      StructTooComplex.new.instance_variable_set("@TestObjectIdStructTooComplex#{i}", 1)
    end
    @obj = StructTooComplex.new
    @obj.instance_variable_set("@a#{rand(10_000)}", 1)

    if defined?(RubyVM::Shape)
      assert_predicate(RubyVM::Shape.of(@obj), :too_complex?)
    end
  end
end
