# frozen_string_literal: true

require "gir_ffi_test_helper"

GirFFI.setup :Regress
GirFFI.setup :GIMarshallingTests

describe GirFFI::Builder do
  let(:gir) { GObjectIntrospection::IRepository.default }

  describe ".build_class" do
    it "does not replace existing classes" do
      oldclass = GObject::Object
      GirFFI::Builder.build_class get_introspection_data("GObject", "Object")
      _(GObject::Object).must_equal oldclass
    end
  end

  describe ".build_module" do
    it "refuses to build existing modules defined elsewhere" do
      result = _(-> { GirFFI::Builder.build_module("Array") }).must_raise RuntimeError
      _(result.message).must_equal "The module Array was already defined elsewhere"
    end

    describe "building a module for the first time" do
      before do
        save_module :Regress
        GirFFI::Builder.build_module "Regress"
      end

      it "creates a Lib module ready to attach functions from the shared library" do
        gir = GObjectIntrospection::IRepository.default
        expected = [gir.shared_library("Regress")]
        assert_equal expected, Regress::Lib.ffi_libraries.map(&:name)
      end

      after do
        restore_module :Regress
      end
    end

    describe "building a module that already exists" do
      it "does not replace the existing module" do
        oldmodule = Regress
        GirFFI::Builder.build_module "Regress"
        assert_equal oldmodule, Regress
      end

      it "does not replace the existing Lib module" do
        oldmodule = Regress::Lib
        GirFFI::Builder.build_module "Regress"
        assert_equal oldmodule, Regress::Lib
      end
    end

    it "passes the version on to ModuleBuilder" do
      builder = double(generate: nil)
      expect(GirFFI::Builders::ModuleBuilder).to receive(:new)
        .with("Foo", namespace: "Foo", version: "1.0")
        .and_return builder

      GirFFI::Builder.build_module "Foo", "1.0"
    end
  end

  describe ".build_by_gtype" do
    it "returns the class types known to the GIR" do
      result = GirFFI::Builder.build_by_gtype GObject::Object.gtype
      _(result).must_equal GObject::Object
    end

    it "returns the class for user-defined types" do
      klass = Class.new GIMarshallingTests::OverridesObject
      Object.const_set "Derived#{Sequence.next}", klass
      gtype = GirFFI.define_type klass

      found_klass = GirFFI::Builder.build_by_gtype gtype
      _(found_klass).must_equal klass
    end

    it "returns the class for user-defined types not derived from GObject" do
      klass = Class.new Regress::TestFundamentalObject
      Object.const_set "Derived#{Sequence.next}", klass
      gtype = GirFFI.define_type klass

      found_klass = GirFFI::Builder.build_by_gtype gtype
      _(found_klass).must_equal klass
    end

    it "returns a valid class for boxed classes unknown to GIR" do
      object_class = GIMarshallingTests::PropertiesObject.object_class
      property = object_class.find_property "some-boxed-glist"
      gtype = property.value_type

      _(gtype).wont_equal GObject::TYPE_NONE

      found_klass = GirFFI::Builder.build_by_gtype gtype
      _(found_klass.name).must_be_nil
      _(found_klass.superclass).must_equal GirFFI::BoxedBase
    end

    it "refuse to build classes for base types" do
      _(-> { GirFFI::Builder.build_by_gtype GObject::TYPE_INT })
        .must_raise RuntimeError, "Unable to handle type gint"
    end
  end

  describe ".attach_ffi_function" do
    let(:lib) { Module.new }

    it "calls attach_function with the correct types for Regress.test_callback_destroy_notify" do
      function_info = get_introspection_data "Regress", "test_callback_destroy_notify"

      expect(lib)
        .to receive(:attach_function)
        .with("regress_test_callback_destroy_notify",
              [Regress::TestCallbackUserData, :pointer, GLib::DestroyNotify],
              :int32)
        .and_return true

      GirFFI::Builder.attach_ffi_function(lib, function_info)
    end

    it "calls attach_function with the correct types for Regress::TestObj#torture_signature_0" do
      info = get_method_introspection_data "Regress", "TestObj", "torture_signature_0"

      expect(lib)
        .to receive(:attach_function)
        .with("regress_test_obj_torture_signature_0",
              [:pointer, :int32, :pointer, :pointer, :pointer, :pointer, :uint32],
              :void)
        .and_return true

      GirFFI::Builder.attach_ffi_function(lib, info)
    end

    it "calls attach_function with the correct types for Regress::TestObj#instance_method" do
      info = get_method_introspection_data "Regress", "TestObj", "instance_method"
      expect(lib).to receive(:attach_function)
        .with("regress_test_obj_instance_method", [:pointer], :int32)
        .and_return true
      GirFFI::Builder.attach_ffi_function(lib, info)
    end

    it "calls attach_function with the correct types for Regress.test_array_gint32_in" do
      info = get_introspection_data "Regress", "test_array_gint32_in"
      expect(lib).to receive(:attach_function)
        .with("regress_test_array_gint32_in", [:int32, :pointer], :int32)
        .and_return true
      GirFFI::Builder.attach_ffi_function(lib, info)
    end

    it "calls attach_function with the correct types for Regress.test_enum_param" do
      info = get_introspection_data "Regress", "test_enum_param"
      expect(lib).to receive(:attach_function)
        .with("regress_test_enum_param", [Regress::TestEnum], :pointer)
        .and_return true
      GirFFI::Builder.attach_ffi_function(lib, info)
    end

    it "does not attach the function if it is already defined" do
      info = get_introspection_data "Regress", "test_array_gint32_in"
      allow(lib).to receive(:method_defined?).and_return true
      expect(lib).not_to receive(:attach_function)
      GirFFI::Builder.attach_ffi_function(lib, info)
    end
  end

  #
  # NOTE: Legacy tests below.
  #

  describe "looking at Regress.test_callback_destroy_notify" do
    before do
      save_module :GObject
      save_module :Regress
      GirFFI::Builder.build_module "GObject"
      GirFFI::Builder.build_module "Regress"
      @go = get_introspection_data "Regress", "test_callback_destroy_notify"
    end

    it "defines ffi callback types :Callback and :ClosureNotify" do
      Regress.setup_method! "test_callback_destroy_notify"
      tcud = Regress::Lib.find_type :TestCallbackUserData
      dn = GLib::Lib.find_type :DestroyNotify

      assert_equal FFI.find_type(:int32), tcud.result_type
      assert_equal FFI.find_type(:void), dn.result_type
      assert_equal [FFI.find_type(:pointer)], tcud.param_types
      assert_equal [FFI.find_type(:pointer)], dn.param_types
    end

    after do
      restore_module :Regress
      restore_module :GObject
    end
  end

  describe "building Regress::TestBoxed" do
    before do
      GirFFI::Builder.build_class get_introspection_data("Regress", "TestBoxed")
    end

    it "sets up #wrap" do
      assert Regress::TestBoxed.respond_to? "wrap"
    end

    it "sets up #allocate" do
      assert Regress::TestBoxed.respond_to? "allocate"
    end
  end

  describe "built Regress module" do
    before do
      save_module :Regress
      GirFFI::Builder.build_module "Regress"
    end

    it "autocreates singleton methods" do
      refute_defines_singleton_method Regress, :test_uint
      Regress.test_uint 31
      assert_defines_singleton_method Regress, :test_uint
    end

    it "autocreates the TestObj class on first access" do
      assert !Regress.const_defined?(:TestObj)
      _(Regress::TestObj).must_be_instance_of Class
      assert Regress.const_defined? :TestObj
    end

    it "knows its own module builder" do
      _(Regress.gir_ffi_builder).must_be_instance_of GirFFI::Builders::ModuleBuilder
    end

    after do
      restore_module :Regress
    end
  end

  describe "having built Regress::TestObj" do
    before do
      GirFFI::Builder.build_class get_introspection_data("Regress", "TestObj")
    end

    it "C functions for called instance methods get attached to Regress::Lib" do
      o = Regress::TestObj.new_from_file("foo")
      o.instance_method
      _(Regress::Lib).must_respond_to :regress_test_obj_instance_method
    end

    it "the built class knows its own GIR info" do
      _(Regress::TestObj.gir_info.name).must_equal "TestObj"
    end

    it "the built class knows its own class builder" do
      _(Regress::TestObj.gir_ffi_builder).must_be_instance_of GirFFI::Builders::ObjectBuilder
    end
  end

  describe "built Regress::TestSubObj" do
    it "inherits #set_bare from its superclass" do
      o1 = Regress::TestSubObj.new
      o1.set_bare(nil)
      pass
    end

    it "overrides #instance_method" do
      obj = Regress::TestObj.new_from_file("foo")
      subobj = Regress::TestSubObj.new

      _(obj.instance_method).must_equal(-1)
      _(subobj.instance_method).must_equal 0
    end
  end

  describe "building Regress::TestSubObj" do
    before do
      save_module :Regress
      GirFFI::Builder.build_class get_introspection_data("Regress", "TestSubObj")
    end

    it "sets up the Regress namespace" do
      assert Regress.const_defined? :Lib
      assert Regress.respond_to? :gir_ffi_builder
      assert Regress.const_defined? :GIR_FFI_BUILDER
    end

    it "creates the Regress::Lib module ready to attach functions from the shared library" do
      expected = [gir.shared_library("Regress")]
      assert_equal expected, Regress::Lib.ffi_libraries.map(&:name)
    end

    it "builds parent classes also" do
      assert Regress.const_defined? :TestObj
      assert Object.const_defined? :GObject
      assert GObject.const_defined? :Object
    end

    it "sets up the inheritance chain" do
      # Introduced in version 1.59.1
      expected = if Regress::TestSubObj.find_property :boolean
                   [Regress::TestSubObj,
                    Regress::TestInterface,
                    Regress::TestObj,
                    GObject::Object]
                 else
                   [Regress::TestSubObj,
                    Regress::TestObj,
                    GObject::Object]
                 end
      _(Regress::TestSubObj.registered_ancestors).must_equal expected
    end

    it "creates a Regress::TestSubObj#to_ptr method" do
      assert Regress::TestSubObj.public_method_defined? :to_ptr
    end

    after do
      restore_module :Regress
    end
  end
end
