#include <config/lua/LuaEventHandler.hpp>

#include <config/lua/objects/LuaEventSubscription.hpp>
#include <config/lua/objects/LuaKeybind.hpp>
#include <config/lua/objects/LuaWorkspaceRule.hpp>

#include <keybinds/Manager.hpp>
#include <event/EventBus.hpp>

#include <gtest/gtest.h>

#include <stdexcept>

extern "C" {
#include <lualib.h>
#include <lauxlib.h>
}

using namespace Config::Lua;

namespace {
    Keybinds::CBind makeBind(std::vector<std::string> keys = {"Q"}, Keybinds::BindFlags flags = 0, Keybinds::SExtraBindArgs extra = {}) {
        auto bind = Keybinds::CBind::make(std::move(keys), flags, [] { return Keybinds::SBindResult{}; }, std::move(extra));
        if (!bind)
            throw std::runtime_error(bind.error());

        return std::move(*bind);
    }

    Keybinds::PBind makeBindPtr(std::vector<std::string> keys = {"Q"}, Keybinds::BindFlags flags = 0, Keybinds::SExtraBindArgs extra = {}) {
        return makeShared<Keybinds::CBind>(makeBind(std::move(keys), flags, std::move(extra)));
    }

    class CScopedKeybindManager {
      public:
        CScopedKeybindManager() : m_previous(std::move(Keybinds::mgr())) {
            Keybinds::mgr() = makeUnique<Keybinds::CKeybindManager>();
        }

        ~CScopedKeybindManager() {
            Keybinds::mgr() = std::move(m_previous);
        }

      private:
        UP<Keybinds::CKeybindManager> m_previous;
    };

    class CLuaState {
      public:
        CLuaState() : m_lua(luaL_newstate()) {
            luaL_openlibs(m_lua);
        }

        ~CLuaState() {
            if (m_lua)
                lua_close(m_lua);
        }

        lua_State* get() const {
            return m_lua;
        }

      private:
        lua_State* m_lua = nullptr;
    };

    int getGlobalInt(lua_State* L, const char* name) {
        lua_getglobal(L, name);
        const int v = sc<int>(lua_tointeger(L, -1));
        lua_pop(L, 1);
        return v;
    }

    bool getGlobalBool(lua_State* L, const char* name) {
        lua_getglobal(L, name);
        const bool v = lua_toboolean(L, -1) != 0;
        lua_pop(L, 1);
        return v;
    }

    bool isGlobalNil(lua_State* L, const char* name) {
        lua_getglobal(L, name);
        const bool isnil = lua_isnil(L, -1);
        lua_pop(L, 1);
        return isnil;
    }
}

TEST(ConfigLuaObjects, keybindCanToggleEnabledFromLua) {
    CLuaState  S;
    const auto L = S.get();

    Objects::CLuaKeybind{}.setup(L);

    auto keybind = makeBindPtr();

    Objects::CLuaKeybind::push(L, keybind);
    lua_setglobal(L, "kb");

    ASSERT_EQ(luaL_dostring(L, R"(
        assert(kb:is_enabled() == true)
        kb:set_enabled(false)
        assert(kb:is_enabled() == false)
    )"),
              LUA_OK);

    EXPECT_FALSE(keybind->enabled());
}

TEST(ConfigLuaObjects, workspaceRuleCanToggleEnabledFromLua) {
    CLuaState  S;
    const auto L = S.get();

    Objects::CLuaWorkspaceRule{}.setup(L);

    auto rule = makeShared<Config::CWorkspaceRule>();
    rule->setEnabled(true);

    Objects::CLuaWorkspaceRule::push(L, rule);
    lua_setglobal(L, "rule");

    ASSERT_EQ(luaL_dostring(L, R"(
        assert(rule:is_enabled() == true)
        rule:set_enabled(false)
        assert(rule:is_enabled() == false)
    )"),
              LUA_OK);

    EXPECT_FALSE(rule->isEnabled());
}

TEST(ConfigLuaObjects, keybindExposesMetadataAndRemoveMethods) {
    CScopedKeybindManager MANAGER;
    CLuaState             S;
    const auto            L = S.get();

    Objects::CLuaKeybind{}.setup(L);

    const auto FLAGS = sc<Keybinds::BindFlags>(Keybinds::BIND_FLAG_REPEAT | Keybinds::BIND_FLAG_LOCKED | Keybinds::BIND_FLAG_NON_CONSUMING | Keybinds::BIND_FLAG_DONT_INHIBIT |
                                               Keybinds::BIND_FLAG_DEVICE_INCLUSIVE | Keybinds::BIND_FLAG_ALLOW_INPUT_CAPTURE);
    const auto makeMetadataBind = [&] {
        return makeBind({"SUPER", "Q"}, FLAGS,
                        {
                            .devices = {"kbd-a", "kbd-b"},
                            .metadata =
                                {
                                    .displayKey  = "SUPER + Q",
                                    .description = "Close active window",
                                    .handler     = "exec",
                                    .argument    = "kitty",
                                    .submap      = "default",
                                },
                        });
    };

    const auto keybind = Keybinds::mgr()->addBind(makeMetadataBind());
    const auto other   = Keybinds::mgr()->addBind(makeMetadataBind());

    Objects::CLuaKeybind::push(L, keybind);
    lua_setglobal(L, "kb");

    ASSERT_EQ(luaL_dostring(L, R"(
        assert(kb.enabled == true)
        assert(kb.description == "Close active window")
        assert(kb.display_key == "SUPER + Q")
        assert(kb.submap == "default")
        assert(kb.handler == "exec")
        assert(kb.arg == "kitty")
        assert(kb.modmask ~= nil)
        assert(kb.key == "Q")
        assert(kb.keycode == 0)
        assert(kb.repeating == true)
        assert(kb.locked == true)
        assert(kb.non_consuming == true)
        assert(kb.dont_inhibit == true)
        assert(type(kb.devices) == "table")
        assert(kb.allow_input_capture == true)

        kb:remove()
        kb:unbind()
    )"),
              LUA_OK);

    ASSERT_EQ(Keybinds::mgr()->registry().size(), 1);
    EXPECT_EQ(Keybinds::mgr()->registry().binds().front(), other);
}

TEST(ConfigLuaObjects, keybindExposesKeycodeTrigger) {
    CLuaState  S;
    const auto L = S.get();

    Objects::CLuaKeybind{}.setup(L);

    auto keybind = makeBindPtr({"code:24"});
    Objects::CLuaKeybind::push(L, keybind);
    lua_setglobal(L, "kb");

    ASSERT_EQ(luaL_dostring(L, R"(
        assert(kb.key == "")
        assert(kb.keycode == 24)
    )"),
              LUA_OK);
}

TEST(ConfigLuaObjects, keybindRemovalDoesNotUnrefCallback) {
    CScopedKeybindManager MANAGER;
    CLuaState             S;
    const auto            L = S.get();

    Objects::CLuaKeybind{}.setup(L);

    ASSERT_EQ(luaL_dostring(L, "return function() end"), LUA_OK);
    ASSERT_TRUE(lua_isfunction(L, -1));
    const int  REF = luaL_ref(L, LUA_REGISTRYINDEX);

    const auto keybind = Keybinds::mgr()->addBind(makeBind({"Q"}, 0,
                                                           {
                                                               .metadata =
                                                                   {
                                                                       .handler  = "__lua",
                                                                       .argument = std::to_string(REF),
                                                                   },
                                                           }));
    Objects::CLuaKeybind::push(L, keybind);
    lua_setglobal(L, "kb");

    ASSERT_EQ(luaL_dostring(L, "kb:remove()"), LUA_OK);

    lua_rawgeti(L, LUA_REGISTRYINDEX, REF);
    EXPECT_TRUE(lua_isfunction(L, -1));
    lua_pop(L, 1);
    luaL_unref(L, LUA_REGISTRYINDEX, REF);
}

TEST(ConfigLuaObjects, objectsAreReadOnlyFromLua) {
    CLuaState  S;
    const auto L = S.get();

    Objects::CLuaKeybind{}.setup(L);

    auto keybind = makeBindPtr();
    Objects::CLuaKeybind::push(L, keybind);
    lua_setglobal(L, "kb");

    luaL_dostring(L, "kb.foo = 1");
    luaL_dostring(L, "x = kb.foo");
    EXPECT_TRUE(isGlobalNil(L, "x"));
}

TEST(ConfigLuaObjects, keybindSupportsEqAndToString) {
    CLuaState  S;
    const auto L = S.get();

    Objects::CLuaKeybind{}.setup(L);

    auto keybindA = makeBindPtr();
    auto keybindB = makeBindPtr();

    Objects::CLuaKeybind::push(L, keybindA);
    lua_setglobal(L, "kb1");

    Objects::CLuaKeybind::push(L, keybindA);
    lua_setglobal(L, "kb2");

    Objects::CLuaKeybind::push(L, keybindB);
    lua_setglobal(L, "kb3");

    ASSERT_EQ(luaL_dostring(L, R"(
        eq12  = (kb1 == kb2)
        neq13 = (kb1 ~= kb3)

        local s = tostring(kb1)
        hasPrefix = type(s) == "string" and string.sub(s, 1, 11) == "HL.Keybind("
    )"),
              LUA_OK);

    EXPECT_TRUE(getGlobalBool(L, "eq12"));
    EXPECT_TRUE(getGlobalBool(L, "neq13"));
    EXPECT_TRUE(getGlobalBool(L, "hasPrefix"));
}

TEST(ConfigLuaObjects, eventSubscriptionRemoveAndIsActive) {
    CLuaState  S;
    const auto L = S.get();

    Objects::CLuaEventSubscription{}.setup(L);
    CLuaEventHandler handler(L);

    ASSERT_EQ(luaL_dostring(L, R"(
        count = 0
        function onReload()
            count = count + 1
        end
    )"),
              LUA_OK);

    lua_getglobal(L, "onReload");
    ASSERT_TRUE(lua_isfunction(L, -1));
    const int  ref = luaL_ref(L, LUA_REGISTRYINDEX);

    const auto handle = handler.registerEvent("config.reloaded", ref);
    ASSERT_TRUE(handle.has_value());

    Objects::CLuaEventSubscription::push(L, &handler, *handle);
    lua_setglobal(L, "sub");

    Event::bus()->m_events.config.reloaded.emit();
    EXPECT_EQ(getGlobalInt(L, "count"), 1);

    ASSERT_EQ(luaL_dostring(L, R"(
        assert(sub:is_active() == true)
        sub:remove()
        assert(sub:is_active() == false)
        sub:remove()
    )"),
              LUA_OK);

    Event::bus()->m_events.config.reloaded.emit();
    EXPECT_EQ(getGlobalInt(L, "count"), 1);
}

TEST(ConfigLuaObjects, eventSubscriptionSupportsEqAndToString) {
    CLuaState  S;
    const auto L = S.get();

    Objects::CLuaEventSubscription{}.setup(L);
    CLuaEventHandler handler(L);

    ASSERT_EQ(luaL_dostring(L, R"(
        function cbA() end
        function cbB() end
    )"),
              LUA_OK);

    lua_getglobal(L, "cbA");
    ASSERT_TRUE(lua_isfunction(L, -1));
    const int refA = luaL_ref(L, LUA_REGISTRYINDEX);

    lua_getglobal(L, "cbB");
    ASSERT_TRUE(lua_isfunction(L, -1));
    const int  refB = luaL_ref(L, LUA_REGISTRYINDEX);

    const auto handleA = handler.registerEvent("config.reloaded", refA);
    const auto handleB = handler.registerEvent("config.reloaded", refB);
    ASSERT_TRUE(handleA.has_value());
    ASSERT_TRUE(handleB.has_value());

    Objects::CLuaEventSubscription::push(L, &handler, *handleA);
    lua_setglobal(L, "sub1");

    Objects::CLuaEventSubscription::push(L, &handler, *handleA);
    lua_setglobal(L, "sub2");

    Objects::CLuaEventSubscription::push(L, &handler, *handleB);
    lua_setglobal(L, "sub3");

    ASSERT_EQ(luaL_dostring(L, R"(
        eq12  = (sub1 == sub2)
        neq13 = (sub1 ~= sub3)

        local s = tostring(sub1)
        hasPrefix = type(s) == "string" and string.sub(s, 1, 21) == "HL.EventSubscription("
    )"),
              LUA_OK);

    EXPECT_TRUE(getGlobalBool(L, "eq12"));
    EXPECT_TRUE(getGlobalBool(L, "neq13"));
    EXPECT_TRUE(getGlobalBool(L, "hasPrefix"));
}
