#include "StarLua.hpp"
#include "StarLuaConverters.hpp"
#include "StarLexicalCast.hpp"

#include "gtest/gtest.h"

using namespace Star;

TEST(LuaTest, BasicGetSet) {
  auto luaEngine = LuaEngine::create();
  LuaContext luaContext = luaEngine->createContext();
  luaContext.load(R"SCRIPT(
      data1 = 1.0
      data2 = 3.0 > 2.0
      data3 = "hello"
    )SCRIPT");

  luaContext.set("data4", 4.0);

  EXPECT_EQ(luaContext.get<double>("data1"), 1.0);
  EXPECT_EQ(luaContext.get<bool>("data2"), true);
  EXPECT_EQ(luaContext.get<String>("data3"), "hello");
  EXPECT_EQ(luaContext.get<double>("data4"), 4.0);
}

TEST(LuaTest, TableReferences) {
  auto luaEngine = LuaEngine::create();
  LuaContext luaContext = luaEngine->createContext();
  luaContext.load(R"SCRIPT(
      table = {foo=1, bar=2}
      tableRef = table
    )SCRIPT");

  auto table = luaContext.get<LuaTable>("table");
  auto tableRef1 = luaContext.get<LuaTable>("tableRef");
  auto tableRef2 = table;

  EXPECT_EQ(table.get<double>("foo"), 1.0);
  EXPECT_EQ(table.get<double>("bar"), 2.0);

  table.set("baz", 3.0);
  EXPECT_EQ(tableRef1.get<double>("baz"), 3.0);

  tableRef1.set("baf", 4.0);
  EXPECT_EQ(table.get<double>("baf"), 4.0);
  EXPECT_EQ(tableRef2.get<double>("baf"), 4.0);
}

TEST(LuaTest, FunctionCallTest) {
  weak_ptr<int> destructionObserver;

  {
    auto luaEngine = LuaEngine::create();
    LuaContext luaContext = luaEngine->createContext();
    luaContext.load(R"SCRIPT(
        function testFunc(arg1, arg2)
          return callback(3) + arg1 + arg2
        end

        function testEmpty()
          return emptyCallback()
        end
      )SCRIPT");

    auto toDestruct = make_shared<int>();
    destructionObserver = toDestruct;
    luaContext.set("callback", luaEngine->createFunction([toDestruct](double n) { return n * 2; }));

    luaContext.set("emptyCallback", luaEngine->createFunction([]() { return "heyooo"; }));

    EXPECT_EQ(luaContext.invokePath<double>("testFunc", 5.0, 10.0), 21.0);
    EXPECT_EQ(luaContext.invokePath<String>("emptyCallback"), "heyooo");
  }

  EXPECT_TRUE(destructionObserver.expired());
}

TEST(LuaTest, CoroutineTest) {
  auto luaEngine = LuaEngine::create();
  LuaContext luaContext = luaEngine->createContext();

  luaContext.load(R"SCRIPT(
      function accumulate(sum)
        return sum + callback(coroutine.yield(sum))
      end

      function run()
          local sum = 0
          for i=1,4 do
            sum = accumulate(sum)
          end
          return sum
      end

      co = coroutine.create(run)
    )SCRIPT");

  luaContext.set("callback", luaEngine->createFunction([](double num) { return num * 2; }));

  LuaThread thread = luaEngine->createThread();
  EXPECT_EQ(thread.status(), LuaThread::Status::Dead);
  LuaFunction func = luaContext.get<LuaFunction>("run");
  thread.pushFunction(func);
  EXPECT_EQ(thread.status(), LuaThread::Status::Active);
  EXPECT_EQ(thread.resume<double>(), 0.0);
  EXPECT_EQ(thread.resume<double>(1.0), 2.0);
  EXPECT_EQ(thread.resume<double>(3.0), 8.0);
  EXPECT_EQ(thread.resume<double>(5.0), 18.0);
  EXPECT_EQ(thread.resume<double>(7.0), 32.0);
  // manually created threads are empty after execution is finished
  EXPECT_EQ(thread.status(), LuaThread::Status::Dead);

  thread.pushFunction(func);
  EXPECT_EQ(thread.resume<double>(), 0.0);
  EXPECT_EQ(thread.resume<double>(1.0), 2.0);
  EXPECT_THROW(thread.pushFunction(func), LuaException); // pushing function to suspended or errored thread

  auto coroutine = luaContext.get<LuaThread>("co");
  EXPECT_EQ(coroutine.status(), LuaThread::Status::Active);
  EXPECT_EQ(coroutine.resume<double>(), 0.0);
  EXPECT_EQ(coroutine.resume<double>(1.0), 2.0);
  EXPECT_EQ(coroutine.resume<double>(3.0), 8.0);
  EXPECT_EQ(coroutine.resume<double>(5.0), 18.0);
  EXPECT_EQ(coroutine.resume<double>(7.0), 32.0);
  EXPECT_EQ(coroutine.status(), LuaThread::Status::Dead);
  EXPECT_THROW(coroutine.resume(), LuaException);
  EXPECT_EQ(coroutine.status(), LuaThread::Status::Dead);

  LuaThread thread2 = luaEngine->createThread();
  EXPECT_EQ(thread2.status(), LuaThread::Status::Dead);
  thread2.pushFunction(func);
  EXPECT_EQ(thread2.status(), LuaThread::Status::Active);
  EXPECT_EQ(thread2.resume<double>(), 0.0);
  EXPECT_EQ(thread2.resume<double>(1.0), 2.0);
  EXPECT_THROW(thread2.resume<String>("not_a_number"), LuaException);
  EXPECT_EQ(thread2.status(), LuaThread::Status::Error);
  EXPECT_THROW(thread2.resume(), LuaException);
  EXPECT_EQ(thread2.status(), LuaThread::Status::Error);
}

template <typename T>
bool roundTripEqual(LuaContext const& context, T t) {
  return context.invokePath<T>("roundTrip", t) == t;
}

TEST(LuaTest, Converters) {
  auto luaEngine = LuaEngine::create();
  LuaContext luaContext = luaEngine->createContext();

  luaContext.load( R"SCRIPT(
      function makeVec()
        return {1, 2}
      end

      function makePoly()
        return {{1, 2}, {3, 4}, {5, 6}}
      end

      function roundTrip(ret)
        return ret
      end
    )SCRIPT");

  auto vecCompare = Vec2F(1.0f, 2.0f);
  auto polyCompare = PolyF({Vec2F(1.0f, 2.0f), Vec2F(3.0f, 4.0f), Vec2F(5.0f, 6.0f)});

  EXPECT_TRUE(luaContext.invokePath<Vec2F>("makeVec") == vecCompare);
  EXPECT_TRUE(luaContext.invokePath<PolyF>("makePoly") == polyCompare);
  EXPECT_TRUE(luaContext.invokePath<Vec2F>("roundTrip", vecCompare) == vecCompare);
  EXPECT_TRUE(luaContext.invokePath<PolyF>("roundTrip", polyCompare) == polyCompare);

  EXPECT_TRUE(roundTripEqual(luaContext, PolyF()));
  EXPECT_TRUE(roundTripEqual(luaContext, List<int>{1, 2, 3, 4}));
  EXPECT_TRUE(roundTripEqual(luaContext, List<PolyF>{PolyF(), PolyF()}));
  EXPECT_TRUE(roundTripEqual(luaContext, Maybe<int>(1)));
  EXPECT_TRUE(roundTripEqual(luaContext, Maybe<int>()));

  auto listCompare = List<int>{1, 2, 3, 4};
  EXPECT_TRUE(luaContext.invokePath<List<int>>("roundTrip", JsonArray{1, 2, 3, 4}) == listCompare);
  auto mapCompare = StringMap<String>{{"one", "two"}, {"three", "four"}};
  EXPECT_TRUE(luaContext.invokePath<StringMap<String>>("roundTrip", JsonObject{{"one", "two"}, {"three", "four"}}) == mapCompare);
}

struct TestUserData1 {
  int field;
};

struct TestUserData2 {
  int field;
};

namespace Star {
template <>
struct LuaConverter<TestUserData1> : LuaUserDataConverter<TestUserData1> {};

template <>
struct LuaConverter<TestUserData2> : LuaUserDataConverter<TestUserData2> {};
}

TEST(LuaTest, UserDataTest) {
  auto luaEngine = LuaEngine::create();

  LuaContext luaContext = luaEngine->createContext();
  luaContext.load(
      R"SCRIPT(
        function doit(ref)
          global = ref
        end
      )SCRIPT");

  auto userdata1 = luaEngine->createUserData(TestUserData1{1});
  auto userdata2 = luaEngine->createUserData(TestUserData2{2});

  luaContext.invokePath("doit", userdata1);
  auto userdata3 = luaContext.get<LuaUserData>("global");

  EXPECT_TRUE(userdata2.is<TestUserData2>());
  EXPECT_FALSE(userdata2.is<TestUserData1>());

  EXPECT_TRUE(userdata1.is<TestUserData1>());
  EXPECT_FALSE(userdata1.is<TestUserData2>());

  EXPECT_TRUE(userdata3.is<TestUserData1>());
  EXPECT_FALSE(userdata3.is<TestUserData2>());

  EXPECT_EQ(userdata1.get<TestUserData1>().field, 1);
  EXPECT_EQ(userdata2.get<TestUserData2>().field, 2);
  EXPECT_EQ(userdata3.get<TestUserData1>().field, 1);

  userdata1.get<TestUserData1>().field = 3;
  EXPECT_EQ(userdata1.get<TestUserData1>().field, 3);
  EXPECT_EQ(userdata3.get<TestUserData1>().field, 3);

  luaContext.invokePath("doit", TestUserData1());
  auto userdata4 = luaContext.get("global");
  EXPECT_TRUE(luaEngine->luaMaybeTo<TestUserData1>(userdata4).isValid());

  luaContext.invokePath("doit", "notuserdata");
  auto notuserdata = luaContext.get("global");
  EXPECT_FALSE(luaEngine->luaMaybeTo<TestUserData1>(notuserdata).isValid());
}

namespace Star {
template <>
struct LuaConverter<Vec3F> : LuaUserDataConverter<Vec3F> {};

template <>
struct LuaUserDataMethods<Vec3F> {
  static LuaMethods<Vec3F> make() {
    LuaMethods<Vec3F> methods;
    methods.registerMethodWithSignature<float, Vec3F const&>("magnitude", mem_fn(&Vec3F::magnitude));
    return methods;
  }
};
}

TEST(LuaTest, UserMethodTest) {
  auto luaEngine = LuaEngine::create();
  luaEngine->setGlobal("vec3", luaEngine->createFunctionWithSignature<Vec3F, float, float, float>(construct<Vec3F>()));

  LuaContext luaContext = luaEngine->createContext();
  luaContext.load(R"SCRIPT(
      v = vec3(3, 2, 1)
      function testMagnitude(v2)
        return v:magnitude() + v2:magnitude()
      end
    )SCRIPT");

  float magnitude = luaContext.invokePath<float>("testMagnitude", Vec3F(5, 5, 5));
  EXPECT_TRUE(fabs(magnitude - (Vec3F(3, 2, 1).magnitude() + Vec3F(5, 5, 5).magnitude())) < 0.00001f);
}

TEST(LuaTest, GlobalTest) {
  auto luaEngine = LuaEngine::create();
  luaEngine->setGlobal("globalfoo", LuaInt(42));
  EXPECT_EQ(luaEngine->getGlobal("globalfoo"), LuaInt(42));

  LuaContext luaContext = luaEngine->createContext();
  luaContext.load(R"SCRIPT(
      function test()
        return globalfoo
      end
    )SCRIPT");

  EXPECT_EQ(luaContext.invokePath("test"), LuaInt(42));
}

TEST(LuaTest, ArgTest) {
  auto luaEngine = LuaEngine::create();

  LuaContext luaContext = luaEngine->createContext();
  luaContext.load(R"SCRIPT(
      function test()
        callback("2", 3, nil)
      end
    )SCRIPT");

  luaContext.set("callback",
      luaEngine->createFunction([](LuaFloat n, LuaString s, LuaBoolean b, LuaValue o) {
        EXPECT_EQ(n, 2.0);
        EXPECT_EQ(s, String("3"));
        EXPECT_EQ(b, false);
        EXPECT_EQ(o, LuaNil);
      }));

  luaContext.invokePath("test");
}

TEST(LuaTest, ArrayTest) {
  auto luaEngine = LuaEngine::create();
  LuaContext luaContext = luaEngine->createContext();
  luaContext.load(R"SCRIPT(
      function test()
        return {2, 4, 6, 8, 10}
      end
    )SCRIPT");

  auto arrayTable = luaContext.invokePath("test").get<LuaTable>();

  EXPECT_EQ(arrayTable.length(), 5);
  EXPECT_EQ(arrayTable.get(2), LuaInt(4));
  EXPECT_EQ(arrayTable.get(5), LuaInt(10));

  List<pair<int, int>> values;
  arrayTable.iterate([&values](LuaValue const& key, LuaValue const& value) {
    auto ikey = key.get<LuaInt>();
    auto ivalue = value.get<LuaInt>();
    values.append({ikey, ivalue});
  });
  List<pair<int, int>> compare = {{1, 2}, {2, 4}, {3, 6}, {4, 8}, {5, 10}};
  EXPECT_EQ(values, compare);
}

TEST(LuaTest, PathTest) {
  auto luaEngine = LuaEngine::create();
  LuaContext luaContext = luaEngine->createContext();
  luaContext.load(R"SCRIPT(
      foo = {
          bar = {
            baz = 1
          }
        }

      function test()
        return foo.bar.baf
      end
    )SCRIPT");

  EXPECT_EQ(luaContext.containsPath("foo.bar.baz"), true);
  EXPECT_EQ(luaContext.getPath("foo.bar.baz"), LuaInt(1));
  EXPECT_EQ(luaContext.containsPath("foo.nothing.at.all"), false);

  luaContext.setPath("foo.bar.baf", LuaInt(5));
  EXPECT_EQ(luaContext.invokePath("test"), LuaInt(5));

  luaContext.setPath("new.table.value", LuaInt(5));
  EXPECT_EQ(luaContext.getPath("new.table.value"), LuaInt(5));
}

TEST(LuaTest, CallbackTest) {
  auto luaEngine = LuaEngine::create();

  LuaCallbacks callbacks;
  callbacks.registerCallback("add", [](LuaInt a, LuaInt b) { return a + b; });
  callbacks.registerCallbackWithSignature<LuaInt, LuaInt, LuaInt>("subtract", [](int a, int b) { return a - b; });
  callbacks.registerCallbackWithSignature<LuaInt, LuaInt>("multiply2", bind([](int a, int b) { return a * b; }, 2, _1));
  callbacks.registerCallbackWithSignature<void, LuaValue>("nothing", [](LuaValue v) { return v; });

  LuaContext luaContext = luaEngine->createContext();
  luaContext.setCallbacks("callbacks", callbacks);
  luaContext.load(R"SCRIPT(
      function test1()
        return callbacks.multiply2(callbacks.add(5, 10) + callbacks.subtract(3, 10))
      end

      function test2()
        return callbacks.nothing(1)
      end
    )SCRIPT");

  EXPECT_EQ(luaContext.invokePath("test1").get<LuaInt>(), 16);
  EXPECT_EQ(luaContext.invokePath("test2"), LuaNil);
}

TEST(LuaTest, VariableParameters) {
  auto luaEngine = LuaEngine::create();
  auto context1 = luaEngine->createContext();

  context1.load(R"SCRIPT(
      function variableArgsCount(...)
        local arg = {...}
        return #arg
      end
    )SCRIPT");

  auto res1 = context1.invokePath<int>("variableArgsCount");
  auto res2 = context1.invokePath<int>("variableArgsCount", 1);
  auto res3 = context1.invokePath<int>("variableArgsCount", 1, 1);
  auto res4 = context1.invokePath<int>("variableArgsCount", 1, 1, 1);

  EXPECT_EQ(res1, 0);
  EXPECT_EQ(res2, 1);
  EXPECT_EQ(res3, 2);
  EXPECT_EQ(res4, 3);
}

TEST(LuaTest, Scope) {
  auto luaEngine = LuaEngine::create();
  auto script1 = luaEngine->compile(R"SCRIPT(
      function create(param)
        local self = {}
        local foo = param

        local getValue = function()
          return foo
        end

        function self.get()
          return getValue()
        end

        return self
      end
    )SCRIPT");

  auto script2 = luaEngine->compile(R"SCRIPT(
      function init()
        obj = create(param)
      end

      function produce()
        return obj.get()
      end
    )SCRIPT");

  auto context1 = luaEngine->createContext();
  context1.load(script1);
  context1.load(script2);

  auto context2 = luaEngine->createContext();
  context2.load(script1);
  context2.load(script2);

  context1.setPath("param", 1);
  context1.invokePath("init");

  context2.setPath("param", 2);
  context2.invokePath("init");

  EXPECT_EQ(context1.invokePath<int>("produce"), 1);
  EXPECT_EQ(context2.invokePath<int>("produce"), 2);

  context1.setPath("param", 2);
  context1.invokePath("init");

  context2.setPath("param", 1);
  context2.invokePath("init");

  EXPECT_EQ(context1.invokePath<int>("produce"), 2);
  EXPECT_EQ(context2.invokePath<int>("produce"), 1);
}

TEST(LuaTest, Scope2) {
  auto luaEngine = LuaEngine::create();

  auto context1 = luaEngine->createContext();
  context1.load(R"SCRIPT(
      function init1()
        global = {}
        global.val = 10
      end
    )SCRIPT");

  auto context2 = luaEngine->createContext();
  context2.load(R"SCRIPT(
      function init2()
        global = {}
        global.val = 20
      end
    )SCRIPT");

  EXPECT_TRUE(context1.contains("init1"));
  EXPECT_TRUE(context2.contains("init2"));
  EXPECT_FALSE(context1.contains("init2"));
  EXPECT_FALSE(context2.contains("init1"));

  context1.invokePath("init1");
  EXPECT_EQ(context1.getPath<int>("global.val"), 10);

  EXPECT_TRUE(context2.getPath("global") == LuaNil);

  context2.invokePath("init2");
  EXPECT_EQ(context2.getPath<int>("global.val"), 20);

  EXPECT_EQ(context1.getPath<int>("global.val"), 10);
}

TEST(LuaTest, MetaTable) {
  auto luaEngine = LuaEngine::create();

  auto context = luaEngine->createContext();
  context.load(R"SCRIPT(
      function add(a, b)
        return a + b
      end
    )SCRIPT");

  auto mt = luaEngine->createTable();
  mt.set("__add",
      luaEngine->createFunction([](LuaEngine& engine, LuaTable const& a, LuaTable const& b) {
        return engine.createArrayTable(
            initializer_list<double>{a.get<double>(1) + b.get<double>(1), a.get<double>(2) + b.get<double>(2)});
      }));
  mt.set("test", "hello");

  auto t1 = luaEngine->createArrayTable(initializer_list<double>{1, 2});
  t1.setMetatable(mt);

  auto t2 = luaEngine->createArrayTable(initializer_list<double>{5, 6});
  t2.setMetatable(mt);

  auto tr = context.invokePath<LuaTable>("add", t1, t2);
  EXPECT_EQ(tr.get<double>(1), 6);
  EXPECT_EQ(tr.get<double>(2), 8);
  EXPECT_EQ(t1.getMetatable()->get<String>("test"), "hello");
  EXPECT_EQ(t2.getMetatable()->get<String>("test"), "hello");
}

TEST(LuaTest, Integers) {
  auto luaEngine = LuaEngine::create();
  auto context = luaEngine->createContext();
  context.load(R"SCRIPT(
      n1 = 0
      n2 = 1
      n3 = 1.0
      n4 = 1.1
      n5 = 5.0
      n6 = 5
    )SCRIPT");

  EXPECT_EQ(context.get("n1"), LuaInt(0));
  EXPECT_EQ(context.get("n2"), LuaInt(1));
  EXPECT_EQ(context.get("n3"), LuaFloat(1.0));
  EXPECT_EQ(context.get("n4"), LuaFloat(1.1));
  EXPECT_EQ(context.get("n5"), LuaFloat(5.0));
  EXPECT_EQ(context.get("n6"), LuaInt(5));
}

TEST(LuaTest, Require) {
  auto luaEngine = LuaEngine::create();
  auto context = luaEngine->createContext();
  context.setRequireFunction([](LuaContext& context, LuaString const& arg) {
      context.set(arg, context.createFunction([arg]() {
          return arg;
        }));
    });

  context.load(R"SCRIPT(
      require "a"
      require "b"
      require "c"

      function res()
        return a() .. b() .. c()
      end
    )SCRIPT");

  EXPECT_EQ(context.invokePath<LuaString>("res"), String("abc"));
}

TEST(LuaTest, Eval) {
  auto luaEngine = LuaEngine::create();
  auto context = luaEngine->createContext();

  context.eval("i = 3");
  // Make sure statements and expressions both work in eval.
  EXPECT_EQ(context.eval<int>("i + 1"), 4);
  EXPECT_EQ(context.eval<int>("return i + 1"), 4);
}

TEST(LuaTest, Multi) {
  auto luaEngine = LuaEngine::create();
  auto script = luaEngine->compile(R"SCRIPT(
      function entry()
        return callbacks.func(2, 4)
      end

      function sum(...)
        local sum = 0
        for i,v in ipairs(arg) do
          sum = sum + v
        end
        return sum
      end
    )SCRIPT");

  auto context1 = luaEngine->createContext();
  auto context2 = luaEngine->createContext();
  auto context3 = luaEngine->createContext();

  context1.load(script);
  context2.load(script);
  context3.load(script);

  LuaCallbacks addCallbacks;
  addCallbacks.registerCallback("func",
      [](LuaVariadic<int> const& args) -> int {
        int sum = 0.0;
        for (auto arg : args)
          sum += arg;
        return sum;
      });

  LuaCallbacks multCallbacks;
  multCallbacks.registerCallback("func",
      [](LuaVariadic<int> const& args) -> int {
        int mult = 1.0;
        for (auto arg : args)
          mult *= arg;
        return mult;
      });

  context1.setCallbacks("callbacks", addCallbacks);
  context2.setCallbacks("callbacks", multCallbacks);
  context3.setCallbacks("callbacks", addCallbacks);

  EXPECT_EQ(context1.invokePath<int>("entry"), 6);
  EXPECT_EQ(context2.invokePath<int>("entry"), 8);
  EXPECT_EQ(context3.invokePath<int>("entry"), 6);
  EXPECT_EQ(context1.invokePath<int>("entry"), 6);
  EXPECT_EQ(context2.invokePath<int>("entry"), 8);
  EXPECT_EQ(context3.invokePath<int>("entry"), 6);
  EXPECT_EQ(context1.invokePath<int>("entry"), 6);

  auto context4 = luaEngine->createContext();
  context4.load(R"SCRIPT(
      function sum(...)
        local args = {...}
        local sum = 0
        for i = 1, #args do
          sum = sum + args[i]
        end
        return sum
      end

      function mreturn(...)
        return ...
      end

      function callbacktest(...)
        local x, y = callback()
        return x, y
      end

      function emptycallbacktest(...)
        return emptycallback()
      end
    )SCRIPT");
  EXPECT_EQ(context4.invokePath<int>("sum", LuaVariadic<int>{1, 2, 3}), 6);
  EXPECT_EQ(context4.invokePath<int>("sum", 5, LuaVariadic<int>{1, 2, 3}, 10), 21);
  EXPECT_EQ(context4.invokePath<LuaVariadic<int>>("mreturn", 1, 2, 3), LuaVariadic<int>({1, 2, 3}));

  int a;
  float b;
  String c;
  luaTie(a, b, c) = context4.invokePath<LuaTupleReturn<int, float, String>>("mreturn", 1, 2.0f, "foo");
  EXPECT_EQ(a, 1);
  EXPECT_EQ(b, 2.0f);
  EXPECT_EQ(c, "foo");

  context4.set("callback", context4.createFunction([]() { return luaTupleReturn(5, 10); }));

  context4.set("emptycallback", context4.createFunction([]() { return luaTupleReturn(); }));

  int d;
  int e;
  luaTie(d, e) = context4.invokePath<LuaTupleReturn<int, int>>("callbacktest");
  EXPECT_EQ(d, 5);
  EXPECT_EQ(e, 10);

  EXPECT_EQ(context4.invokePath("emptycallbacktest"), LuaNil);
}

TEST(LuaTest, Limits) {
  auto luaEngine = LuaEngine::create();
  luaEngine->setInstructionLimit(500000);
  luaEngine->setRecursionLimit(64);
  auto context = luaEngine->createContext();
  context.load(R"SCRIPT(
      function toinfinityandbeyond()
        while true do
        end
      end

      function toabignumberandthenstop()
        for i = 0, 50000 do
        end
      end
    )SCRIPT");

  // Make sure infinite loops trigger the instruction limit.
  EXPECT_THROW(context.invokePath("toinfinityandbeyond"), LuaInstructionLimitReached);

  // Make sure the instruction count is reset after each call.
  context.invokePath("toabignumberandthenstop");

  auto infLoop = R"SCRIPT(
        while true do
        end
      )SCRIPT";

  // Make sure loading code into context with infinite loops in their
  // evaluation triggers instruction limit.
  EXPECT_THROW(context.load(infLoop), LuaInstructionLimitReached);

  // And the same for eval
  EXPECT_THROW(context.eval(infLoop), LuaInstructionLimitReached);

  auto call1 = [&context]() { context.invokePath("call2"); };

  auto call2 = [&context]() { context.invokePath("call1"); };

  context.set("call1", context.createFunction(call1));
  context.set("call2", context.createFunction(call2));

  EXPECT_THROW(context.invokePath("call1"), LuaRecursionLimitReached);

  // Make sure the context still functions properly after these previous
  // errors.
  EXPECT_EQ(context.eval<int>("1 + 1"), 2);
}

TEST(LuaTest, Errors) {
  auto luaEngine = LuaEngine::create();
  auto context = luaEngine->createContext();

  EXPECT_THROW(context.eval("while true do"), LuaIncompleteStatementException);
  context.setPath("val", 1.0);
  EXPECT_THROW(context.getPath<Vec2D>("val"), LuaConversionException);
  EXPECT_EQ(luaEngine->luaMaybeTo<RectF>(context.get("val")), Maybe<RectF>());

  context.set("throwException", luaEngine->createFunction([]() {
      throw StarException("lua caught the exception!");
    }));

  context.load(R"SCRIPT(
      function throwError()
        return throwException()
      end
      function catchError()
        return pcall(throwException)
      end
    )SCRIPT");

  EXPECT_THROW(context.invokePath("throwError"), LuaException);

  bool status;
  LuaValue error;
  luaTie(status, error) = context.invokePath<LuaTupleReturn<bool, LuaValue>>("catchError");
  EXPECT_EQ(status, false);
  EXPECT_TRUE(String(toString(error)).contains("lua caught the exception"));
}

TEST(LuaTest, GarbageCollection) {
  auto engine = LuaEngine::create();
  auto context = engine->createContext();

  auto ptr = make_shared<int>(5);
  engine->setAutoGarbageCollection(false);
  context.setPath("ref", context.createUserData(ptr));
  EXPECT_EQ(ptr.use_count(), 2);
  context.setPath("ref", LuaNil);
  EXPECT_EQ(ptr.use_count(), 2);
  engine->collectGarbage();
  EXPECT_EQ(ptr.use_count(), 1);
}

TEST(LuaTest, IntTest) {
  auto engine = LuaEngine::create();
  auto context = engine->createContext();
  context.setPath("test", (uint64_t)-1);
  EXPECT_EQ(context.getPath<uint64_t>("test"), (uint64_t)-1);
}

TEST(LuaTest, VariantTest) {
  auto engine = LuaEngine::create();
  auto context = engine->createContext();

  typedef Variant<int, String> IntOrString;

  EXPECT_EQ(context.eval<IntOrString>("'foo'"), IntOrString(String("foo")));
  EXPECT_EQ(context.eval<IntOrString>("'1'"), IntOrString(1));

  typedef MVariant<Maybe<int>, String> MIntOrString;

  EXPECT_EQ(context.eval<MIntOrString>("'foo'"), MIntOrString(String("foo")));
  EXPECT_EQ(context.eval<MIntOrString>("'1'"), MIntOrString(Maybe<int>(1)));
  EXPECT_EQ(context.eval<MIntOrString>("nil"), MIntOrString());
}

TEST(LuaTest, ProfilingTest) {
  auto luaEngine = LuaEngine::create();
  luaEngine->setProfilingEnabled(true);
  luaEngine->setInstructionMeasureInterval(1000);

  auto context = luaEngine->createContext();
  context.eval(R"SCRIPT(
      function function1()
        for i = 1, 1000 do
        end
      end

      function function2()
        for i = 1, 1000 do
        end
      end

      function function3()
        for i = 1, 1000 do
        end
      end

      for i = 1, 10000 do
        function1()
        function2()
        function3()
      end
    )SCRIPT");

  StringSet names;
  List<LuaProfileEntry> profile = luaEngine->getProfile();
  for (auto const& p : profile[0].calls)
    names.add(p.second->name.value());

  EXPECT_TRUE(names.contains("function1"));
  EXPECT_TRUE(names.contains("function2"));
  EXPECT_TRUE(names.contains("function3"));
}