# frozen_string_literal: true

require "gir_ffi_test_helper"

GirFFI.setup :GIMarshallingTests
GirFFI.setup :Regress

describe GirFFI::Builders::UserDefinedBuilder do
  let(:base_class) { GIMarshallingTests::Object }
  let(:derived_class) do
    Object.const_set("DerivedClass#{Sequence.next}", Class.new(base_class))
  end
  let(:builder) { GirFFI::Builders::UserDefinedBuilder.new info }
  let(:info) { GirFFI::UserDefinedObjectInfo.new derived_class }

  describe "#build_class" do
    before do
      builder.build_class
    end

    describe "with type info containing one integer property" do
      let(:info) do
        GirFFI::UserDefinedObjectInfo.new derived_class do |it|
          it.install_property GObject.param_spec_int("foo-bar", "foo bar",
                                                     "The Foo Bar Property",
                                                     10, 20, 15,
                                                     readable: true, writable: true)
        end
      end

      it "registers a type that is bigger than the parent" do
        gtype = derived_class.gtype
        q = GObject.type_query gtype
        _(q.instance_size).must_be :>, GIMarshallingTests::Object::Struct.size
      end

      it "gives the type's Struct fields for the parent and the property" do
        _(derived_class::Struct.members).must_equal [:parent, :foo_bar]
      end

      it "creates accessor functions for the property" do
        obj = derived_class.new
        obj.foo_bar = 13
        _(obj.foo_bar).must_equal 13
      end

      it "makes the property retrievable using #get_property" do
        obj = derived_class.new
        obj.foo_bar = 13
        _(obj.get_property("foo-bar")).must_equal 13
      end

      it "makes the property settable using #set_property" do
        obj = derived_class.new
        obj.set_property("foo-bar", 20)
        _(obj.foo_bar).must_equal 20
      end

      it "keeps parent properties accessible through their accessors" do
        obj = derived_class.new
        obj.int = 24
        _(obj.int).must_equal 24
      end

      it "keeps parent properties accessible through get_property and set_property" do
        obj = derived_class.new
        obj.set_property("int", 24)
        _(obj.get_property("int")).must_equal 24
      end
    end

    describe "with type info containing properties of several different types" do
      let(:info) do
        GirFFI::UserDefinedObjectInfo.new derived_class do |it|
          it.install_property GObject.param_spec_string("string-prop", "string property",
                                                        "The String Property",
                                                        "this is the default value",
                                                        readable: true, writable: true)
          it.install_property GObject.param_spec_int("int-prop", "integer property",
                                                     "The Integer Property",
                                                     10, 20, 15,
                                                     readable: true, writable: true)
          it.install_property GObject.param_spec_long("long-prop", "long property",
                                                      "The Long Property",
                                                      10.0, 50.0, 42.0,
                                                      readable: true, writable: true)
        end
      end

      it "registers a type of the proper size" do
        expected_size = derived_class::Struct.size
        gtype = derived_class.gtype
        q = GObject.type_query gtype
        _(q.instance_size).must_equal expected_size
      end

      it "gives the type's Struct fields for the parent and the properties with the correct offsets" do
        offsets = derived_class::Struct.offsets
        alignment = derived_class::Struct.alignment
        _(alignment).must_equal 8 # TODO: Fix tests for platforms where this fails.
        _(offsets).must_equal [[:parent, 0], [:string_prop, 32], [:int_prop, 40], [:long_prop, 48]]
      end

      it "creates accessor functions for the string property" do
        obj = derived_class.new
        obj.string_prop = "hello!"
        _(obj.string_prop).must_equal "hello!"
      end

      it "creates accessor functions for the integer property" do
        obj = derived_class.new
        obj.int_prop = 13
        _(obj.int_prop).must_equal 13
      end
    end

    describe "with a boxed property" do
      let(:boxed_gtype) { GIMarshallingTests::BoxedStruct.gtype }
      let(:info) do
        GirFFI::UserDefinedObjectInfo.new derived_class do |it|
          it.install_property GObject.param_spec_boxed("boxed-prop", "boxed property",
                                                       "The Boxed Property",
                                                       boxed_gtype,
                                                       readable: true, writable: true)
        end
      end

      it "registers a type of the proper size" do
        expected_size = derived_class::Struct.size
        gtype = derived_class.gtype
        q = GObject.type_query gtype
        _(q.instance_size).must_equal expected_size
      end

      it "gives the type's Struct fields for the parent and the property" do
        _(derived_class::Struct.members).must_equal [:parent, :boxed_prop]
      end

      it "creates accessor functions for the property" do
        obj = derived_class.new
        boxed = GIMarshallingTests::BoxedStruct.new
        boxed.long_ = 423
        obj.boxed_prop = boxed
        _(obj.boxed_prop.long_).must_equal 423
      end
    end

    describe "with an object property" do
      let(:object_gtype) { GIMarshallingTests::Object.gtype }
      let(:info) do
        GirFFI::UserDefinedObjectInfo.new derived_class do |it|
          it.install_property GObject.param_spec_object("object-prop", "object property",
                                                        "The Object Property",
                                                        object_gtype,
                                                        readable: true, writable: true)
        end
      end

      it "registers a type of the proper size" do
        expected_size = derived_class::Struct.size
        gtype = derived_class.gtype
        q = GObject.type_query gtype
        _(q.instance_size).must_equal expected_size
      end

      it "gives the type's Struct fields for the parent and the property" do
        _(derived_class::Struct.members).must_equal [:parent, :object_prop]
      end

      it "creates accessor functions for the property" do
        obj = derived_class.new
        object = GIMarshallingTests::Object.new 42
        object.int = 423
        obj.object_prop = object
        _(obj.object_prop.int).must_equal 423
      end

      it "allows clearing the property throught the setter method" do
        obj = derived_class.new
        obj.object_prop = nil
        _(obj.object_prop).must_be_nil
      end

      it "handles reference counting correctly when using the accessor" do
        obj = derived_class.new
        object = GIMarshallingTests::Object.new 42
        obj.object_prop = object
        _(object_ref_count(object)).must_equal 2
      end

      it "handles reference counting correctly when using the set_property method" do
        obj = derived_class.new
        object = GIMarshallingTests::Object.new 42

        _(object_ref_count(object)).must_equal 1
        obj.set_property("object_prop", object)
        _(object_ref_count(object)).must_equal 4 # Due to extra Value#set_value + Value#get_value
      end
    end

    describe "with a boolean property" do
      let(:object_gtype) { GIMarshallingTests::Object.gtype }
      let(:info) do
        GirFFI::UserDefinedObjectInfo.new derived_class do |it|
          it.install_property GObject.param_spec_boolean("the-prop", "the property",
                                                         "The Property",
                                                         true,
                                                         readable: true, writable: true)
        end
      end

      it "creates accessor functions for the property" do
        obj = derived_class.new
        obj.the_prop = true
        _(obj.the_prop).must_equal true
      end
    end

    describe "with a char property" do
      let(:object_gtype) { GIMarshallingTests::Object.gtype }
      let(:info) do
        GirFFI::UserDefinedObjectInfo.new derived_class do |it|
          it.install_property GObject.param_spec_char("the-prop", "the property",
                                                      "The Property",
                                                      -20, 100, 15,
                                                      readable: true, writable: true)
        end
      end

      it "creates accessor functions for the property" do
        obj = derived_class.new
        obj.the_prop = -13
        _(obj.the_prop).must_equal(-13)
      end
    end

    describe "with a uchar property" do
      let(:object_gtype) { GIMarshallingTests::Object.gtype }
      let(:info) do
        GirFFI::UserDefinedObjectInfo.new derived_class do |it|
          it.install_property GObject.param_spec_uchar("the-prop", "the property",
                                                       "The Property",
                                                       10, 100, 15,
                                                       readable: true, writable: true)
        end
      end

      it "creates accessor functions for the property" do
        obj = derived_class.new
        obj.the_prop = 13
        _(obj.the_prop).must_equal 13
      end
    end

    describe "with a uint property" do
      let(:object_gtype) { GIMarshallingTests::Object.gtype }
      let(:info) do
        GirFFI::UserDefinedObjectInfo.new derived_class do |it|
          it.install_property GObject.param_spec_uint("the-prop", "the property",
                                                      "The Property",
                                                      10, 100, 15,
                                                      readable: true, writable: true)
        end
      end

      it "creates accessor functions for the property" do
        obj = derived_class.new
        obj.the_prop = 423
        _(obj.the_prop).must_equal 423
      end
    end

    describe "with a ulong property" do
      let(:object_gtype) { GIMarshallingTests::Object.gtype }
      let(:info) do
        GirFFI::UserDefinedObjectInfo.new derived_class do |it|
          it.install_property GObject.param_spec_ulong("the-prop", "the property",
                                                       "The Property",
                                                       10, 100, 15,
                                                       readable: true, writable: true)
        end
      end

      it "creates accessor functions for the property" do
        obj = derived_class.new
        obj.the_prop = 423_432
        _(obj.the_prop).must_equal 423_432
      end
    end

    describe "with a int64 property" do
      let(:object_gtype) { GIMarshallingTests::Object.gtype }
      let(:info) do
        GirFFI::UserDefinedObjectInfo.new derived_class do |it|
          it.install_property GObject.param_spec_int64("the-prop", "the property",
                                                       "The Property",
                                                       10, 100, 15,
                                                       readable: true, writable: true)
        end
      end

      it "creates accessor functions for the property" do
        obj = derived_class.new
        obj.the_prop = -423_432
        _(obj.the_prop).must_equal(-423_432)
      end
    end

    describe "with a uint64 property" do
      let(:object_gtype) { GIMarshallingTests::Object.gtype }
      let(:info) do
        GirFFI::UserDefinedObjectInfo.new derived_class do |it|
          it.install_property GObject.param_spec_uint64("the-prop", "the property",
                                                        "The Property",
                                                        10, 100, 15,
                                                        readable: true, writable: true)
        end
      end

      it "creates accessor functions for the property" do
        obj = derived_class.new
        obj.the_prop = 423_432
        _(obj.the_prop).must_equal 423_432
      end
    end

    describe "with a float property" do
      let(:object_gtype) { GIMarshallingTests::Object.gtype }
      let(:info) do
        GirFFI::UserDefinedObjectInfo.new derived_class do |it|
          it.install_property GObject.param_spec_float("the-prop", "the property",
                                                       "The Property",
                                                       10.0, 100.0, 15.0,
                                                       readable: true, writable: true)
        end
      end

      it "creates accessor functions for the property" do
        obj = derived_class.new
        obj.the_prop = 42.23
        _(obj.the_prop).must_be_within_epsilon 42.23
      end
    end

    describe "with a double property" do
      let(:object_gtype) { GIMarshallingTests::Object.gtype }
      let(:info) do
        GirFFI::UserDefinedObjectInfo.new derived_class do |it|
          it.install_property GObject.param_spec_double("the-prop", "the property",
                                                        "The Property",
                                                        10.0, 100.0, 15.0,
                                                        readable: true, writable: true)
        end
      end

      it "creates accessor functions for the property" do
        obj = derived_class.new
        obj.the_prop = 42.23
        _(obj.the_prop).must_equal 42.23
      end
    end

    describe "with an enum property" do
      let(:object_gtype) { GIMarshallingTests::Object.gtype }
      let(:info) do
        GirFFI::UserDefinedObjectInfo.new derived_class do |it|
          prop = GObject.param_spec_enum("the-prop", "the property",
                                         "The Property",
                                         GIMarshallingTests::GEnum.gtype, 0,
                                         readable: true, writable: true)
          it.install_property prop
        end
      end

      it "creates accessor functions for the property" do
        obj = derived_class.new
        obj.the_prop = :value2
        _(obj.the_prop).must_equal :value2
      end
    end

    describe "with a flags property" do
      let(:object_gtype) { GIMarshallingTests::Object.gtype }
      let(:info) do
        GirFFI::UserDefinedObjectInfo.new derived_class do |it|
          prop = GObject.param_spec_flags("the-prop", "the property",
                                          "The Property",
                                          GIMarshallingTests::Flags.gtype, 0,
                                          readable: true, writable: true)
          it.install_property prop
        end
      end

      it "creates accessor functions for the property" do
        obj = derived_class.new
        obj.the_prop = { value2: true }
        _(obj.the_prop).must_equal value2: true
      end
    end

    describe "when deriving from a class with hidden struct size" do
      let(:parent_class) { Regress::TestInheritDrawable }
      let(:parent_size) do
        GObject.type_query(parent_class.gtype).instance_size
      end
      let(:derived_class) do
        Object.const_set("DerivedClass#{Sequence.next}", Class.new(parent_class))
      end
      let(:info) do
        GirFFI::UserDefinedObjectInfo.new derived_class do |it|
          it.install_property GObject.param_spec_int("int-prop", "integer property",
                                                     "Integer Property",
                                                     10, 20, 15,
                                                     readable: true, writable: true)
        end
      end

      it "registers a type that is bigger than the parent" do
        class_size = GObject.type_query(derived_class.gtype).instance_size
        _(class_size).must_be :>, parent_size
      end

      it "leaves enough space in derived struct layout" do
        struct_size = derived_class::Struct.size
        _(struct_size).must_be :>, parent_size
      end
    end

    describe "with type info containing an overridden g_name" do
      let(:info) do
        GirFFI::UserDefinedObjectInfo.new derived_class do |it|
          it.g_name = "OtherName#{Sequence.next}"
        end
      end

      it "registers a type under the overridden name" do
        registered_name = GObject.type_name(derived_class.gtype)
        _(registered_name).must_equal info.g_name
        _(registered_name).wont_equal derived_class.name
      end
    end

    describe "with type info containing a vfunc" do
      let(:info) do
        GirFFI::UserDefinedObjectInfo.new derived_class do |it|
          it.install_vfunc_implementation :method_int8_in, proc { |instance, in_|
            instance.int = in_
          }
        end
      end

      it "allows the vfunc to be called through its invoker" do
        obj = derived_class.new
        obj.method_int8_in 12
        _(obj.int).must_equal 12
      end
    end

    describe "when using a default vfunc implementation" do
      let(:base_class) { Regress::TestObj }
      let(:info) do
        GirFFI::UserDefinedObjectInfo.new derived_class do |it|
          it.install_vfunc_implementation :matrix
        end
      end

      before do
        derived_class.class_eval do
          def matrix(_arg)
            44
          end
        end
      end

      it "allows the vfunc to be called through its invoker" do
        obj = derived_class.new
        _(obj.do_matrix("bar")).must_equal 44
      end
    end

    describe "with type info containing a vfunc from an included Interface" do
      let(:info) do
        derived_class.class_eval { include GIMarshallingTests::Interface }
        GirFFI::UserDefinedObjectInfo.new derived_class do |it|
          it.install_vfunc_implementation :test_int8_in,
                                          proc { |instance, in_| instance.int = in_ }
        end
      end

      it "marks the type as conforming to the included Interface" do
        iface_gtype = GIMarshallingTests::Interface.gtype
        _(GObject.type_interfaces(derived_class.gtype).to_a).must_equal [iface_gtype]
      end

      it "allows the vfunc to be called through its invoker" do
        obj = derived_class.new
        obj.test_int8_in 12
        _(obj.int).must_equal 12
      end
    end

    it "keeps the gtype for an already registered class" do
      gtype = derived_class.gtype
      other_builder = GirFFI::Builders::UserDefinedBuilder.new info
      other_class = other_builder.build_class
      _(other_class.gtype).must_equal gtype
    end

    it "creates a class with a new GType" do
      _(derived_class.gtype).wont_equal GIMarshallingTests::Object.gtype
    end

    it "makes the registered class return objects with the correct GType" do
      obj = derived_class.new
      _(GObject.type_from_instance(obj)).must_equal derived_class.gtype
    end
  end

  describe "#object_class_struct" do
    it "returns the parent class struct" do
      _(builder.object_class_struct).must_equal GIMarshallingTests::ObjectClass
    end
  end
end
