
#include <iostream>
#include <memory>
#include <vector>

#include "assert.h"

#define GI_CLASS_IMPL_PRAGMA

#include "gi.hpp"

#include "test/test_boxed.h"
#include "test/test_object.h"

using namespace gi;
namespace GLib = gi::repository::GLib;
namespace GObject_ = gi::repository::GObject;

// test examples;
class GBoxedExample : public detail::GBoxedWrapper<GBoxedExample, GBExample>
{
  typedef detail::GBoxedWrapper<GBoxedExample, GBExample> super_type;

public:
  static GType get_type_() { return GI_CPP_TYPE_BOXED_EXAMPLE; }
  GBoxedExample(std::nullptr_t = nullptr) : super_type() {}
};

class CBoxedExample : public detail::CBoxedWrapper<CBoxedExample, CBExample>
{
  typedef detail::CBoxedWrapper<CBoxedExample, CBExample> super_type;

public:
  CBoxedExample(std::nullptr_t = nullptr) : super_type() {}
};

class CBoxedFinal
    : public detail::CBoxedWrapper<CBoxedFinal, CBExample, CBoxedExample>
{
  typedef detail::CBoxedWrapper<CBoxedFinal, CBExample, CBoxedExample>
      super_type;
  using super_type::super_type;
};

enum class CppEnum { VALUE_0 = ENUM_VALUE_0, VALUE_1 = ENUM_VALUE_1 };

enum class CppFlags { VALUE_0 = FLAG_VALUE_0, VALUE_1 = FLAG_VALUE_1 };

GI_FLAG_OPERATORS(CppFlags)

namespace gi
{
namespace repository
{
template<>
struct declare_cpptype_of<GBExample>
{
  typedef GBoxedExample type;
};

template<>
struct declare_cpptype_of<CBExample>
{
  typedef CBoxedExample type;
};

template<>
struct declare_ctype_of<CppEnum>
{
  typedef CEnum type;
};

template<>
struct declare_cpptype_of<CEnum>
{
  typedef CppEnum type;
};

template<>
struct declare_gtype_of<CppEnum>
{
  static GType get_type() { return GI_CPP_TYPE_ENUM; }
};

template<>
struct is_enumeration<CppEnum> : public std::true_type
{};

template<>
struct declare_ctype_of<CppFlags>
{
  typedef CFlags type;
};

template<>
struct declare_cpptype_of<CFlags>
{
  typedef CppFlags type;
};

template<>
struct declare_gtype_of<CppFlags>
{
  static GType get_type() { return GI_CPP_TYPE_FLAGS; }
};

template<>
struct is_bitfield<CppFlags> : public std::true_type
{};

} // namespace repository

} // namespace gi

#define DECLARE_PROPERTY(pname, ptype) \
  property_proxy<ptype> property_##pname() \
  { \
    return property_proxy<ptype>(*this, #pname); \
  } \
  const property_proxy<ptype> property_##pname() const \
  { \
    return property_proxy<ptype>(*this, #pname); \
  }
#define DECLARE_SIGNAL(name, sig) \
  signal_proxy<sig> signal_##name() { return signal_proxy<sig>(*this, #name); }

// simulate override construction
class Derived;

typedef GICppExample CDerived;

namespace base
{
class DerivedBase : public GObject_::Object
{
  typedef Object super_type;

public:
  typedef Derived self;
  typedef CDerived BaseObjectType;

  DerivedBase(std::nullptr_t = nullptr) : super_type() {}
  BaseObjectType *gobj_() { return (BaseObjectType *)super_type::gobj_(); }
  const BaseObjectType *gobj_() const
  {
    return (const BaseObjectType *)super_type::gobj_();
  }
  BaseObjectType *gobj_copy_() const
  {
    return (BaseObjectType *)super_type::gobj_copy_();
  }

  static GType get_type_() { return GI_CPP_TYPE_EXAMPLE; }

  DECLARE_PROPERTY(number, int)
  DECLARE_PROPERTY(fnumber, double)
  DECLARE_PROPERTY(data, std::string)
  DECLARE_PROPERTY(present, gboolean)
  DECLARE_PROPERTY(object, Object)
  DECLARE_PROPERTY(choice, CppEnum)
  DECLARE_PROPERTY(flags, CppFlags)

  DECLARE_SIGNAL(to_int, int(Derived, Object, bool, const std::string &))
  DECLARE_SIGNAL(to_string, std::string(Derived, int, gint64))
  DECLARE_SIGNAL(to_void, void(Derived, double, CppEnum, CppFlags))
};

} // namespace base

class Derived : public base::DerivedBase
{
  typedef base::DerivedBase super;
  using super::super;
};

namespace gi
{
namespace repository
{
template<>
struct declare_cpptype_of<CDerived>
{
  typedef Derived type;
};

} // namespace repository

} // namespace gi

void
test_trait()
{
  static_assert(traits::is_basic<int>::value, "");
  static_assert(traits::is_basic<float>::value, "");
  static_assert(traits::is_basic<void *>::value, "");
  static_assert(!traits::is_basic<char *>::value, "");

  static_assert(traits::is_boxed<GBoxedExample>::value, "");
  static_assert(traits::is_object<Derived>::value, "");
  static_assert(!traits::is_object<int>::value, "");

  static_assert(traits::has_ctype_member<Derived>::value, "");
  static_assert(!traits::has_ctype_member<int>::value, "");

  static_assert(
      std::is_same<traits::ctype<Derived>::type, CDerived *>::value, "");
  static_assert(
      std::is_same<traits::ctype<const Derived>::type, const CDerived *>::value,
      "");

  static_assert(
      std::is_same<traits::cpptype<CDerived *>::type, Derived>::value, "");
  static_assert(std::is_same<traits::cpptype<const CDerived *>::type,
                    const Derived>::value,
      "");

  static_assert(std::is_same<traits::ctype<CppEnum>::type, CEnum>::value, "");
  static_assert(std::is_same<traits::cpptype<CEnum>::type, CppEnum>::value, "");

  static_assert(
      std::is_same<traits::ctype<CBoxedExample>::type, CBExample *>::value, "");
  static_assert(
      std::is_same<traits::cpptype<CBExample>::type, CBoxedExample>::value, "");

  static_assert(
      std::is_same<traits::ctype<GBoxedExample>::type, GBExample *>::value, "");
  static_assert(
      std::is_same<traits::cpptype<GBExample>::type, GBoxedExample>::value, "");

  static_assert(
      std::is_same<traits::ctype<GObject_::Object>::type, _GObject *>::value,
      "");
  static_assert(
      std::is_same<traits::cpptype<_GObject>::type, GObject_::Object>::value,
      "");

  static_assert(traits::gtype<int>::value, "");
  static_assert(traits::gtype<std::string>::value, "");

  static_assert(traits::gvalue<int>::value, "");
  static_assert(traits::gvalue<CppEnum>::value, "");

  static_assert(traits::is_enumeration<CppEnum>::value, "");
  static_assert(!traits::is_bitfield<CppEnum>::value, "");
  static_assert(traits::is_bitfield<CppFlags>::value, "");

  static_assert(!traits::unset_wrapper<int>::value, "");
  static_assert(traits::unset_wrapper<GValue>::value, "");

  static_assert(
      std::is_pointer<GLib::DestroyNotify::cfunction_type>::value, "");
}

// test helper
template<typename T>
int
refcount(T *obj)
{
  return (((GObject *)(obj))->ref_count);
}

void
test_wrap()
{
  { // plain boxed
    CBExample *ex = gi_cpp_cbexample_new();
    auto cb = wrap(ex, transfer_full, direction_out);
    assert(cb.gobj_() == ex);
    auto cb2 = wrap(ex, transfer_none, direction_out);
    assert(cb2 != cb);
    assert(cb.gobj_() != cb2.gobj_());
    auto cb3 = cb2;
    assert(cb3 == cb2);
    assert(cb2.gobj_() == cb3.gobj_());

    auto cbn = CBoxedExample::new_();
    assert(cbn.gobj_() != nullptr);
    assert(cbn.gobj_()->data == 0);
    // a peek
    auto ex2 = unwrap(cbn, transfer_none, direction_in);
    assert(ex2 == cbn.gobj_());
    // a fresh copy
    auto ex3 = unwrap(cbn, transfer_full);
    assert(ex3 != cbn.gobj_());
    gi_cpp_cbexample_free(ex3);

    cbn = CBoxedExample();
    assert(cbn == nullptr);
    assert(cbn.gobj_() == nullptr);
    assert(!cbn);
    auto ex4 = unwrap(cbn, transfer_full);
    assert(ex4 == nullptr);

    // various basic operations
    CBoxedExample cbm(std::move(cbn));
    cbm = std::move(cbn);
    CBoxedExample cbm2(cbm);
    cbm2 = cbm;
    std::swap(cbm2, cbm);

    // compile check
    CBoxedFinal f = CBoxedFinal::new_();
    f = nullptr;
  }

  { // similarly so gboxed
    const GBExample *exc = gi_cpp_gbexample_new();
    const GBoxedExample w1 = wrap(exc, transfer_full, direction_in);
    // const easily lost btw
    GBoxedExample w2 = w1;
    assert(w2 == w1);
    assert(w2.gobj_() == exc);
    auto w3 = w2.copy();
    assert(w3 != w2);
    if (w2)
      w2 = nullptr;
  }

  { // string
    const gchar *str{};
    std::string s1 = wrap(str, transfer_none, direction_in);
    assert(s1.empty());
    str = "TEST";
    std::string s2 = wrap(str, transfer_none, direction_out);
    assert(s2 == str);
    auto *cs = unwrap(s2, transfer_none);
    assert(g_strcmp0(cs, str) == 0);
    // now something we should be able to free
    cs = unwrap(s2, transfer_full);
    g_free((gpointer)cs);
  }

  { // some more plain cases
    wrap(5);
    unwrap(5, transfer_none);

    const double ex = 5.0;
    auto w = wrap(ex);
    auto ex2 = unwrap(w);
    assert(ex == ex2);

    CEnum ec{};
    CppEnum ecpp{};
    ecpp = wrap(ec);
    unwrap(ecpp, transfer_none);
  }

  { // object
    CDerived *ob = gi_cpp_example_new();
    assert(refcount(ob) == 1);
    auto wo = wrap(ob, transfer_none, direction_in);
    assert(refcount(ob) == 2);
    assert(wo.gobj_() == ob);
    {
      auto wo2 = wo;
      assert(wo2 == wo);
      assert(wo2.gobj_() == ob);
      assert(refcount(ob) == 3);
      // cast to own type should work fine
      auto wo3 = object_cast<Derived>(wo2);
      assert(wo3);
      assert(refcount(ob) == 4);
    }
    assert(refcount(ob) == 2);
    auto ob2 = unwrap(wo, transfer_none, direction_in);
    assert(ob2 == ob);
    assert(refcount(ob) == 2);
    ob2 = unwrap(wo, transfer_full);
    assert(ob2 == ob);
    assert(refcount(ob) == 3);
    g_object_unref(ob2);
    ob2 = nullptr;
    assert(refcount(ob) == 2);

    // no change for full transfer
    auto wo2 = wrap(ob, transfer_full, direction_out);
    assert(wo2.gobj_() == ob);
    assert(refcount(ob) == 2);
    // compensate
    g_object_ref(ob);
    wo2 = Derived();
    assert(wo2.gobj_() == nullptr);
    assert(refcount(ob) == 2);
    wo2 = std::move(wo);
    assert(wo.gobj_() == nullptr);
    assert(wo2.gobj_() == ob);
    wo2 = wo;
    assert(refcount(ob) == 1);
    g_object_unref(ob);

    Derived wo3(wo2);
    if (wo3)
      wo3 = nullptr;

    // special floating
    ob = gi_cpp_example_new();
    wo = wrap(ob, transfer_full, direction_in);
    assert(refcount(ob) == 1);
    ob2 = unwrap(wo, transfer_none, direction_out);
    // got a floating ref
    assert(ob == ob2);
    assert(refcount(ob) == 2);
    assert(g_object_is_floating(ob2));
    // accept
    ob2 = (CDerived *)g_object_ref_sink(ob2);
    // clear
    wo = nullptr;
    ob = nullptr;
    assert(refcount(ob2) == 1);
    g_object_unref(ob2);
  }

  {
    // object creation
    Derived ob = GObject_::Object::new_<Derived>();
    ob = GObject_::Object::new_<Derived>(
        NAME_NUMBER, (int)5, NAME_PRESENT, true);
    assert(ob.property_number().get() == 5);
    assert(ob.property_present().get() == true);

    Derived ob2 = GObject_::Object::new_<Derived>();
    std::swap(ob2, ob);
  }

  { // containers
    {
      GList *l{};
      l = g_list_append(l, gi_cpp_example_new());
      auto v = detail::wrap_list<Derived>(l, transfer_full);
      // l is gone now
      l = nullptr;
      assert(v.size() == 1);
      assert(refcount(v[0].gobj_()) == 1);

      auto liw = detail::make_list_unwrap_range<GList>(v);
      auto li = unwrap(liw, transfer_container, direction_in);
      l = li.gobj_(false);
      assert(g_list_length(l) == 1);
      assert(refcount(v[0].gobj_()) == 1);
      // take over all again
      auto v2 = detail::wrap_list<Derived>(l, transfer_none);
      assert(v2.size() == 1);
      assert(v2[0] == v[0]);
      assert(refcount(v2[0].gobj_()) == 2);
      auto liw2 = detail::make_list_unwrap_range<GSList>(v);
      auto li2 = unwrap(liw2, transfer_none, direction_in);
      auto l2 = li2.gobj_(false);
      // still only refs in cpp lists
      assert(refcount(v2[0].gobj_()) == 2);
      assert(g_slist_length(l2) == 1);
      // we have not stolen ownership, so iterators clean up lists
    }

    {
      GList *l{};
      auto v1 = detail::wrap_list<std::string>(l, transfer_full);
      assert(v1.size() == 0);
      auto lw = detail::make_list_unwrap_range<GList>(v1);
      auto l2 = unwrap(lw, transfer_full, direction_in);
      assert(l2.gobj_() == nullptr);
    }

    {
      GList *l{};
      l = g_list_append(l, gi_cpp_example_new());
      auto src = detail::ListIterator<GList>(l, true, -1);
      auto lr =
          detail::make_list_wrap_range<std::vector<Derived>>(std::move(src));
      auto v = wrap(lr, transfer_full, direction_out);
      // l is gone now
      l = nullptr;
      assert(v.size() == 1);
      assert(refcount(v[0].gobj_()) == 1);
    }

    {
      GPtrArray *l = g_ptr_array_new();
      g_ptr_array_add(l, gi_cpp_example_new());
      auto v = detail::wrap_list<Derived>(l, transfer_full);
      // l is gone now
      l = nullptr;
      assert(v.size() == 1);
      assert(refcount(v[0].gobj_()) == 1);
      auto lw = detail::make_list_unwrap_range<GPtrArray>(v);
      auto l2 = unwrap(lw, transfer_none, direction_in);
      assert(l2.gobj_()->len == 1);
    }

    {
      int a[] = {3, 4}, b[2] = {0, 0};
      auto src = detail::ListIterator<int>(a, false, G_N_ELEMENTS(a));
      auto lr = detail::make_list_wrap_range(std::move(src), b);
      wrap(lr, transfer_none, direction_out);
      assert(b[0] == a[0]);
      assert(b[1] == a[1]);
    }

    {
      const char *s[]{"s1", "s2", nullptr};
      auto src = detail::ListIterator<const char *>(s, false, -1);
      auto lr = detail::make_list_wrap_range<std::vector<std::string>>(
          std::move(src));
      auto v = wrap(lr, transfer_none, direction_out);
      assert(v.size() == 2);
    }

    {
      std::vector<std::string> l1{"el1", "el2"};
      // no element transfer to allow for return value auto cleanup
      auto tl = detail::make_list_unwrap_range<GList>(l1, false);
      unwrap(tl, transfer_container, direction_in);
      auto tl2 = detail::make_list_unwrap_range<char *>(l1, false);
      unwrap(tl2, transfer_container, direction_in);

      int a[] = {2, 3};
      auto ta = detail::make_list_unwrap_range(a, G_N_ELEMENTS(a), false);
      unwrap(ta, transfer_full);
    }

    {
      std::vector<std::string> l1{"el1", "el2"};
      auto slw = detail::make_list_unwrap_range(l1.data(), l1.size(), true);
      auto sl = unwrap(slw, transfer_none, direction_in);
      const int len = g_strv_length(sl.data());
      assert(len == 2);

      const std::vector<std::string> cl1{"el1", "el2"};
      auto cslw = detail::make_list_unwrap_range(cl1.data(), cl1.size(), true);
      auto csl = unwrap(cslw, transfer_none, direction_in);
      const int clen = g_strv_length(csl.data());
      assert(clen == 2);

      std::vector<int> l2{5, 7};
      auto saw = detail::make_list_unwrap_range(l2.data(), l2.size(), false);
      auto sa = unwrap(saw, transfer_full, direction_in);
      assert(sa.data()[0] == l2[0]);
      assert(sa.data()[1] == l2[1]);
    }

    {
      int a[] = {2, 3};
      std::vector<int> wa;
      wa.resize(2);
      detail::wrap_array(a, transfer_full, wa.size(), wa.data());
      assert(wa[0] == a[0]);
      assert(wa[1] == a[1]);

      GObject *oa[] = {NULL};
      std::vector<GObject_::Object> woa;
      woa.resize(1);
      detail::wrap_array(oa, transfer_full, woa.size(), woa.data());
      assert(woa[0] == oa[0]);

      const char *sa[] = {"s"};
      std::vector<std::string> wsa;
      wsa.resize(1);
      detail::wrap_array(sa, transfer_none, wsa.size(), wsa.data());
      assert(wsa[0] == sa[0]);
    }

    { // mainly compile check
      gchar **l1 = g_strsplit("ab:cd:ef", ":", -1);
      const int len = g_strv_length(l1);
      assert(len == 3);
      detail::wrap_list<std::string>(l1, transfer_full, len);
      l1 = nullptr;
      gint l2[] = {3, 5};
      auto wl = detail::wrap_list<gint>(l2, transfer_none, 2);
      assert(wl.size() == 2);
      CDerived **l3{};
      detail::wrap_list<Derived>(l3, transfer_none, 0);
    }

    const char *k1 = "key_1", *k2 = "key_2", *e1 = "entry_1", *e2 = "entry_2";
    {
      GHashTable *m =
          g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
      g_hash_table_insert(m, g_strdup(k1), g_strdup(e1));
      g_hash_table_insert(m, g_strdup(k2), g_strdup(e2));
      auto wm = detail::wrap_map<std::string, std::string>(m, transfer_full);
      assert(wm.size() == 2);
      assert(wm[k1] == e1);
      assert(wm[k2] == e2);

      auto it = unwrap(wm, transfer_full, direction_in);
      auto t = it.gobj_(true);
      assert(g_hash_table_size(t) == 2);
      g_hash_table_unref(t);
    }

    {
      GHashTable *m1 = g_hash_table_new_full(
          g_str_hash, g_str_equal, g_free, g_object_unref);
      CDerived *ob = gi_cpp_example_new();
      g_hash_table_insert(m1, g_strdup(k1), g_object_ref(ob));
      auto wm1 = detail::wrap_map<std::string, Derived>(m1, transfer_none);
      assert(wm1.size() == 1);
      assert(refcount(wm1[k1].gobj_()) == 3);
      wm1.clear();
      assert(refcount(ob) == 2);
      g_hash_table_unref(m1);
      assert(refcount(ob) == 1);
      g_object_unref(ob);

      wm1[k1] = Derived();
      wm1[k2] = wm1[k1];
      auto it = unwrap(wm1, transfer_full, direction_in);
      auto t = it.gobj_(true);
      assert(g_hash_table_size(t) == 2);
      g_hash_table_unref(t);
    }
  }

  {
    // paramspec
    auto pspec = GObject_::ParamSpec::new_<int>("p", "p", "p", 0, 10);
    // is otherwise similar to property and is tested there further
  }
}

void
test_exception()
{
  static_assert(traits::is_gboxed<GLib::Error>::value, "");

  GQuark domain = g_quark_from_string("test-domain");
  const char *msg = "exception_test";
  const int code = 42;
  GError *err = g_error_new_literal(domain, code, msg);
  auto w = wrap(err, transfer_full, direction_out);
  assert(w.matches(domain, code));
  auto what = w.what();
  assert(strstr(what, msg) != NULL);

  GLib::Error e;
  e = w;
  assert(e.matches(domain, code));

  check_error(nullptr);
  err = g_error_new_literal(domain, code, msg);
  detail::make_unexpected(err);
  bool value = true;
  auto r = make_result<bool>(value, nullptr);
  static_assert(detail::is_result<decltype(r)>::value, "");
  static_assert(!detail::is_result<bool>::value, "");
  // make sure we have bool result
  auto s = expect(std::move(r));
  static_assert(std::is_same<bool, decltype(s)>::value, "");
  auto &e2 = expect(e);
  assert(&e2 == &e);
}

void
test_enumflag()
{
  const char *name = "EnumValue1";
  const char *nick = "v1";
  auto v = CppEnum::VALUE_1;

  auto w = value_info(v);
  auto w1 = EnumValue<CppEnum>::get_by_name(name);
  auto w2 = EnumValue<CppEnum>::get_by_nick(nick);
  assert(w == w1);
  assert(w1 == w2);
  assert(w1.value_name() == name);
  assert(w1.value_nick() == nick);

  // convenient operations
  auto f = CppFlags::VALUE_0 | CppFlags::VALUE_1;
  f = f & CppFlags::VALUE_1;
  f = ~f;
  f ^= CppFlags::VALUE_0;
}

typedef int(CCallback)(int, float);
typedef detail::callback<int(int, float), transfer_none_t, transfer_none_t>
    CppCallback;

void
test_callback()
{
  int calls = 0;

  auto l = [&](int a, float b) {
    ++calls;
    return a * b;
  };
  detail::transform_callback_wrapper<int(int, float)>::with_transfer<false,
      transfer_full_t, transfer_none_t, transfer_none_t>
      x{l};
  x.wrapper(1, 2, &x);
  assert(calls == 1);
  x.take_data(std::make_shared<int>(0));

  auto m = [&](int a, CBoxedExample /*b*/) {
    ++calls;
    return a;
  };
  detail::transform_callback_wrapper<void(int, CBoxedExample)>::with_transfer<
      false, transfer_full_t, transfer_full_t, transfer_full_t>
      y{m};
  y.wrapper(1, gi_cpp_cbexample_new(), &y);
  assert(calls == 2);

  auto w = wrap(gi_cpp_example_new(), transfer_full, direction_out);
  auto n = [&](GBoxedExample /*b*/) {
    ++calls;
    return w;
  };
  detail::transform_callback_wrapper<Derived(
      GBoxedExample)>::with_transfer<false, transfer_full_t, transfer_full_t>
      z{n};
  auto r = z.wrapper(gi_cpp_gbexample_new(), &z);
  assert(calls == 3);
  assert(r == w.gobj_());
  assert(refcount(r) == 2);
  g_object_unref(r);

  { // compilation checks
    const CppCallback cppcb(l);
    auto uw{unwrap(cppcb, gi::scope_async)};
    delete uw;
    auto uw2{unwrap(cppcb, gi::scope_notified)};
    delete uw2;
  }
}

void
test_value()
{
  using GObject_::Value;
  static_assert(traits::gtype<Value>::value, "");
  assert(g_type_is_a(GI_CPP_TYPE_ENUM, G_TYPE_ENUM));
  assert(g_type_is_a(GI_CPP_TYPE_FLAGS, G_TYPE_FLAGS));

  // detail helper
  {
    detail::Value v(5);
    auto vs = detail::transform_value<std::string>(&v);
    assert(vs == "5");
    detail::Value v2("ab");
    auto w = detail::get_value<std::string>(&v2);
    assert(w == "ab");
  }

  // main Value
  {
    Value v;
    v.init(G_TYPE_STRING);
    v.set_value("ab");
    auto w = v.get_value<std::string>();
    assert(w == "ab");
  }
  {
    Value v(0);
    auto w = v.get_value<int>();
    assert(w == 0);
    v.set_value(5);
    w = v.get_value<int>();
    assert(w == 5);
  }
  {
    Value v{std::string()};
    auto w = v.get_value<std::string>();
    assert(w.empty());
  }
  {
    Value v((double)1.0);
    auto w = v.get_value<double>();
    assert(w == 1.0);
  }
  {
    Value v('a');
    auto w = v.get_value<char>();
    assert(w == 'a');
  }
  {
    CDerived *ob = gi_cpp_example_new();
    auto wob = wrap(ob, transfer_none, direction_out);
    Value v(wob);
    assert(refcount(ob) == 3);
    auto w2 = v.get_value<Derived>();
    assert(w2 == wob);
    assert(refcount(ob) == 4);
    g_object_unref(ob);
    // others clean up by magic
  }
  {
    Value v(GBoxedExample{});
    auto w = v.get_value<GBoxedExample>();
    assert(w.gobj_() == nullptr);
  }
  {
    Value v(CppEnum::VALUE_0);
    auto w = v.get_value<CppEnum>();
    assert(w == CppEnum::VALUE_0);
    Value v1(v);
    assert(v == v1);
  }
  {
    Value v(CppFlags::VALUE_1);
    auto w = v.get_value<CppFlags>();
    assert(w == CppFlags::VALUE_1);
    Value v1(v);
    assert(v == v1);
  }
  { // test function; auto conversion
    auto tf = [](Value v, GType t) { assert(G_VALUE_TYPE(v.gobj_()) == t); };
    tf(5, G_TYPE_INT);
    tf("ab", G_TYPE_STRING);
    tf(GBoxedExample(), GI_CPP_TYPE_BOXED_EXAMPLE);
  }
  { // wrapping
    GValue *v = (GValue *)1;
    auto w = wrap(v, transfer_none, direction_in);
    auto v1 = unwrap(w, transfer_none, direction_in);
    assert(v1 = v);
  }
}

void
test_property()
{
  CDerived *ob = gi_cpp_example_new();
  // take ownership
  Derived w = wrap(ob, transfer_full, direction_out);

  // manual
  w.set_property(NAME_NUMBER, 5);
  assert(w.get_property<int>(NAME_NUMBER) == 5);
  w.set_property(NAME_PRESENT, true);
  assert(w.get_property<bool>(NAME_PRESENT) == true);
  const char *str = "value";
  w.set_property(NAME_DATA, str);
  assert(w.get_property<std::string>(NAME_DATA) == str);
  w.set_property(NAME_OBJECT, w);
  auto w2 = w.get_property<Derived>(NAME_OBJECT);
  assert(w2 == w);
  assert(refcount(ob) == 3);
  // remove cycle ref held within ob
  w.set_property(NAME_OBJECT, Derived());
  assert(refcount(ob) == 2);
  w.set_property(NAME_ENUM, CppEnum::VALUE_1);
  assert(w.get_property<CppEnum>(NAME_ENUM) == CppEnum::VALUE_1);

  // multiple props
  w.set_properties(NAME_NUMBER, 10, NAME_FNUMBER, 5.2, NAME_PRESENT, FALSE);
  assert(w.get_property<int>(NAME_NUMBER) == 10);
  assert(w.get_property<double>(NAME_FNUMBER) == 5.2);
  assert(w.get_property<bool>(NAME_PRESENT) == false);

  // generic value
#ifdef GI_GOBJECT_PROPERTY_VALUE
  w.get_property(NAME_NUMBER);
#endif

  // via proxy
  Derived w3 = wrap(gi_cpp_example_new(), transfer_full, direction_out);
  w.property_number().set(7);
  w.property_fnumber().set(6.2);
  w.property_data().set(str);
  w.property_object().set(w3);
  w.property_present().set(true);
  w.property_choice().set(CppEnum::VALUE_0);
  w.property_flags().set(CppFlags::VALUE_0);

  const Derived cw = w;
  assert(cw.property_number().get() == 7);
  assert(cw.property_fnumber().get() == 6.2);
  assert(cw.property_data().get() == str);
  assert(cw.property_object().get() == w3);
  assert(cw.property_data().get() == str);
  assert(cw.property_choice().get() == CppEnum::VALUE_0);
  assert(cw.property_flags().get() == CppFlags::VALUE_0);

  // property queries
  auto pspec = cw.find_property(NAME_NUMBER);
  assert(pspec);
  assert(pspec.get_name() == NAME_NUMBER);

  auto pspecs = cw.list_properties();
  assert(pspecs.size() == PROP_LAST);
}

void
test_signal()
{
  // example values
  double v_d = 2.7;
  int v_i = 4;
  std::string v_s = "values";
  bool v_b = true;
  CppEnum v_e = CppEnum::VALUE_1;
  CppFlags v_f = CppFlags::VALUE_1;
  Derived v_o = wrap(gi_cpp_example_new(), transfer_full, direction_out);

  // object to signal on
  CDerived *ob = gi_cpp_example_new();
  // take ownership
  Derived w = wrap(ob, transfer_full, direction_out);

  // lambda callbacks
  int recv = 0;
  int ret = 7;
  auto l1 = [&](Derived src, GObject_::Object o, bool b,
                const std::string &s) -> int {
    assert(src == w);
    assert(o == v_o);
    assert(s == v_s);
    assert(b == v_b);
    ++recv;
    return ret;
  };
  w.signal_to_int().connect(l1);
  auto r = w.signal_to_int().emit(v_o, v_b, v_s);
  assert(recv == 1);
  assert(r == ret);

  // another signal
  auto l2 = [&](Derived src, int i, gint64 ll) -> std::string {
    assert(src == w);
    ++recv;
    return std::to_string(i + ll);
  };
  w.signal_to_string().connect(l2);
  gint64 ll = 4;
  auto sr = w.signal_to_string().emit(v_i, ll);
  assert(recv == 2);
  assert(std::stoi(sr) == v_i + ll);

  // and another
  auto l3 = w.signal_to_void().slot(
      [&](Derived src, double d, CppEnum e, CppFlags f) {
        assert(src == w);
        assert(d == v_d);
        assert(e == v_e);
        assert(f == v_f);
        ++recv;
      });
  auto id = w.signal_to_void().connect(l3);
  w.signal_to_void().emit(v_d, v_e, v_f);
  assert(recv == 3);

  auto conn = make_connection(id, l3, w);
  assert(conn.connected());
  {
    // safe to disconnect twice (or attempt so)
    GObject_::SignalScopedConnection sconn(conn), sconn2(conn);
    assert(sconn.connected());
    assert(sconn2.connected());
  }

  assert(!conn.connected());
  w.signal_to_void().emit(v_d, v_e, v_f);
  assert(recv == 3);

  // assert exception helper
  auto assert_exc = [](const std::function<void()> &func) {
    bool exc = false;
    try {
      func();
    } catch (std::exception &) {
      exc = true;
    }
    assert(exc);
  };

  // check connect check
  assert_exc(
      [&]() { w.connect<std::string(Derived, int, gint64)>("to_void", l2); });
  // check property value conversion
  assert_exc([&]() { w.set_property<std::string>(NAME_OBJECT, "blah"); });
}

// ExampleInterface

class ExampleInterface : public gi::InterfaceBase
{
public:
  typedef GICppExampleItf BaseObjectType;
  static GType get_type_() G_GNUC_CONST
  {
    return gi_cpp_example_interface_get_type();
  }
};

class ExampleInterfaceDef
{
  typedef ExampleInterfaceDef self;

public:
  typedef ExampleInterface instance_type;
  typedef GICppExampleInterface interface_type;

protected:
  ~ExampleInterfaceDef() = default;

  static void interface_init(gpointer iface, gpointer /*data*/)
  {
    auto itf = (interface_type *)(iface);
    itf->vmethod = gi::detail::method_wrapper<self, int (*)(int),
        transfer_full_t, transfer_none_t>::wrapper<&self::vmethod_>;
  }

  virtual int vmethod_(int a) = 0;
};

using ExampleInterfaceImpl = gi::detail::InterfaceImpl<ExampleInterfaceDef>;

class ExampleInterfaceClassImpl
    : public gi::detail::InterfaceClassImpl<ExampleInterfaceImpl>
{
  typedef ExampleInterfaceImpl self;
  typedef gi::detail::InterfaceClassImpl<ExampleInterfaceImpl> super;

protected:
  using super::super;

  int vmethod_(int a) override
  {
    auto _struct = get_struct_();
    return _struct->vmethod(this->gobj_(), a);
  }
};

// PropertyInterface
class PropertyInterface : public gi::InterfaceBase
{
public:
  typedef GICppPropertyItf BaseObjectType;
  static GType get_type_() G_GNUC_CONST
  {
    return gi_cpp_property_interface_get_type();
  }
};

class PropertyInterfaceDef
{
  typedef PropertyInterfaceDef self;

public:
  typedef PropertyInterface instance_type;
  typedef GICppPropertyInterface interface_type;

protected:
  static void interface_init(gpointer iface, gpointer /*data*/)
  {
    auto itf = (interface_type *)(iface);
    (void)itf;
  }
};

using PropertyInterfaceImpl = gi::detail::InterfaceImpl<PropertyInterfaceDef>;

class PropertyInterfaceClassImpl
    : public gi::detail::InterfaceClassImpl<PropertyInterfaceImpl>
{
  typedef PropertyInterfaceImpl self;
  typedef gi::detail::InterfaceClassImpl<PropertyInterfaceImpl> super;

protected:
  using super::super;
};

class DerivedClassDef
{
  typedef DerivedClassDef self;

public:
  typedef Derived instance_type;
  typedef GICppExampleClass class_type;

protected:
  ~DerivedClassDef() = default;

  static void class_init(gpointer g_class, gpointer /*class_data*/)
  {
    GICppExampleClass *klass = (GICppExampleClass *)g_class;

    klass->vmethod =
        gi::detail::method_wrapper<self, int (*)(int, int), transfer_full_t,
            transfer_none_t, transfer_none_t>::wrapper<&self::vmethod_>;
  }

  virtual int vmethod_(int a, int b) = 0;
};

GI_CLASS_IMPL_BEGIN

class DerivedClass
    : public gi::detail::ClassTemplate<DerivedClassDef,
          GObject_::impl::internal::ObjectClass, ExampleInterfaceClassImpl>
{
  typedef DerivedClass self;
  typedef gi::detail::ClassTemplate<DerivedClassDef,
      GObject_::impl::internal::ObjectClass, ExampleInterfaceClassImpl>
      super;

public:
  typedef ExampleInterfaceClassImpl ExampleInterface_type;

private:
  // make local helpers private
  using super::get_struct_;
  using super::gobj_;

protected:
  using super::super;

  virtual int vmethod_(int a, int b)
  {
    auto _struct = get_struct_();
    return _struct->vmethod(gobj_(), a, b);
  }
};

GI_CLASS_IMPL_END

using DerivedImpl = gi::detail::ObjectImpl<Derived, DerivedClass>;

template<typename T>
class custom_property : public gi::property<T>
{
public:
  using super = gi::property<T>;
  using super::super;

  using handler = std::function<void(const T &)>;
  handler handler_;

  void set_property(const GValue *value) override
  {
    super::set_property(value);
    if (handler_)
      handler_(this->get_value());
  }
};

class UserDerived : public DerivedImpl
{
public:
  UserDerived()
      : DerivedImpl(typeid(UserDerived)),
        prop_int_set(
            this, "prop_int_set", "prop_int_set", "prop_int_set", 0, 10, 0),
        prop_bool_override(this, NAME_PRESENT)
  {}

  int vmethod_(int a, int b) override { return a * b; }

  int pvmethod(int a, int b) { return DerivedImpl::vmethod_(a, b); }

  int vmethod_(int a) override { return 2 * a; }

  int pivmethod(int a) { return ExampleInterface_type::vmethod_(a); }

  custom_property<int> prop_int_set;
  custom_property<bool> prop_bool_override;
};

static const int DEFAULT_PROP_INT = 7;

class UserObject : public ExampleInterfaceImpl,
                   public PropertyInterfaceImpl,
                   public GObject_::impl::ObjectImpl
{
public:
  UserObject()
      : ObjectImpl(
            typeid(UserObject), {}, {{NAME_INUMBER, {&prop_itf_int, nullptr}}}),
        signal_demo_(this, "demo"), prop_itf_int(this, NAME_INUMBER),
        prop_int(
            this, "prop_int", "prop_int", "prop_int", 0, 10, DEFAULT_PROP_INT),
        prop_bool(this, "prop_bool", "prop_bool", "prop_bool", false),
        prop_str(this, "prop_str", "prop_str", "prop_str", ""),
        prop_object(this, "prop_object", "prop_object", "prop_object"),
        prop_enum(this, "prop_enum", "prop_enum", "prop_enum")
  {}

  int vmethod_(int a) override { return 5 * a; }

  gi::signal<void(Object, int)> signal_demo_;
  gi::property<int> prop_itf_int;
  gi::property<int> prop_int;
  gi::property<bool> prop_bool;
  gi::property<std::string> prop_str;
  gi::property<Object> prop_object;
  gi::property<CppEnum> prop_enum;
};

void
test_impl()
{
  {
    // base object implements interface
    UserDerived u, v;
    assert(u.gobj_type_() == v.gobj_type_());
    assert(u.pvmethod(2, 3) == 5);
    auto klass =
        G_TYPE_INSTANCE_GET_CLASS(u.gobj_(), u.gobj_type(), GICppExampleClass);
    assert(klass->vmethod(u.gobj_(), 2, 3) == 6);
    // interface
    assert(u.pivmethod(4) == 6);
    auto iface = G_TYPE_INSTANCE_GET_INTERFACE(
        u.gobj_(), ExampleInterface::get_type_(), GICppExampleInterface);
    assert(iface->vmethod((GICppExampleItf *)u.gobj_(), 4) == 8);
    // compilation check
    assert(u.gobj_klass()->vmethod);
    { // check custom property (on a non-Object derived class)
      int value = 0;
      auto &tp = u.prop_int_set;
      tp.handler_ = [&value](int setv) { value = setv; };
      auto proxy = tp.get_proxy();
      //
      const int NEW_VALUE = 7;
      proxy.set(NEW_VALUE);
      assert(value == NEW_VALUE);
      assert(tp.get_value() == NEW_VALUE);
      tp.handler_ = nullptr;
    }
    { // likewise on overriden property
      auto &tp = u.prop_bool_override;
      auto proxy = tp.get_proxy();
      proxy.set(false);
      assert(!proxy.get());
      bool value = false;
      tp.handler_ = [&value](bool setv) { value = setv; };
      const bool NEW_VALUE = true;
      //
      proxy.set(NEW_VALUE);
      assert(value == NEW_VALUE);
      assert(tp.get_value() == NEW_VALUE);
      tp.handler_ = nullptr;
    }
  }

  {
    // vanilla object
    UserObject u, v;
    assert(u.gobj_type_() == v.gobj_type_());
    auto iface = G_TYPE_INSTANCE_GET_INTERFACE(
        u.gobj_(), ExampleInterface::get_type_(), GICppExampleInterface);
    assert(iface);
    assert(iface->vmethod((GICppExampleItf *)u.gobj_(), 4) == 20);
    // signal
    int i = 0, j = 5;
    u.signal_demo_.connect([&i](GObject_::Object, int in) -> void { i = in; });
    u.signal_demo_.emit(j);
    assert(i == j);
    // properties
    {
      // also check notification
      bool notified = false;
      auto proxy = u.prop_int.get_proxy();
      auto l = proxy.signal_notify().slot(
          [&notified](
              GObject_::Object, GObject_::ParamSpec) { notified = true; });
      GObject_::SignalScopedConnection conn =
          make_connection(proxy.signal_notify().connect(l), l, u);
      assert(u.prop_int == DEFAULT_PROP_INT);
      u.prop_int = j;
      assert(notified);
      notified = false;
      assert(proxy.get() == j);
      proxy.set(2 * j);
      assert(u.prop_int == 2 * j);
      assert(notified);
    }
    {
      auto proxy = u.prop_bool.get_proxy();
      u.prop_bool = true;
      assert(proxy.get() == true);
      proxy.set(false);
      assert(u.prop_bool == false);
    }
    {
      auto proxy = u.prop_str.get_proxy();
      const std::string strv = "value";
      u.prop_str = strv;
      assert(proxy.get() == strv);
      proxy.set(strv + strv);
      assert(u.prop_str.get_value() == (strv + strv));
    }
    {
      auto proxy = u.prop_object.get_proxy();
      u.prop_object = v;
      assert(proxy.get() == v);
      proxy.set(nullptr);
      assert(u.prop_object.get_value() == nullptr);
    }
    {
      CppEnum v1 = CppEnum::VALUE_1, v0 = CppEnum::VALUE_0;
      auto proxy = u.prop_enum.get_proxy();
      u.prop_enum = v1;
      assert(proxy.get() == v1);
      proxy.set(v0);
      assert(u.prop_enum == v0);
    }
    {
      const int val = 8;
      u.prop_itf_int = val;
      auto proxy = u.prop_itf_int.get_proxy();
      assert(proxy.get() == val);
      proxy.set(val);
      assert(u.prop_itf_int == val);
    }

    {
      // local properties
      property_read<bool> p{&u, "p", "p", "p", false};
      p.get_proxy();

      property_write<bool> q{&u, "p", "p", "p", false};
      q.get_proxy();
    }
  }

  {
    // create non-stack
    auto u = gi::make_ref<UserObject>();
    assert(u->list_properties().size() > 0);
    // cast works ok
    GObject_::Object v = u;
    assert(refcount(v.gobj_()) == 2);
    auto l = [](GObject_::Object) {};
    l(u);
    auto u2 = ref_ptr_cast<UserObject>(v);
    assert(u2 == u);
  }
}

int
main(int argc, char *argv[])
{
  (void)argc;
  (void)argv;

  test_trait();
  test_wrap();
  test_exception();
  test_enumflag();
  test_value();
  test_property();
  test_signal();
  test_callback();
  test_impl();

  return 0;
}
