require "spec"
require "../support/finalize"

private class StringWrapper
  delegate downcase, to: @string
  delegate upcase, capitalize, char_at, scan, to: @string

  @string : String

  def initialize(@string)
  end
end

private EQ_OPERATORS = %w(<= >= == != []= ===)

private class TestObject
  getter getter1
  getter getter2 : Int32
  getter getter3 : Int32 = 3
  getter getter4 = 4

  getter! getter5
  @getter5 : Int32?

  getter! getter6 : Int32

  getter? getter7
  getter? getter8 : Bool
  getter? getter9 : Bool = true
  getter? getter10 = true

  getter(getter11) { 11 }

  @@getter12_value = 12
  getter getter12 : Int32 { @@getter12_value }

  def self.getter12_value=(@@getter12_value)
  end

  @getter13_counter = 0
  getter(getter13) { @getter13_counter += 1; false }

  getter?(getter14 : Bool) { true }
  getter?(getter15) { true }

  setter setter1
  setter setter2 : Int32
  setter setter3 : Int32 = 3
  setter setter4 = 4

  property property1
  property property2 : Int32
  property property3 : Int32 = 3
  property property4 = 4

  property! property5
  @property5 : Int32?

  property! property6 : Int32

  property? property7
  property? property8 : Bool
  property? property9 : Bool = true
  property? property10 = true

  property(property11) { 11 }
  property property12 : Int32 { 10 + 2 }

  @property13_counter = 0
  property(property13) { @property13_counter += 1; false }

  property?(property14 : Bool) { true }
  property?(property15) { true }

  def initialize
    @getter1 = 1
    @getter2 = 2

    @getter7 = true
    @getter8 = true

    @setter1 = 1
    @setter2 = 2

    @property1 = 1
    @property2 = 2

    @property7 = true
    @property8 = true
  end

  def getter5=(@getter5)
  end

  def getter6=(@getter6)
  end

  def setter1
    @setter1
  end

  def setter2
    @setter2
  end

  def setter3
    @setter3
  end

  def setter4
    @setter4
  end

  def []=(key, value)
    {key, value}
  end

  # NOTE: these methods are a syntax error in older versions
  {% if compare_versions(Crystal::VERSION, "1.12.0-dev") >= 0 %}
    {% for op in EQ_OPERATORS %}
      def {{ op.id }}(*args, **opts)
        [args, opts]
      end

      def {{ op.id }}(*args, **opts, &)
        [args, opts, yield]
      end
    {% end %}
  {% end %}

  annotation TestAnnotation
  end

  @[TestAnnotation]
  property(x : Int32) { 1 }

  def self.test_annotation_count
    {{ @type.instance_vars.select(&.annotation(TestObject::TestAnnotation)).size }}
  end

  def self.do_set_crystal_type_id(ptr)
    set_crystal_type_id(ptr)
  end
end

private class DelegatedTestObject
  delegate :property1=, to: @test_object

  {% for op in EQ_OPERATORS %}
    delegate {{ op.id.symbolize }}, to: @test_object
  {% end %}

  def initialize(@test_object : TestObject)
  end
end

private class TestObjectWithFinalize
  include FinalizeCounter

  def_clone
end

private class HashedTestObject
  property a : Int32
  property b : Int32

  def initialize(@a, @b)
  end

  def_hash :a, :b
end

private struct NonReflexive
  def ==(other)
    false
  end
end

private class DefEquals
  def initialize
    @x = NonReflexive.new
  end

  def_equals @x
end

private struct TestMutableStruct
  getter x = 0

  def foo
    @x += 1
  end
end

describe Object do
  describe "delegate" do
    it "delegates" do
      wrapper = StringWrapper.new("HellO")
      wrapper.downcase.should eq("hello")
      wrapper.upcase.should eq("HELLO")
      wrapper.capitalize.should eq("Hello")

      wrapper.char_at(0).should eq('H')
      wrapper.char_at(index: 1).should eq('e')

      wrapper.char_at(10) { 20 }.should eq(20)

      matches = [] of String
      wrapper.scan(/l/) do |match|
        matches << match[0]
      end
      matches.should eq(["l", "l"])
    end

    it "delegates setter" do
      test_object = TestObject.new
      delegated = DelegatedTestObject.new(test_object)
      delegated.property1 = 42
      test_object.property1.should eq 42
    end

    it "delegates []=" do
      test_object = TestObject.new
      delegated = DelegatedTestObject.new(test_object)
      (delegated["foo"] = "bar").should eq({"foo", "bar"})
    end

    {% if compare_versions(Crystal::VERSION, "1.12.0-dev") >= 0 %}
      {% for op in EQ_OPERATORS %}
        it "forwards \#{{ op.id }} with multiple parameters" do
          test_object = TestObject.new
          delegated = DelegatedTestObject.new(test_object)
          delegated.{{ op.id }}(1, 2, a: 3, b: 4).should eq [{1, 2}, {a: 3, b: 4}]
        end

        it "forwards \#{{ op.id }} with multiple parameters and block parameter" do
          test_object = TestObject.new
          delegated = DelegatedTestObject.new(test_object)
          delegated.{{ op.id }}(1, 2, a: 3, b: 4) { 5 }.should eq [{1, 2}, {a: 3, b: 4}, 5]
        end
      {% end %}
    {% end %}
  end

  describe "getter" do
    it "uses simple getter" do
      obj = TestObject.new
      obj.getter1.should eq(1)
      typeof(obj.@getter1).should eq(Int32)
      typeof(obj.getter1).should eq(Int32)
    end

    it "uses getter with type declaration" do
      obj = TestObject.new
      obj.getter2.should eq(2)
      typeof(obj.@getter2).should eq(Int32)
      typeof(obj.getter2).should eq(Int32)
    end

    it "uses getter with type declaration and default value" do
      obj = TestObject.new
      obj.getter3.should eq(3)
      typeof(obj.@getter3).should eq(Int32)
      typeof(obj.getter3).should eq(Int32)
    end

    it "uses getter with assignment" do
      obj = TestObject.new
      obj.getter4.should eq(4)
      typeof(obj.@getter4).should eq(Int32)
      typeof(obj.getter4).should eq(Int32)
    end

    it "defines lazy getter with block" do
      obj = TestObject.new
      obj.getter11.should eq(11)
      obj.getter12.should eq(12)
      TestObject.getter12_value = 24
      obj.getter12.should eq(12)

      obj2 = TestObject.new
      obj2.getter12.should eq(24)
    end

    it "defines lazy getter with block returning false" do
      obj = TestObject.new
      obj.@getter13_counter.should eq(0)
      3.times { obj.getter13.should be_false }
      obj.@getter13_counter.should eq(1)
    end
  end

  describe "getter!" do
    it "uses getter!" do
      obj = TestObject.new
      expect_raises(NilAssertionError, "TestObject#getter5 cannot be nil") do
        obj.getter5
      end
      obj.getter5 = 5
      obj.getter5.should eq(5)
      typeof(obj.@getter5).should eq(Int32 | Nil)
      typeof(obj.getter5).should eq(Int32)
    end

    it "uses getter! with type declaration" do
      obj = TestObject.new
      expect_raises(NilAssertionError, "TestObject#getter6 cannot be nil") do
        obj.getter6
      end
      obj.getter6 = 6
      obj.getter6.should eq(6)
      typeof(obj.@getter6).should eq(Int32 | Nil)
      typeof(obj.getter6).should eq(Int32)
    end
  end

  describe "getter?" do
    it "uses getter?" do
      obj = TestObject.new
      obj.getter7?.should be_true
      typeof(obj.@getter7).should eq(Bool)
      typeof(obj.getter7?).should eq(Bool)
    end

    it "uses getter? with type declaration" do
      obj = TestObject.new
      obj.getter8?.should be_true
      typeof(obj.@getter8).should eq(Bool)
      typeof(obj.getter8?).should eq(Bool)
    end

    it "uses getter? with type declaration and default value" do
      obj = TestObject.new
      obj.getter9?.should be_true
      typeof(obj.@getter9).should eq(Bool)
      typeof(obj.getter9?).should eq(Bool)
    end

    it "uses getter? with default value" do
      obj = TestObject.new
      obj.getter10?.should be_true
      typeof(obj.@getter10).should eq(Bool)
      typeof(obj.getter10?).should eq(Bool)
    end

    it "uses getter? with type declaration and block" do
      obj = TestObject.new
      typeof(obj.@getter14).should eq(Bool?)
      typeof(obj.getter14?).should eq(Bool)
      obj.@getter14.should be_nil
      obj.getter14?.should be_true
      obj.@getter14.should be_true
    end

    it "uses getter? with block" do
      obj = TestObject.new
      typeof(obj.@getter15).should eq(Bool?)
      typeof(obj.getter15?).should eq(Bool)
      obj.@getter15.should be_nil
      obj.getter15?.should be_true
      obj.@getter15.should be_true
    end
  end

  describe "setter" do
    it "uses setter" do
      obj = TestObject.new
      obj.setter1.should eq(1)
      obj.setter1 = 2
      obj.setter1.should eq(2)
    end

    it "uses setter with type declaration" do
      obj = TestObject.new
      obj.setter2.should eq(2)
      obj.setter2 = 3
      obj.setter2.should eq(3)
    end

    it "uses setter with type declaration and default value" do
      obj = TestObject.new
      obj.setter3.should eq(3)
      obj.setter3 = 4
      obj.setter3.should eq(4)
    end

    it "uses setter with default value" do
      obj = TestObject.new
      obj.setter4.should eq(4)
      obj.setter4 = 5
      obj.setter4.should eq(5)
    end
  end

  describe "property" do
    it "uses property" do
      obj = TestObject.new
      obj.property1.should eq(1)
      obj.property1 = 2
      obj.property1.should eq(2)
    end

    it "uses property with type declaration" do
      obj = TestObject.new
      obj.property2.should eq(2)
      obj.property2 = 3
      obj.property2.should eq(3)
    end

    it "uses property with type declaration and default value" do
      obj = TestObject.new
      obj.property3.should eq(3)
      obj.property3 = 4
      obj.property3.should eq(4)
    end

    it "uses property with default value" do
      obj = TestObject.new
      obj.property4.should eq(4)
      obj.property4 = 5
      obj.property4.should eq(5)
    end

    it "defines lazy property with block" do
      obj = TestObject.new
      obj.property11.should eq(11)
      obj.property11 = 12
      obj.property11.should eq(12)

      obj.property12.should eq(12)
      obj.property12 = 13
      obj.property12.should eq(13)
    end

    it "defines lazy property with block returning false" do
      obj = TestObject.new
      obj.@property13_counter.should eq(0)
      3.times { obj.property13.should be_false }
      obj.@property13_counter.should eq(1)
    end
  end

  describe "property!" do
    it "uses property!" do
      obj = TestObject.new
      expect_raises(NilAssertionError, "TestObject#property5 cannot be nil") do
        obj.property5
      end
      obj.property5 = 5
      obj.property5.should eq(5)
    end

    it "uses property! with type declaration" do
      obj = TestObject.new
      expect_raises(NilAssertionError, "TestObject#property6 cannot be nil") do
        obj.property6
      end
      obj.property6 = 6
      obj.property6.should eq(6)
    end
  end

  describe "property?" do
    it "uses property?" do
      obj = TestObject.new
      obj.property7?.should be_true
      obj.property7 = false
      obj.property7?.should be_false
    end

    it "uses property? with type declaration" do
      obj = TestObject.new
      obj.property8?.should be_true
      obj.property8 = false
      obj.property8?.should be_false
    end

    it "uses property? with type declaration and default value" do
      obj = TestObject.new
      obj.property9?.should be_true
      obj.property9 = false
      obj.property9?.should be_false
    end

    it "uses property? with default value" do
      obj = TestObject.new
      obj.property10?.should be_true
      obj.property10 = false
      obj.property10?.should be_false
    end

    it "uses property? with type declaration and block" do
      obj = TestObject.new
      typeof(obj.@property14).should eq(Bool?)
      typeof(obj.property14?).should eq(Bool)
      obj.@property14.should be_nil
      obj.property14?.should be_true
      obj.@property14.should be_true
    end

    it "uses property? with block" do
      obj = TestObject.new
      typeof(obj.@property15).should eq(Bool?)
      typeof(obj.property15?).should eq(Bool)
      obj.@property15.should be_nil
      obj.property15?.should be_true
      obj.@property15.should be_true
    end
  end

  describe "#in?" do
    it "works with Enumerable-s" do
      "foo".in?(["foo", "bar"]).should be_true
      "bar".in?({"foo", "baz"}).should be_false
      42.in?(0..100).should be_true
      4242.in?(0..100).should be_false
    end

    it "works with splatted arguments" do
      "baz".in?("foo", "bar").should be_false
      1.in?(1, 10, 100).should be_true
    end

    it "works with other objects implementing #includes?" do
      "o".in?("foo").should be_true
      'o'.in?("foo").should be_true
      'x'.in?("foo").should be_false
    end
  end

  it "#unsafe_as" do
    0x12345678.unsafe_as(Tuple(UInt8, UInt8, UInt8, UInt8)).should eq({0x78, 0x56, 0x34, 0x12})
  end

  it "calls #finalize on #clone'd objects" do
    obj = TestObjectWithFinalize.new
    assert_finalizes("clone") { obj.clone }
  end

  describe "def_hash" do
    it "should return same hash for equal property values" do
      HashedTestObject.new(1, 2).hash.should eq HashedTestObject.new(1, 2).hash
    end

    it "shouldn't return same hash for different property values" do
      HashedTestObject.new(1, 2).hash.should_not eq HashedTestObject.new(3, 4).hash
    end
  end

  it "applies annotation to lazy property (#9139)" do
    TestObject.test_annotation_count.should eq(1)
  end

  describe "def_equals" do
    it "compares by reference" do
      x = DefEquals.new
      y = DefEquals.new
      (x == x).should be_true
      (x == y).should be_false
    end
  end

  describe "#not_nil!" do
    it "basic" do
      1.not_nil!
      expect_raises(NilAssertionError, "Nil assertion failed") do
        nil.not_nil!
      end
    end

    it "removes Nil type" do
      x = TestObject.new.as(TestObject?)
      typeof(x.not_nil!).should eq TestObject
      x.not_nil!.should be x
    end

    it "raises NilAssertionError" do
      x = nil.as(TestObject?)
      typeof(x.not_nil!).should eq TestObject
      expect_raises(NilAssertionError, "Nil assertion failed") do
        x.not_nil!
      end
    end

    it "with message" do
      x = TestObject.new
      x.not_nil!("custom message").should be x
      expect_raises(NilAssertionError, "custom message") do
        nil.not_nil!("custom message")
      end
    end

    it "does not copy its receiver when it is a value (#13263)" do
      x = TestMutableStruct.new
      x.not_nil!.foo.should eq(1)
      x.not_nil!.foo.should eq(2)
      x.foo.should eq(3)
    end
  end

  it ".set_crystal_type_id" do
    ary = StaticArray[Int32::MAX, Int32::MAX]
    TestObject.do_set_crystal_type_id(pointerof(ary))
    ary[0].should eq TestObject.crystal_instance_type_id
    ary[1].should eq Int32::MAX
  end
end
