// https://github.com/vinniefalco/LuaBridge
// Copyright 2020, Dmitry Tarakanov
// SPDX-License-Identifier: MIT

#include "TestBase.h"

#include <exception>
#include <functional>
#include <map>
#include <memory>

struct ClassTests : TestBase
{
    template<class T>
    T variable(const std::string& name)
    {
        runLua("result = " + name);
        return result<T>();
    }
};

namespace {

struct EmptyBase
{
};

template<class T, class Base>
struct Class : Base
{
    Class() : data() {}

    Class(T data) : data(data) {}

    static Class<T, Base> staticFunction(Class<T, Base> value) { return value; }

    std::string toString() const
    {
        std::ostringstream stream;
        stream << data;
        return stream.str();
    }

    bool operator==(const Class<T, Base>& rhs) const { return data == rhs.data; }

    bool operator<(const Class<T, Base>& rhs) const { return data < rhs.data; }

    bool operator<=(const Class<T, Base>& rhs) const { return data <= rhs.data; }

    Class<T, Base> operator+(const Class<T, Base>& rhs) const
    {
        return Class<T, Base>(data + rhs.data);
    }

    Class<T, Base> operator-(const Class<T, Base>& rhs) const
    {
        return Class<T, Base>(data - rhs.data);
    }

    Class<T, Base> operator*(const Class<T, Base>& rhs) const
    {
        return Class<T, Base>(data * rhs.data);
    }

    Class<T, Base> operator/(const Class<T, Base>& rhs) const
    {
        return Class<T, Base>(data / rhs.data);
    }

    Class<T, Base> operator%(const Class<T, Base>& rhs) const
    {
        return Class<T, Base>(data % rhs.data);
    }

    Class<T, Base> operator()(T param) { return Class<T, Base>(param); }

    int len() const { return data; }

    Class<T, Base> negate() const { return Class<T, Base>(-data); }

    T method(T value) { return value; }

    T methodState(T value, lua_State*) { return value; }

    T constMethod(T value) const { return value; }

    T getData() const { return data; }

    void setData(T data) { this->data = data; }

    T getDataState(lua_State*) const { return data; }

    void setDataState(T data, lua_State*) { this->data = data; }

    mutable T data;
    static T staticData;
};

template<class T, class Base>
T Class<T, Base>::staticData = {};

} // namespace

TEST_F(ClassTests, IsInstance)
{
    using BaseClass = Class<int, EmptyBase>;
    using OtherClass = Class<float, EmptyBase>;
    using DerivedClass = Class<float, BaseClass>;

    luabridge::getGlobalNamespace(L)
        .beginClass<BaseClass>("BaseClass")
        .endClass()
        .deriveClass<DerivedClass, BaseClass>("DerivedClass")
        .endClass()
        .beginClass<OtherClass>("OtherClass")
        .endClass();

    BaseClass base;
    luabridge::push(L, base);

    DerivedClass derived;
    luabridge::push(L, derived);

    OtherClass other;
    luabridge::push(L, other);

    ASSERT_TRUE(luabridge::isInstance<BaseClass>(L, -3));
    ASSERT_FALSE(luabridge::isInstance<DerivedClass>(L, -3)); // BaseClass is not DerivedClass
    ASSERT_FALSE(luabridge::isInstance<OtherClass>(L, -3));

    ASSERT_TRUE(luabridge::isInstance<BaseClass>(L, -2));
    ASSERT_TRUE(luabridge::isInstance<DerivedClass>(L, -2)); // DerivedClass is BaseClass
    ASSERT_FALSE(luabridge::isInstance<OtherClass>(L, -2));

    ASSERT_FALSE(luabridge::isInstance<BaseClass>(L, -1));
    ASSERT_FALSE(luabridge::isInstance<DerivedClass>(L, -1));
    ASSERT_TRUE(luabridge::isInstance<OtherClass>(L, -1));
}

TEST_F(ClassTests, PassingUnregisteredClassToLuaThrows)
{
    using Unregistered = Class<int, EmptyBase>;

    runLua("function process_fn (value) end");

    auto process_fn = luabridge::getGlobal(L, "process_fn");
    ASSERT_TRUE(process_fn.isFunction());

    Unregistered value(1);
    const Unregistered constValue(2);
    ASSERT_THROW(process_fn(value), std::exception);
    ASSERT_THROW(process_fn(constValue), std::exception);
    ASSERT_THROW(process_fn(&value), std::exception);
    ASSERT_THROW(process_fn(&constValue), std::exception);
}

TEST_F(ClassTests, PassWrongClassFromLuaThrows)
{
    using Right = Class<int, EmptyBase>;
    using WrongBase = Class<float, EmptyBase>;
    using Wrong = Class<int, WrongBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Right>("Right")
        .endClass()
        .beginClass<WrongBase>("WrongBase")
        .endClass()
        .beginClass<Wrong>("Wrong")
        .addConstructor<void (*)(int)>()
        .endClass()
        .addFunction("processRight", &Right::staticFunction);

    // bad argument #1 to 'processRight' (Right expected, got Wrong)
    ASSERT_THROW(runLua("result = processRight (Wrong (5))"), std::exception);
    ASSERT_TRUE(result().isNil());
}

TEST_F(ClassTests, PassDerivedClassInsteadOfBase)
{
    using Base = Class<int, EmptyBase>;
    using Derived = Class<float, Base>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Base>("Base")
        .endClass()
        .deriveClass<Derived, Base>("Derived")
        .addConstructor<void (*)(float)>()
        .endClass()
        .addFunction("processBase", &Base::staticFunction);

    runLua("result = processBase (Derived (3.14))");
    ASSERT_EQ(0, result<Base>().data);
}

namespace {

template<class T, class Base>
T processNonConst(Class<T, Base>* object)
{
    return object->data;
}

} // namespace

TEST_F(ClassTests, PassConstClassInsteadOfNonConstThrows)
{
    using Base = Class<int, EmptyBase>;
    using Derived = Class<float, Base>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Base>("Base")
        .endClass()
        .deriveClass<Derived, Base>("Derived")
        .endClass()
        .addFunction("processNonConst", &processNonConst<float, Base>);

    const Derived constObject(1.2f);
    luabridge::setGlobal(L, &constObject, "constObject");

    // bad argument #1 to 'processNonConst' (Derived expected, got const Derived)
    ASSERT_THROW(runLua("result = processNonConst (constObject)"), std::exception);
    ASSERT_TRUE(result().isNil());
}

TEST_F(ClassTests, PassOtherTypeInsteadOfNonConstThrows)
{
    using Int = Class<int, EmptyBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>() // Show that it does't matter
        .endClass()
        .addFunction("processNonConst", &processNonConst<int, EmptyBase>);

    // bad argument #1 to 'processNonConst' (Int expected, got number)
    ASSERT_THROW(runLua("result = processNonConst (1)"), std::exception);
    ASSERT_TRUE(result().isNil());
}

TEST_F(ClassTests, PassRegisteredClassInsteadOfUnregisteredThrows)
{
    using Int = Class<int, EmptyBase>;
    using Float = Class<float, EmptyBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Float>("Float")
        .addConstructor<void (*)(float)>()
        .endClass()
        .addFunction("processUnregisteredInt", &Int::staticFunction);

    // bad argument #1 to 'processUnregisteredInt' (unregistered class expected, got Float)
    ASSERT_THROW(runLua("result = processUnregisteredInt (Float (1.2))"), std::exception);
    ASSERT_TRUE(result().isNil());
}

namespace {

Class<int, EmptyBase>& returnRef()
{
    static Class<int, EmptyBase> value(1);
    return value;
}

const Class<int, EmptyBase>& returnConstRef()
{
    return returnRef();
}

Class<int, EmptyBase>* returnPtr()
{
    return &returnRef();
}

const Class<int, EmptyBase>* returnConstPtr()
{
    return &returnConstRef();
}

Class<int, EmptyBase> returnValue()
{
    return Class<int, EmptyBase>(2);
}

void addHelperFunctions(lua_State* L)
{
    luabridge::getGlobalNamespace(L)
        .addFunction("returnRef", &returnRef)
        .addFunction("returnConstRef", &returnConstRef)
        .addFunction("returnPtr", &returnPtr)
        .addFunction("returnConstPtr", &returnConstPtr)
        .addFunction("returnValue", &returnValue);
}

} // namespace

TEST_F(ClassTests, PassingUnregisteredClassFromLuaThrows)
{
    using Unregistered = Class<int, EmptyBase>;

    addHelperFunctions(L);

    ASSERT_THROW(runLua("result = returnRef ()"), std::exception);
    ASSERT_THROW(runLua("result = returnConstRef ()"), std::exception);
    ASSERT_THROW(runLua("result = returnPtr ()"), std::exception);
    ASSERT_THROW(runLua("result = returnConstPtr ()"), std::exception);
    ASSERT_THROW(runLua("result = returnValue ()"), std::exception);
}

TEST_F(ClassTests, DeriveFromUnregisteredClassThrows)
{
    using Base = Class<int, EmptyBase>;
    using Derived = Class<float, Base>;

    ASSERT_THROW((luabridge::getGlobalNamespace(L).deriveClass<Derived, Base>("Derived")),
                 std::exception);

    ASSERT_EQ(1, lua_gettop(L));
}

struct ClassFunctions : ClassTests
{
};

TEST_F(ClassFunctions, MemberFunctions)
{
    using Int = Class<int, EmptyBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addFunction("method", &Int::method)
        .endClass();

    addHelperFunctions(L);

    runLua("result = returnRef ():method (1)");
    ASSERT_EQ(1, result<int>());

    runLua("result = returnConstRef ().method"); // Don't call, just get
    ASSERT_TRUE(result().isNil());

    runLua("result = returnPtr ():method (2)");
    ASSERT_EQ(2, result<int>());

    runLua("result = returnConstPtr ().method"); // Don't call, just get
    ASSERT_TRUE(result().isNil());

    runLua("result = returnValue ():method (3)");
    ASSERT_EQ(3, result<int>());
}

TEST_F(ClassFunctions, MemberFunctions_PassState)
{
    using Int = Class<int, EmptyBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addFunction("method", &Int::methodState)
        .endClass();

    addHelperFunctions(L);

    runLua("result = returnRef ():method (1)");
    ASSERT_EQ(1, result<int>());

    runLua("result = returnConstRef ().method"); // Don't call, just get
    ASSERT_TRUE(result().isNil());

    runLua("result = returnPtr ():method (2)");
    ASSERT_EQ(2, result<int>());

    runLua("result = returnConstPtr ().method"); // Don't call, just get
    ASSERT_TRUE(result().isNil());

    runLua("result = returnValue ():method (3)");
    ASSERT_EQ(3, result<int>());
}

TEST_F(ClassFunctions, ConstMemberFunctions)
{
    using Int = Class<int, EmptyBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addFunction("constMethod", &Int::constMethod)
        .endClass();

    addHelperFunctions(L);

    runLua("result = returnRef ():constMethod (1)");
    ASSERT_EQ(1, result<int>());

    runLua("result = returnConstRef ():constMethod (2)");
    ASSERT_EQ(2, result<int>());

    runLua("result = returnPtr ():constMethod (3)");
    ASSERT_EQ(3, result<int>());

    runLua("result = returnConstPtr ():constMethod (4)");
    ASSERT_EQ(4, result<int>());

    runLua("result = returnValue ():constMethod (5)");
    ASSERT_EQ(5, result<int>());
}

namespace {

template<class T, class Base>
T proxyFunction(Class<T, Base>* object, T value)
{
    object->data = value;
    return value;
}

template<class T, class Base>
T proxyFunctionState(Class<T, Base>* object, T value, lua_State*)
{
    object->data = value;
    return value;
}

template<class T, class Base>
T proxyConstFunction(const Class<T, Base>* object, T value)
{
    return value;
}

} // namespace

TEST_F(ClassFunctions, ProxyFunctions)
{
    using Int = Class<int, EmptyBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addFunction("method", &proxyFunction<int, EmptyBase>)
        .endClass();

    addHelperFunctions(L);

    runLua("result = returnRef ():method (1)");
    ASSERT_EQ(1, result<int>());

    runLua("result = returnConstRef ().method"); // Don't call, just get
    ASSERT_TRUE(result().isNil());

    runLua("result = returnPtr ():method (2)");
    ASSERT_EQ(2, result<int>());

    runLua("result = returnConstPtr ().method"); // Don't call, just get
    ASSERT_TRUE(result().isNil());

    runLua("result = returnValue ():method (3)");
    ASSERT_EQ(3, result<int>());
}

TEST_F(ClassFunctions, ProxyFunctions_PassState)
{
    using Int = Class<int, EmptyBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addFunction("method", &proxyFunctionState<int, EmptyBase>)
        .endClass();

    addHelperFunctions(L);

    runLua("result = returnRef ():method (1)");
    ASSERT_EQ(1, result<int>());

    runLua("result = returnConstRef ().method"); // Don't call, just get
    ASSERT_TRUE(result().isNil());

    runLua("result = returnPtr ():method (2)");
    ASSERT_EQ(2, result<int>());

    runLua("result = returnConstPtr ().method"); // Don't call, just get
    ASSERT_TRUE(result().isNil());

    runLua("result = returnValue ():method (3)");
    ASSERT_EQ(3, result<int>());
}

TEST_F(ClassFunctions, ConstProxyFunctions)
{
    using Int = Class<int, EmptyBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addFunction("constMethod", &proxyConstFunction<int, EmptyBase>)
        .endClass();

    addHelperFunctions(L);

    runLua("result = returnRef ():constMethod (1)");
    ASSERT_EQ(1, result<int>());

    runLua("result = returnConstRef ():constMethod (2)");
    ASSERT_EQ(2, result<int>());

    runLua("result = returnPtr ():constMethod (3)");
    ASSERT_EQ(3, result<int>());

    runLua("result = returnConstPtr ():constMethod (4)");
    ASSERT_EQ(4, result<int>());

    runLua("result = returnValue ():constMethod (5)");
    ASSERT_EQ(5, result<int>());
}

TEST_F(ClassFunctions, StdFunctions)
{
    using Int = Class<int, EmptyBase>;

    auto sharedData = std::make_shared<int>();
    std::weak_ptr<int> data = sharedData; // Check __gc meta-method

    std::function<int(Int*, int)> function = [sharedData](Int* object, int value) {
        object->data = value;
        return value;
    };

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addFunction("method", std::move(function))
        .endClass();

    sharedData = nullptr;
    ASSERT_FALSE(data.expired());

    addHelperFunctions(L);

    runLua("result = returnRef ():method (1)");
    ASSERT_EQ(1, result<int>());

    runLua("result = returnConstRef ().method"); // Don't call, just get
    ASSERT_TRUE(result().isNil());

    runLua("result = returnPtr ():method (2)");
    ASSERT_EQ(2, result<int>());

    runLua("result = returnConstPtr ().method"); // Don't call, just get
    ASSERT_TRUE(result().isNil());

    runLua("result = returnValue ():method (3)");
    ASSERT_EQ(3, result<int>());

    runLua("result = nil");
    lua_close(L); // Force garbage collection
    L = nullptr;

    ASSERT_TRUE(data.expired());
}

TEST_F(ClassFunctions, StdFunctions_PassState)
{
    using Int = Class<int, EmptyBase>;

    auto sharedData = std::make_shared<int>();
    std::weak_ptr<int> data = sharedData; // Check __gc meta-method

    std::function<int(Int*, int, lua_State*)> function =
        [sharedData](Int* object, int value, lua_State*) {
            object->data = value;
            return value;
        };

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addFunction("method", std::move(function))
        .endClass();

    sharedData = nullptr;
    ASSERT_FALSE(data.expired());

    addHelperFunctions(L);

    runLua("result = returnRef ():method (1)");
    ASSERT_EQ(1, result<int>());

    runLua("result = returnConstRef ().method"); // Don't call, just get
    ASSERT_TRUE(result().isNil());

    runLua("result = returnPtr ():method (2)");
    ASSERT_EQ(2, result<int>());

    runLua("result = returnConstPtr ().method"); // Don't call, just get
    ASSERT_TRUE(result().isNil());

    runLua("result = returnValue ():method (3)");
    ASSERT_EQ(3, result<int>());

    runLua("result = nil");
    lua_close(L); // Force garbage collection
    L = nullptr;

    ASSERT_TRUE(data.expired());
}

TEST_F(ClassFunctions, ConstStdFunctions)
{
    using Int = Class<int, EmptyBase>;

    auto sharedData = std::make_shared<int>();
    std::weak_ptr<int> data = sharedData; // Check __gc meta-method

    std::function<int(const Int*, int)> function = [sharedData](const Int* object, int value) {
        object->data = value;
        return value;
    };

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addFunction("constMethod", std::move(function))
        .endClass();

    sharedData = nullptr;
    ASSERT_FALSE(data.expired());

    addHelperFunctions(L);

    runLua("result = returnRef ():constMethod (1)");
    ASSERT_EQ(1, result<int>());

    runLua("result = returnConstRef ():constMethod (2)");
    ASSERT_EQ(2, result<int>());

    runLua("result = returnPtr ():constMethod (3)");
    ASSERT_EQ(3, result<int>());

    runLua("result = returnConstPtr ():constMethod (4)");
    ASSERT_EQ(4, result<int>());

    runLua("result = returnValue ():constMethod (5)");
    ASSERT_EQ(5, result<int>());

    runLua("result = nil");
    lua_close(L); // Force garbage collection
    L = nullptr;

    ASSERT_TRUE(data.expired());
}

struct ClassProperties : ClassTests
{
};

TEST_F(ClassProperties, FieldPointers)
{
    using Int = Class<int, EmptyBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addProperty("data", &Int::data, true)
        .endClass();

    runLua("result = Int (501)");
    ASSERT_TRUE(result()["data"].isNumber());
    ASSERT_EQ(501, result()["data"].cast<int>());

    runLua("result.data = 2");
    ASSERT_TRUE(result()["data"].isNumber());
    ASSERT_EQ(2, result()["data"].cast<int>());

    runLua("result = Int (42).data");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(42, result<int>());
}

TEST_F(ClassProperties, FieldPointers_ReadOnly)
{
    using Int = Class<int, EmptyBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addProperty("data", &Int::data, false)
        .endClass();

    runLua("result = Int (501)");
    ASSERT_TRUE(result()["data"].isNumber());
    ASSERT_EQ(501, result()["data"].cast<int>());

    ASSERT_THROW(runLua("result.data = 2"), std::exception);

    runLua("result = Int (42).data");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(42, result<int>());
}

TEST_F(ClassProperties, MemberFunctions)
{
    using Int = Class<int, EmptyBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addProperty("data", &Int::getData, &Int::setData)
        .endClass();

    runLua("result = Int (501)");
    ASSERT_TRUE(result()["data"].isNumber());
    ASSERT_EQ(501, result()["data"].cast<int>());

    runLua("result.data = -2");
    ASSERT_TRUE(result()["data"].isNumber());
    ASSERT_EQ(-2, result()["data"].cast<int>());
}

TEST_F(ClassProperties, MemberFunctions_PassState)
{
    using Int = Class<int, EmptyBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addProperty("data", &Int::getDataState, &Int::setDataState)
        .endClass();

    runLua("result = Int (501)");
    ASSERT_TRUE(result()["data"].isNumber());
    ASSERT_EQ(501, result()["data"].cast<int>());

    runLua("result.data = -2");
    ASSERT_TRUE(result()["data"].isNumber());
    ASSERT_EQ(-2, result()["data"].cast<int>());
}

TEST_F(ClassProperties, MemberFunctions_ReadOnly)
{
    using Int = Class<int, EmptyBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addProperty("data", &Int::getData)
        .endClass();

    runLua("result = Int (501)");
    ASSERT_TRUE(result()["data"].isNumber());
    ASSERT_EQ(501, result()["data"].cast<int>());

    ASSERT_THROW(runLua("result.data = -2"), std::exception);
    ASSERT_EQ(501, result()["data"].cast<int>());
}

TEST_F(ClassProperties, MemberFunctions_Derived)
{
    using Base = Class<std::string, EmptyBase>;
    using Derived = Class<int, Base>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Base>("Base")
        .addProperty("data", &Base::getData, &Base::setData)
        .endClass()
        .deriveClass<Derived, Base>("Derived")
        .endClass();

    Derived derived(12);
    derived.Base::data = "abc";
    luabridge::setGlobal(L, &derived, "derived");

    runLua("result = derived.data");
    ASSERT_TRUE(result().isString());
    ASSERT_EQ("abc", result<std::string>());

    runLua("derived.data = 5"); // Lua just casts integer to string
    ASSERT_EQ("5", derived.Base::data);
    ASSERT_EQ(12, derived.data);

    runLua("derived.data = '123'");
    ASSERT_EQ("123", derived.Base::data);
    ASSERT_EQ(12, derived.data);
}

TEST_F(ClassProperties, MemberFunctions_Overridden)
{
    using Base = Class<float, EmptyBase>;
    using Derived = Class<int, Base>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Base>("Base")
        .addProperty("data", &Base::getData, &Base::setData)
        .endClass()
        .deriveClass<Derived, Base>("Derived")
        .addProperty("data", &Derived::getData, &Derived::setData)
        .endClass();

    Derived derived(50);
    derived.Base::data = 1.23f;
    luabridge::setGlobal(L, static_cast<Base*>(&derived), "base");
    luabridge::setGlobal(L, &derived, "derived");

    runLua("result = base.data");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(1.23f, result<float>());

    runLua("result = derived.data");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(50, result<int>());

    runLua("base.data = -3.14");
    ASSERT_EQ(-3.14f, derived.Base::data);
    ASSERT_EQ(50, derived.data);

    runLua("derived.data = 7");
    ASSERT_EQ(-3.14f, derived.Base::data);
    ASSERT_EQ(7, derived.data);
}

namespace {

template<class T, class BaseClass>
T getData(const Class<T, BaseClass>* object)
{
    return object->data;
}

template<class T, class BaseClass>
void setData(Class<T, BaseClass>* object, T data)
{
    object->data = data;
}

} // namespace

TEST_F(ClassProperties, ProxyFunctions)
{
    using Int = Class<int, EmptyBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addProperty("data", &getData<int, EmptyBase>, &setData<int, EmptyBase>)
        .endClass();

    runLua("result = Int (501)");
    ASSERT_TRUE(result()["data"].isNumber());
    ASSERT_EQ(501, result()["data"].cast<int>());

    runLua("result.data = -2");
    ASSERT_TRUE(result()["data"].isNumber());
    ASSERT_EQ(-2, result()["data"].cast<int>());
}

TEST_F(ClassProperties, ProxyFunctions_ReadOnly)
{
    using Int = Class<int, EmptyBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addProperty("data", &getData<int, EmptyBase>)
        .endClass();

    runLua("result = Int (501)");
    ASSERT_TRUE(result()["data"].isNumber());
    ASSERT_EQ(501, result()["data"].cast<int>());

    ASSERT_THROW(runLua("result.data = -2"), std::exception);
    ASSERT_EQ(501, result()["data"].cast<int>());
}

TEST_F(ClassProperties, ProxyFunctions_Derived)
{
    using Base = Class<std::string, EmptyBase>;
    using Derived = Class<int, Base>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Base>("Base")
        .addProperty("data", &getData<std::string, EmptyBase>, &setData<std::string, EmptyBase>)
        .endClass()
        .deriveClass<Derived, Base>("Derived")
        .endClass();

    Derived derived(12);
    derived.Base::data = "abc";
    luabridge::setGlobal(L, &derived, "derived");

    runLua("result = derived.data");
    ASSERT_TRUE(result().isString());
    ASSERT_EQ("abc", result<std::string>());

    runLua("derived.data = 5"); // Lua just casts integer to string
    ASSERT_EQ("5", derived.Base::data);
    ASSERT_EQ(12, derived.data);

    runLua("derived.data = '123'");
    ASSERT_EQ("123", derived.Base::data);
    ASSERT_EQ(12, derived.data);
}

TEST_F(ClassProperties, ProxyFunctions_Overridden)
{
    using Base = Class<float, EmptyBase>;
    using Derived = Class<int, Base>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Base>("Base")
        .addProperty("data", &getData<float, EmptyBase>, &setData<float, EmptyBase>)
        .endClass()
        .deriveClass<Derived, Base>("Derived")
        .addProperty("data", &getData<int, Base>, &setData<int, Base>)
        .endClass();

    Derived derived(50);
    derived.Base::data = 1.23f;
    luabridge::setGlobal(L, static_cast<Base*>(&derived), "base");
    luabridge::setGlobal(L, &derived, "derived");

    runLua("result = base.data");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(1.23f, result<float>());

    runLua("result = derived.data");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(50, result<int>());

    runLua("base.data = -3.14");
    ASSERT_EQ(-3.14f, derived.Base::data);
    ASSERT_EQ(50, derived.data);

    runLua("derived.data = 7");
    ASSERT_EQ(-3.14f, derived.Base::data);
    ASSERT_EQ(7, derived.data);
}

namespace {

template<class T, class BaseClass>
int getDataC(lua_State* L)
{
    auto objectRef = luabridge::LuaRef::fromStack(L, 1);
    auto* object = objectRef.cast<const Class<T, BaseClass>*>();
    luabridge::Stack<T>::push(L, object->data);
    return 1;
}

template<class T, class BaseClass>
int setDataC(lua_State* L)
{
    auto objectRef = luabridge::LuaRef::fromStack(L, 1);
    auto* object = objectRef.cast<const Class<T, BaseClass>*>();
    auto valueRef = luabridge::LuaRef::fromStack(L, 2);
    T value = valueRef.cast<T>();
    object->data = value;
    return 0;
}

} // namespace

TEST_F(ClassProperties, ProxyCFunctions)
{
    using Int = Class<int, EmptyBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addProperty("data", &getDataC<int, EmptyBase>, &setDataC<int, EmptyBase>)
        .endClass();

    runLua("result = Int (501)");
    ASSERT_TRUE(result()["data"].isNumber());
    ASSERT_EQ(501, result()["data"].cast<int>());

    runLua("result.data = -2");
    ASSERT_TRUE(result()["data"].isNumber());
    ASSERT_EQ(-2, result()["data"].cast<int>());
}

TEST_F(ClassProperties, ProxyCFunctions_ReadOnly)
{
    using Int = Class<int, EmptyBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addProperty("data", &getDataC<int, EmptyBase>)
        .endClass();

    runLua("result = Int (501)");
    ASSERT_TRUE(result()["data"].isNumber());
    ASSERT_EQ(501, result()["data"].cast<int>());

    ASSERT_THROW(runLua("result.data = -2"), std::exception);
    ASSERT_EQ(501, result()["data"].cast<int>());
}

TEST_F(ClassProperties, ProxyCFunctions_Derived)
{
    using Base = Class<std::string, EmptyBase>;
    using Derived = Class<int, Base>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Base>("Base")
        .addProperty("data", &getDataC<std::string, EmptyBase>, &setDataC<std::string, EmptyBase>)
        .endClass()
        .deriveClass<Derived, Base>("Derived")
        .endClass();

    Derived derived(12);
    derived.Base::data = "abc";
    luabridge::setGlobal(L, &derived, "derived");

    runLua("result = derived.data");
    ASSERT_TRUE(result().isString());
    ASSERT_EQ("abc", result<std::string>());

    runLua("derived.data = 5"); // Lua just casts integer to string
    ASSERT_EQ("5", derived.Base::data);
    ASSERT_EQ(12, derived.data);

    runLua("derived.data = '123'");
    ASSERT_EQ("123", derived.Base::data);
    ASSERT_EQ(12, derived.data);
}

TEST_F(ClassProperties, ProxyCFunctions_Overridden)
{
    using Base = Class<float, EmptyBase>;
    using Derived = Class<int, Base>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Base>("Base")
        .addProperty("data", &getDataC<float, EmptyBase>, &setDataC<float, EmptyBase>)
        .endClass()
        .deriveClass<Derived, Base>("Derived")
        .addProperty("data", &getData<int, Base>, &setData<int, Base>)
        .endClass();

    Derived derived(50);
    derived.Base::data = 1.23f;
    luabridge::setGlobal(L, static_cast<Base*>(&derived), "base");
    luabridge::setGlobal(L, &derived, "derived");

    runLua("result = base.data");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(1.23f, result<float>());

    runLua("result = derived.data");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(50, result<int>());

    runLua("base.data = -3.14");
    ASSERT_EQ(-3.14f, derived.Base::data);
    ASSERT_EQ(50, derived.data);

    runLua("derived.data = 7");
    ASSERT_EQ(-3.14f, derived.Base::data);
    ASSERT_EQ(7, derived.data);
}

TEST_F(ClassProperties, StdFunctions)
{
    using Int = Class<int, EmptyBase>;

    auto sharedGetterData = std::make_shared<int>();
    std::weak_ptr<int> getterData = sharedGetterData; // Check __gc meta-method

    auto sharedSetterData = std::make_shared<int>();
    std::weak_ptr<int> setterData = sharedGetterData; // Check __gc meta-method

    std::function<int(const Int*)> getter = [sharedGetterData](const Int* object) {
        return object->data;
    };

    std::function<void(Int*, int)> setter = [sharedSetterData](Int* object, int value) {
        object->data = value;
    };

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addProperty("data", std::move(getter), std::move(setter))
        .endClass();

    sharedGetterData = nullptr;
    ASSERT_FALSE(getterData.expired());

    sharedSetterData = nullptr;
    ASSERT_FALSE(setterData.expired());

    runLua("result = Int (501)");
    ASSERT_EQ(501, result()["data"].cast<int>());

    runLua("result.data = -2");
    ASSERT_TRUE(result()["data"].isNumber());
    ASSERT_EQ(-2, result()["data"].cast<int>());

    runLua("result = nil");
    lua_close(L); // Force garbage collection
    L = nullptr;

    ASSERT_TRUE(getterData.expired());
    ASSERT_TRUE(setterData.expired());
}

TEST_F(ClassProperties, StdFunctions_ReadOnly)
{
    using Int = Class<int, EmptyBase>;

    auto sharedGetterData = std::make_shared<int>();
    std::weak_ptr<int> getterData = sharedGetterData; // Check __gc meta-method

    std::function<int(const Int*)> getter = [sharedGetterData](const Int* object) {
        return object->data;
    };

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addProperty("data", std::move(getter))
        .endClass();

    sharedGetterData = nullptr;
    ASSERT_FALSE(getterData.expired());

    runLua("result = Int (501)");
    ASSERT_TRUE(result()["data"].isNumber());
    ASSERT_EQ(501, result()["data"].cast<int>());

    ASSERT_THROW(runLua("result.data = -2"), std::exception);
    ASSERT_EQ(501, result()["data"].cast<int>());

    runLua("result = nil");
    lua_close(L); // Force garbage collection
    L = nullptr;

    ASSERT_TRUE(getterData.expired());
}

struct ClassStaticFunctions : ClassTests
{
};

TEST_F(ClassStaticFunctions, Functions)
{
    using Int = Class<int, EmptyBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addStaticFunction("static", &Int::staticFunction)
        .endClass();

    runLua("result = Int.static (Int (35))");
    ASSERT_EQ(35, result<Int>().data);
}

TEST_F(ClassStaticFunctions, Functions_Derived)
{
    using Base = Class<std::string, EmptyBase>;
    using Derived = Class<int, Base>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Base>("Base")
        .addConstructor<void (*)(std::string)>()
        .addStaticFunction("static", &Base::staticFunction)
        .endClass()
        .deriveClass<Derived, Base>("Derived")
        .endClass();

    runLua("result = Derived.static (Base ('abc'))");
    ASSERT_EQ("abc", result<Base>().data);
}

TEST_F(ClassStaticFunctions, Functions_Overridden)
{
    using Base = Class<std::string, EmptyBase>;
    using Derived = Class<int, Base>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Base>("Base")
        .addConstructor<void (*)(std::string)>()
        .addStaticFunction("staticFunction", &Base::staticFunction)
        .endClass()
        .deriveClass<Derived, Base>("Derived")
        .addConstructor<void (*)(int)>()
        .addStaticFunction("staticFunction", &Derived::staticFunction)
        .endClass();

    runLua("result = Base.staticFunction (Base ('abc'))");
    ASSERT_EQ("abc", result<Base>().data);

    runLua("result = Derived.staticFunction (Derived (123))");
    ASSERT_EQ(123, result<Derived>().data);
}

TEST_F(ClassStaticFunctions, StdFunctions)
{
    using Int = Class<int, EmptyBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addStaticFunction("static", std::function<Int(Int)>(&Int::staticFunction))
        .endClass();

    runLua("result = Int.static (Int (35))");
    ASSERT_EQ(35, result<Int>().data);
}

struct ClassStaticProperties : ClassTests
{
};

TEST_F(ClassStaticProperties, FieldPointers)
{
    using Int = Class<int, EmptyBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addStaticProperty("staticData", &Int::staticData, true)
        .endClass();

    Int::staticData = 10;

    runLua("result = Int.staticData");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(10, result<int>());

    runLua("Int.staticData = 20");
    ASSERT_EQ(20, Int::staticData);
}

TEST_F(ClassStaticProperties, FieldPointers_ReadOnly)
{
    using Int = Class<int, EmptyBase>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addStaticProperty("staticData", &Int::staticData, false)
        .endClass();

    Int::staticData = 10;

    runLua("result = Int.staticData");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(10, result<int>());

    ASSERT_THROW(runLua("Int.staticData = 20"), std::exception);
    ASSERT_EQ(10, Int::staticData);
}

TEST_F(ClassStaticProperties, FieldPointers_Derived)
{
    using Base = Class<float, EmptyBase>;
    using Derived = Class<int, Base>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Base>("Base")
        .addStaticProperty("staticData", &Base::staticData, true)
        .endClass()
        .deriveClass<Derived, Base>("Derived")
        .endClass();

    Base::staticData = 1.23f;
    Derived::staticData = 50;

    runLua("result = Derived.staticData");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(1.23f, result<float>());

    runLua("Derived.staticData = -3.14");
    ASSERT_EQ(-3.14f, Base::staticData);
    ASSERT_EQ(50, Derived::staticData);
}

TEST_F(ClassStaticProperties, FieldPointers_Overridden)
{
    using Base = Class<float, EmptyBase>;
    using Derived = Class<int, Base>;

    luabridge::getGlobalNamespace(L)
        .beginClass<Base>("Base")
        .addStaticProperty("staticData", &Base::staticData, true)
        .endClass()
        .deriveClass<Derived, Base>("Derived")
        .addStaticProperty("staticData", &Derived::staticData, true)
        .endClass();

    Base::staticData = 1.23f;
    Derived::staticData = 50;

    runLua("result = Base.staticData");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(1.23f, result<float>());

    runLua("result = Derived.staticData");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(50, result<int>());

    runLua("Base.staticData = -3.14");
    ASSERT_EQ(-3.14f, Base::staticData);
    ASSERT_EQ(50, Derived::staticData);

    runLua("Derived.staticData = 7");
    ASSERT_EQ(-3.14f, Base::staticData);
    ASSERT_EQ(7, Derived::staticData);
}

struct ClassMetaMethods : ClassTests
{
};

TEST_F(ClassMetaMethods, __call)
{
    typedef Class<int, EmptyBase> Int;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addFunction("__call", &Int::operator())
        .endClass();

    runLua("result = Int (1) (-1)");
    ASSERT_TRUE(result().isUserdata());
    ASSERT_EQ(-1, result<Int>().data);

    runLua("result = Int (2) (5)");
    ASSERT_TRUE(result().isUserdata());
    ASSERT_EQ(5, result<Int>().data);
}

TEST_F(ClassMetaMethods, __tostring)
{
    typedef Class<int, EmptyBase> Int;
    typedef Class<std::string, EmptyBase> StringClass;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addFunction("__tostring", &Int::toString)
        .endClass()
        .beginClass<StringClass>("String")
        .addConstructor<void (*)(std::string)>()
        .addFunction("__tostring", &StringClass::toString)
        .endClass();

    runLua("result = tostring (Int (-123))");
    ASSERT_EQ("-123", result<std::string>());

#if LUA_VERSION_NUM >= 502
    // Lua 5.1 string.format doesn't use __tostring
    runLua("result = string.format ('%s%s', String ('abc'), Int (-123))");
    ASSERT_EQ("abc-123", result<std::string>());
#endif
}

TEST_F(ClassMetaMethods, __eq)
{
    typedef Class<int, EmptyBase> Int;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addFunction("__eq", &Int::operator==)
        .endClass();

    runLua("result = Int (1) == Int (1)");
    ASSERT_EQ(true, result<bool>());

    runLua("result = Int (1) ~= Int (1)");
    ASSERT_EQ(false, result<bool>());

    runLua("result = Int (1) == Int (2)");
    ASSERT_EQ(false, result<bool>());

    runLua("result = Int (1) ~= Int (2)");
    ASSERT_EQ(true, result<bool>());
}

TEST_F(ClassMetaMethods, __lt)
{
    typedef Class<int, EmptyBase> Int;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addFunction("__lt", &Int::operator<)
        .endClass();

    runLua("result = Int (1) < Int (1)");
    ASSERT_EQ(false, result<bool>());

    runLua("result = Int (1) < Int (2)");
    ASSERT_EQ(true, result<bool>());

    runLua("result = Int (2) < Int (1)");
    ASSERT_EQ(false, result<bool>());
}

TEST_F(ClassMetaMethods, __le)
{
    typedef Class<int, EmptyBase> Int;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addFunction("__le", &Int::operator<=)
        .endClass();

    runLua("result = Int (1) <= Int (1)");
    ASSERT_EQ(true, result<bool>());

    runLua("result = Int (1) <= Int (2)");
    ASSERT_EQ(true, result<bool>());

    runLua("result = Int (2) <= Int (1)");
    ASSERT_EQ(false, result<bool>());
}

TEST_F(ClassMetaMethods, __add)
{
    typedef Class<int, EmptyBase> Int;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addFunction("__add", &Int::operator+)
        .endClass();

    runLua("result = Int (1) + Int (2)");
    ASSERT_TRUE(result().isUserdata());
    ASSERT_EQ(3, result<Int>().data);
}

TEST_F(ClassMetaMethods, __sub)
{
    typedef Class<int, EmptyBase> Int;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addFunction("__sub", &Int::operator-)
        .endClass();

    runLua("result = Int (1) - Int (2)");
    ASSERT_TRUE(result().isUserdata());
    ASSERT_EQ(-1, result<Int>().data);
}

TEST_F(ClassMetaMethods, __mul)
{
    typedef Class<int, EmptyBase> Int;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addFunction("__mul", &Int::operator*)
        .endClass();

    runLua("result = Int (-2) * Int (-5)");
    ASSERT_TRUE(result().isUserdata());
    ASSERT_EQ(10, result<Int>().data);
}

TEST_F(ClassMetaMethods, __div)
{
    typedef Class<int, EmptyBase> Int;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addFunction("__div", &Int::operator/)
        .endClass();

    runLua("result = Int (10) / Int (2)");
    ASSERT_TRUE(result().isUserdata());
    ASSERT_EQ(5, result<Int>().data);
}

TEST_F(ClassMetaMethods, __mod)
{
    typedef Class<int, EmptyBase> Int;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addFunction("__mod", &Int::operator%)
        .endClass();

    runLua("result = Int (7) % Int (2)");
    ASSERT_TRUE(result().isUserdata());
    ASSERT_EQ(1, result<Int>().data);
}

TEST_F(ClassMetaMethods, __pow)
{
    typedef Class<int, EmptyBase> Int;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addFunction("__pow", &Int::operator-)
        .endClass();

    runLua("result = Int (5) ^ Int (2)");
    ASSERT_TRUE(result().isUserdata());
    ASSERT_EQ(3, result<Int>().data);
}

TEST_F(ClassMetaMethods, __unm)
{
    typedef Class<int, EmptyBase> Int;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addFunction("__unm", &Int::negate)
        .endClass();

    runLua("result = -Int (-3)");
    ASSERT_TRUE(result().isUserdata());
    ASSERT_EQ(3, result<Int>().data);
}

TEST_F(ClassMetaMethods, __concat)
{
    typedef Class<std::string, EmptyBase> String;

    luabridge::getGlobalNamespace(L)
        .beginClass<String>("String")
        .addConstructor<void (*)(std::string)>()
        .addFunction("__concat", &String::operator+)
        .endClass();

    ASSERT_THROW(runLua("result = String ('a') + String ('b')"), std::exception);

    runLua("result = String ('ab') .. String ('cd')");
    ASSERT_TRUE(result().isUserdata());
    ASSERT_EQ("abcd", result<String>().data);
}

TEST_F(ClassMetaMethods, __len)
{
    typedef Class<int, EmptyBase> Int;

    luabridge::getGlobalNamespace(L)
        .beginClass<Int>("Int")
        .addConstructor<void (*)(int)>()
        .addFunction("__len", &Int::len)
        .endClass();

    runLua("result = #Int (1)");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(1, result<int>());

    runLua("result = #Int (5)");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(5, result<int>());
}

namespace {

struct Table
{
    int index(const std::string& key) { return map.at(key); }

    void newIndex(const std::string& key, int value) { map.emplace(key, value); }

    std::map<std::string, int> map;
};

} // namespace

TEST_F(ClassMetaMethods, __index)
{
    luabridge::getGlobalNamespace(L)
        .beginClass<Table>("Table")
        .addFunction("__index", &Table::index)
        .endClass();

    Table t{{{"a", 1}, {"b", 2}}};

    luabridge::setGlobal(L, &t, "t");

    runLua("result = t.a");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(1, result<int>());

    runLua("result = t.b");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(2, result<int>());

    ASSERT_THROW(runLua("result = t.c"), std::exception); // at ("c") throws
}

TEST_F(ClassMetaMethods, __newindex)
{
    typedef Class<int, EmptyBase> Int;

    luabridge::getGlobalNamespace(L)
        .beginClass<Table>("Table")
        .addFunction("__newindex", &Table::newIndex)
        .endClass();

    Table t;

    luabridge::setGlobal(L, &t, "t");

    runLua("t.a = 1\n"
           "t ['b'] = 2");

    ASSERT_EQ((std::map<std::string, int>{{"a", 1}, {"b", 2}}), t.map);
}

TEST_F(ClassMetaMethods, __gcForbidden)
{
    typedef Class<int, EmptyBase> Int;

    ASSERT_THROW(luabridge::getGlobalNamespace(L)
                     .beginClass<Int>("Int")
                     .addFunction("__gc", &Int::method)
                     .endClass(),
                 std::exception);
}

TEST_F(ClassTests, EnclosedClassProperties)
{
    typedef Class<int, EmptyBase> Inner;
    typedef Class<Inner, EmptyBase> Outer;

    luabridge::getGlobalNamespace(L)
        .beginClass<Inner>("Inner")
        .addProperty("data", &Inner::data)
        .endClass()
        .beginClass<Outer>("Outer")
        .addProperty("data", &Outer::data)
        .endClass();

    Outer outer(Inner(0));
    luabridge::setGlobal(L, &outer, "outer");

    outer.data.data = 1;
    runLua("outer.data.data = 10");
    ASSERT_EQ(10, outer.data.data);

    runLua("result = outer.data.data");
    ASSERT_EQ(10, result<int>());
}

// namespace {

struct InnerClass
{
    ~InnerClass() { ++destructorCallCount; }

    static unsigned destructorCallCount;
};

unsigned InnerClass::destructorCallCount;

struct OuterClass
{
    OuterClass() { throw std::runtime_error("Exception"); }

    ~OuterClass() { ++destructorCallCount; }

    static unsigned destructorCallCount;
    InnerClass inner;
};

unsigned OuterClass::destructorCallCount;

//} // namespace

TEST_F(ClassTests, DestructorIsNotCalledIfConstructorThrows)
{
    luabridge::getGlobalNamespace(L)
        .beginClass<OuterClass>("OuterClass")
        .addConstructor<void (*)()>()
        .endClass();

    InnerClass::destructorCallCount = 0;
    OuterClass::destructorCallCount = 0;
    ASSERT_THROW(runLua("result = OuterClass ()"), std::exception);
    ASSERT_EQ(1, InnerClass::destructorCallCount);

    lua_close(L);
    L = nullptr;
    ASSERT_EQ(1, InnerClass::destructorCallCount);
    ASSERT_EQ(0, OuterClass::destructorCallCount);
}

TEST_F(ClassTests, DestructorIsCalledOnce)
{
    luabridge::getGlobalNamespace(L)
        .beginClass<InnerClass>("InnerClass")
        .addConstructor<void (*)()>()
        .endClass();

    InnerClass::destructorCallCount = 0;
    runLua("result = InnerClass ()");

    lua_close(L);
    L = nullptr;
    ASSERT_EQ(1, InnerClass::destructorCallCount);
}
