/*
 * SPDX-FileCopyrightText: 2016-2016 CSSlayer <wengxt@gmail.com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 */

#include <cstdint>
#include <memory>
#include <string>
#include <thread>
#include <tuple>
#include <utility>
#include <vector>
#include "fcitx-utils/dbus/bus.h"
#include "fcitx-utils/dbus/matchrule.h"
#include "fcitx-utils/dbus/message.h"
#include "fcitx-utils/dbus/objectvtable.h"
#include "fcitx-utils/dbus/variant.h"
#include "fcitx-utils/event.h"
#include "fcitx-utils/eventloopinterface.h"
#include "fcitx-utils/log.h"

using namespace fcitx::dbus;
using namespace fcitx;

namespace {

class TestObject : public ObjectVTable<TestObject> {
    void test1() {}
    std::string test2(int32_t i) {
        FCITX_INFO() << "test2 called";
        return std::to_string(i);
    }
    std::tuple<int32_t, uint32_t> test3(int32_t i) {
        std::vector<DBusStruct<std::string, int>> data;
        data.emplace_back(std::make_tuple(std::to_string(i), i));
        testSignal(data);
        FCITX_INFO() << "test3 called";
        return std::make_tuple(i - 1, i + 1);
    }
    bool testError() {
        FCITX_INFO() << "testError called";
        throw MethodCallError("org.freedesktop.DBus.Error.FileNotFound",
                              "File not found");
    }
    Variant test4(const Variant &v) {
        Variant result;
        auto *msg = currentMessage();
        FCITX_INFO() << "test4 called";
        FCITX_INFO() << v;
        msg->rewind();
        auto type = msg->peekType();
        if (type.first == 'v' && type.second == "i") {
            *msg >> Container(Container::Type::Variant, Signature(type.second));
            int32_t i;
            *msg >> i;
            *msg >> ContainerEnd();
            return Variant(std::to_string(i));
        }
        return Variant();
    }
    std::string
    test5(const std::vector<DictEntry<std::string, std::string>> &entries) {
        for (const auto &entry : entries) {
            if (entry.key() == "a") {
                return entry.value();
            }
        }
        return "";
    }

private:
    int prop2 = 1;
    FCITX_OBJECT_VTABLE_METHOD(test1, "test1", "", "");
    FCITX_OBJECT_VTABLE_METHOD(test2, "test2", "i", "s");
    FCITX_OBJECT_VTABLE_METHOD(test3, "test3", "i", "iu");
    FCITX_OBJECT_VTABLE_METHOD(test4, "test4", "v", "v");
    FCITX_OBJECT_VTABLE_METHOD(test5, "test5", "a{ss}", "s");
    FCITX_OBJECT_VTABLE_METHOD(testError, "testError", "", "b");
    FCITX_OBJECT_VTABLE_SIGNAL(testSignal, "testSignal", "a(si)");
    FCITX_OBJECT_VTABLE_PROPERTY(testProperty, "testProperty", "i",
                                 []() { return 5; });
    FCITX_OBJECT_VTABLE_WRITABLE_PROPERTY(
        testProperty2, "testProperty2", "i", [this]() { return prop2; },
        [this](int32_t v) { prop2 = v; });
};

#define TEST_SERVICE "org.fcitx.Fcitx.TestDBus"
#define TEST_INTERFACE "org.fcitx.Fcitx.TestDBus.Interface"

void client() {
    Bus clientBus(BusType::Session);
    EventLoop loop;
    clientBus.attachEventLoop(&loop);
    std::unique_ptr<Slot> slot(clientBus.addMatch(
        MatchRule(TEST_SERVICE, "", TEST_INTERFACE, "testSignal"),
        [&loop](dbus::Message &message) {
            FCITX_INFO() << "testSignal";
            std::vector<DBusStruct<std::string, int>> data;
            message >> data;
            FCITX_ASSERT(data.size() == 1);
            FCITX_ASSERT(std::get<0>(data[0]) == "2");
            FCITX_ASSERT(std::get<1>(data[0]) == 2);
            FCITX_ASSERT(std::get<std::string>(data[0]) == "2");
            FCITX_ASSERT(std::get<int>(data[0]) == 2);
            loop.exit();
            return false;
        }));
    FCITX_ASSERT(slot);
    std::unique_ptr<EventSourceTime> s(loop.addTimeEvent(
        CLOCK_MONOTONIC, now(CLOCK_MONOTONIC), 0,
        [&clientBus](EventSource *, uint64_t) {
            FCITX_INFO() << "Client sends Introspect";
            auto msg = clientBus.createMethodCall(
                TEST_SERVICE, "/test", "org.freedesktop.DBus.Introspectable",
                "Introspect");
            auto reply = msg.call(0);
            std::string s;
            reply >> s;
            FCITX_INFO() << "Introspect reply: " << s;
            return false;
        }));
    std::unique_ptr<EventSourceTime> s2(loop.addTimeEvent(
        CLOCK_MONOTONIC, now(CLOCK_MONOTONIC) + 100000, 0,
        [&clientBus](EventSource *, uint64_t) {
            FCITX_INFO() << "Client sends test2";
            auto msg = clientBus.createMethodCall(TEST_SERVICE, "/test",
                                                  TEST_INTERFACE, "test2");
            msg << 2;
            auto reply = msg.call(0);
            FCITX_ASSERT(reply.type() == MessageType::Reply);
            FCITX_ASSERT(reply.signature() == "s");
            std::string ret;
            reply >> ret;
            FCITX_INFO() << "test2 reply: " << ret;
            FCITX_ASSERT(ret == "2");
            return false;
        }));
    std::unique_ptr<EventSourceTime> s3(loop.addTimeEvent(
        CLOCK_MONOTONIC, now(CLOCK_MONOTONIC) + 200000, 0,
        [&clientBus](EventSource *, uint64_t) {
            FCITX_INFO() << "Client sends testError";
            auto msg = clientBus.createMethodCall(TEST_SERVICE, "/test",
                                                  TEST_INTERFACE, "testError");
            auto reply = msg.call(10000000);
            FCITX_ASSERT(reply.type() == MessageType::Error);
            FCITX_ASSERT(reply.errorName() ==
                         "org.freedesktop.DBus.Error.FileNotFound")
                << reply.errorMessage();
            FCITX_ASSERT(reply.errorMessage() == "File not found")
                << reply.errorMessage();
            return false;
        }));
    std::unique_ptr<EventSourceTime> s4(loop.addTimeEvent(
        CLOCK_MONOTONIC, now(CLOCK_MONOTONIC) + 300000, 0,
        [&clientBus](EventSource *, uint64_t) {
            FCITX_INFO() << "Client sends test4";
            auto msg = clientBus.createMethodCall(TEST_SERVICE, "/test",
                                                  TEST_INTERFACE, "test4");
            msg << Variant(123);
            auto reply = msg.call(0);
            FCITX_ASSERT(reply.type() == MessageType::Reply);
            reply >> Container(Container::Type::Variant, Signature("s"));
            std::string s;
            reply >> s;
            FCITX_INFO() << s;
            FCITX_ASSERT(s == "123");
            reply >> ContainerEnd();
            return false;
        }));
    std::unique_ptr<EventSourceTime> s5(loop.addTimeEvent(
        CLOCK_MONOTONIC, now(CLOCK_MONOTONIC) + 400000, 0,
        [&clientBus](EventSource *, uint64_t) {
            auto msg = clientBus.createMethodCall(TEST_SERVICE, "/test",
                                                  TEST_INTERFACE, "test5");
            FCITX_INFO() << "Client sends test5";
            std::vector<DictEntry<std::string, std::string>> v;
            v.emplace_back("abc", "def");
            v.emplace_back("a", "defg");

            msg << v;
            FCITX_INFO() << msg.signature();
            auto reply = msg.call(0);
            FCITX_ASSERT(reply.type() == MessageType::Reply);
            std::string s;
            reply >> s;
            FCITX_INFO() << s;
            FCITX_ASSERT(s == "defg");
            return false;
        }));
    std::unique_ptr<EventSourceTime> s6(loop.addTimeEvent(
        CLOCK_MONOTONIC, now(CLOCK_MONOTONIC) + 500000, 0,
        [&clientBus](EventSource *, uint64_t) {
            FCITX_INFO() << "test3";
            auto msg = clientBus.createMethodCall(TEST_SERVICE, "/test",
                                                  TEST_INTERFACE, "test3");
            msg << 2;
            auto reply = msg.call(0);
            FCITX_ASSERT(reply.type() == MessageType::Reply);
            FCITX_ASSERT(reply.signature() == "iu");
            FCITX_STRING_TO_DBUS_TUPLE("iu") ret;
            reply >> ret;
            FCITX_ASSERT(std::get<0>(ret) == 1);
            FCITX_ASSERT(std::get<1>(ret) == 3);
            FCITX_INFO() << "test3 ret";
            return false;
        }));
    std::unique_ptr<EventSourceTime> s7(
        loop.addTimeEvent(CLOCK_MONOTONIC, now(CLOCK_MONOTONIC) + 600000, 0,
                          [&clientBus](EventSource *, uint64_t) {
                              FCITX_INFO() << "testProperty";
                              auto msg = clientBus.createMethodCall(
                                  TEST_SERVICE, "/test",
                                  "org.freedesktop.DBus.Properties", "Get");
                              msg << TEST_INTERFACE << "testProperty";
                              auto reply = msg.call(0);
                              FCITX_ASSERT(reply.type() == MessageType::Reply);
                              FCITX_ASSERT(reply.signature() == "v");
                              dbus::Variant ret;
                              reply >> ret;
                              FCITX_ASSERT(ret.signature() == "i");
                              FCITX_ASSERT(ret.dataAs<int32_t>() == 5);
                              return false;
                          }));
    loop.exec();
}

void test_client() {
    Bus bus(BusType::Session);
    FCITX_ASSERT(bus.isOpen());
    EventLoop loop;
    bus.attachEventLoop(&loop);
    FCITX_ASSERT(&loop == bus.eventLoop());
    FCITX_ASSERT(
        bus.requestName(TEST_SERVICE, {RequestNameFlag::AllowReplacement,
                                       RequestNameFlag::ReplaceExisting}));
    TestObject obj;
    FCITX_ASSERT(bus.addObjectVTable("/test", TEST_INTERFACE, obj));
    std::unique_ptr<EventSourceTime> s(loop.addTimeEvent(
        CLOCK_MONOTONIC, now(CLOCK_MONOTONIC) + 2000000, 0,
        [&bus, &loop](EventSource *, uint64_t) {
            auto msg = bus.createMethodCall(
                "org.freedesktop.DBus", "/org/freedesktop/DBus",
                "org.freedesktop.DBus.Introspectable", "Introspect");
            auto reply = msg.call(0);
            std::string s;
            reply >> s;
            loop.exit();
            return false;
        }));

    std::thread thread(client);

    loop.exec();

    thread.join();
}

void test_rule() {

    {
        MatchRule rule(TEST_SERVICE, "", TEST_INTERFACE, "testSignal");
        FCITX_ASSERT(
            rule.rule() ==
            "type='signal',sender='org.fcitx.Fcitx.TestDBus',interface='org."
            "fcitx.Fcitx.TestDBus.Interface',member='testSignal'")
            << rule.rule();
    }
    {
        MatchRule rule(MessageType::MethodCall, TEST_SERVICE, "",
                       TEST_INTERFACE, "testSignal", "", {"abc"}, true);
        FCITX_ASSERT(rule.rule() ==
                     "type='method_call',sender='org.fcitx.Fcitx.TestDBus',"
                     "path='org.fcitx.Fcitx.TestDBus.Interface',interface='"
                     "testSignal',arg0='abc',eavesdrop='true'")
            << rule.rule();
    }
    {
        MatchRule rule(MessageType::Reply, TEST_SERVICE, "", TEST_INTERFACE,
                       "testSignal", "Test", {"abc"}, true);
        FCITX_ASSERT(rule.rule() ==
                     "type='method_return',sender='org.fcitx.Fcitx.TestDBus',"
                     "path='org.fcitx.Fcitx.TestDBus.Interface',interface='"
                     "testSignal',member='Test',arg0='abc',eavesdrop='true'")
            << rule.rule();
    }
}

void test_delete() {
    std::unique_ptr<Bus> bus = std::make_unique<Bus>(BusType::Session);
    FCITX_ASSERT(bus->isOpen());
    EventLoop loop;
    bus->attachEventLoop(&loop);
    FCITX_ASSERT(&loop == bus->eventLoop());
    auto call = bus->createMethodCall("org.freedesktop.DBus", "/non/existing",
                                      "org.freedesktop.DBus", "BadMethod");
    auto reply = call.callAsync(0, [&](dbus::Message &msg) {
        FCITX_ASSERT(msg.type() == MessageType::Error);
        FCITX_ASSERT(msg.isError());
        bus.reset();
        loop.exit();
        return true;
    });
    loop.exec();
}

#define DEFER_TEST_SERVICE "org.fcitx.Fcitx.TestDBusDefer"
#define DEFER_TEST_INTERFACE "org.fcitx.Fcitx.TestDBusDefer.Interface"

// Test MethodCallDefer: method defers its reply until an async call completes.
class DeferTestObject : public ObjectVTable<DeferTestObject> {
    std::string testDefer() {
        auto slotHolder = std::make_shared<std::unique_ptr<Slot>>();
        auto *bus = this->bus();

        auto msg = bus->createMethodCall(DEFER_TEST_SERVICE, "/defertest",
                                         DEFER_TEST_INTERFACE, "justReturn");
        auto reply = std::make_shared<Message>(currentMessage()->createReply());
        *slotHolder = msg.callAsync(0,
                                    [reply = std::move(reply),
                                     slotHolder](Message &msg) mutable -> bool {
                                        FCITX_ASSERT(*slotHolder);
                                        FCITX_ASSERT(!msg.isError());
                                        std::string returnValue;
                                        msg >> returnValue;
                                        *reply << returnValue;
                                        reply->send();
                                        slotHolder->reset();
                                        return true;
                                    });

        throw MethodCallNoReply();
    }

    std::string justReturn() { return "justReturn"; }

private:
    FCITX_OBJECT_VTABLE_METHOD(testDefer, "testDefer", "", "s");
    FCITX_OBJECT_VTABLE_METHOD(justReturn, "justReturn", "", "s");
};

void test_defer() {
    Bus bus(BusType::Session);
    FCITX_ASSERT(bus.isOpen());
    EventLoop loop;
    bus.attachEventLoop(&loop);
    FCITX_ASSERT(bus.requestName(
        DEFER_TEST_SERVICE,
        {RequestNameFlag::AllowReplacement, RequestNameFlag::ReplaceExisting}));
    DeferTestObject obj;
    FCITX_ASSERT(bus.addObjectVTable("/defertest", DEFER_TEST_INTERFACE, obj));

    auto msg = bus.createMethodCall(DEFER_TEST_SERVICE, "/defertest",
                                    DEFER_TEST_INTERFACE, "testDefer");
    // Use a generous timeout: the server must do an async call first.
    auto slot = msg.callAsync(5000000, [&loop](Message &reply) {
        FCITX_ASSERT(!reply.isError()) << reply.errorName();
        std::string ret;
        reply >> ret;
        FCITX_ASSERT(ret == "justReturn") << ret;
        loop.exit();
        return true;
    });
    loop.exec();
}

} // namespace

int main() {
    test_client();
    test_rule();
    test_delete();
    test_defer();
    return 0;
}
