// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include <QTest>
#include <QDebug>
#include <QTestEventLoop>
#include <QCoreApplication>
#include <QMetaType>
#include <QRegularExpression>
#include <QVariant>
#include <QVersionNumber>
#include <QProcess>
#include <QTimer>
#include <QDBusInterface>
#include <QDBusConnectionInterface>
#include <QDBusVirtualObject>

#include <private/qdbus_symbols_p.h>

#include "../qdbusmarshall/common.h"
#include "myobject.h"

using namespace Qt::StringLiterals;

#define TEST_INTERFACE_NAME "org.qtproject.QtDBus.MyObject"
#define TEST_SIGNAL_NAME "somethingHappened"

static const char serviceName[] = "org.qtproject.autotests.dbusinterface.qmyserver";
static const char objectPath[] = "/org/qtproject/dbusinterface/qmyserver";
static const char *interfaceName = serviceName;

int MyObject::callCount = 0;
QVariantList MyObject::callArgs;

class MyObjectUnknownType: public QObject
{
    Q_OBJECT
    Q_CLASSINFO("D-Bus Interface", "org.qtproject.QtDBus.MyObject")
    Q_CLASSINFO("D-Bus Introspection", ""
"  <interface name=\"org.qtproject.QtDBus.MyObjectUnknownTypes\" >\n"
"    <property access=\"readwrite\" type=\"~\" name=\"prop1\" />\n"
"    <signal name=\"somethingHappened\" >\n"
"      <arg direction=\"out\" type=\"~\" />\n"
"    </signal>\n"
"    <method name=\"ping\" >\n"
"      <arg direction=\"in\" type=\"~\" name=\"ping\" />\n"
"      <arg direction=\"out\" type=\"~\" name=\"ping\" />\n"
"    </method>\n"
"    <method name=\"regularMethod\" />\n"
"  </interface>\n"
                "")
};

class Spy: public QObject
{
    Q_OBJECT
public:
    QString received;
    int count;

    Spy() : count(0)
    { }

public slots:
    void spySlot(const QString& arg)
    {
        received = arg;
        ++count;
    }
};

class DerivedFromQDBusInterface: public QDBusInterface
{
    Q_OBJECT
public:
    DerivedFromQDBusInterface()
        : QDBusInterface("com.example.Test", "/")
    {}

public slots:
    void method() {}
};

// helper function
void emitSignal(const QString &interface, const QString &name, const QString &arg)
{
    QDBusMessage msg = QDBusMessage::createSignal("/", interface, name);
    msg << arg;
    QDBusConnection::sessionBus().send(msg);

    QTest::qWait(1000);
}

void emitSignalPeer(const QString &interface, const QString &name, const QString &arg)
{
    QDBusMessage req = QDBusMessage::createMethodCall(serviceName, objectPath, interfaceName, "emitSignal");
    req << interface;
    req << name;
    req << arg;
    QDBusConnection::sessionBus().send(req);

    QTest::qWait(1000);
}

int callCountPeer()
{
    QDBusMessage req = QDBusMessage::createMethodCall(serviceName, objectPath, interfaceName, "callCount");
    QDBusMessage reply = QDBusConnection::sessionBus().call(req);
    return reply.arguments().at(0).toInt();
}

QVariantList callArgsPeer()
{
    QDBusMessage req = QDBusMessage::createMethodCall(serviceName, objectPath, interfaceName, "callArgs");
    QDBusMessage reply = QDBusConnection::sessionBus().call(req);
    return qdbus_cast<QVariantList>(reply.arguments().at(0));
}

void setProp1Peer(int val)
{
    QDBusMessage req = QDBusMessage::createMethodCall(serviceName, objectPath, interfaceName, "setProp1");
    req << val;
    QDBusMessage reply = QDBusConnection::sessionBus().call(req);
}

int prop1Peer()
{
    QDBusMessage req = QDBusMessage::createMethodCall(serviceName, objectPath, interfaceName, "prop1");
    QDBusMessage reply = QDBusConnection::sessionBus().call(req);
    return reply.arguments().at(0).toInt();
}

void setComplexPropPeer(QList<int> val)
{
    QDBusMessage req = QDBusMessage::createMethodCall(serviceName, objectPath, interfaceName, "setComplexProp");
    req << QVariant::fromValue(val);
    QDBusMessage reply = QDBusConnection::sessionBus().call(req);
}

QList<int> complexPropPeer()
{
    QDBusMessage req = QDBusMessage::createMethodCall(serviceName, objectPath, interfaceName, "complexProp");
    QDBusMessage reply = QDBusConnection::sessionBus().call(req);
    return qdbus_cast<QList<int> >(reply.arguments().at(0));
}

void resetPeer()
{
    QDBusMessage req = QDBusMessage::createMethodCall(serviceName, objectPath, interfaceName, "reset");
    QDBusConnection::sessionBus().call(req);
}

class tst_QDBusInterface: public QObject
{
    Q_OBJECT
    MyObject obj;

public slots:
    void testServiceOwnerChanged(const QString &service)
    {
        if (service == "com.example.Test")
            QTestEventLoop::instance().exitLoop();
    }

private slots:
    void initTestCase();
    void cleanupTestCase();

    void notConnected();
    void notValid();
    void notValidDerived();
    void invalidAfterServiceOwnerChanged();
    void introspect();
    void introspectUnknownTypes();
    void introspectVirtualObject();
    void callMethod();
    void invokeMethod();
    void invokeMethodWithReturn();
    void invokeMethodWithMultiReturn();
    void invokeMethodWithComplexReturn();

    void introspectPeer();
    void callMethodPeer();
    void invokeMethodPeer();
    void invokeMethodWithReturnPeer();
    void invokeMethodWithMultiReturnPeer();
    void invokeMethodWithComplexReturnPeer();

    void signal();
    void signalPeer();

    void propertyRead();
    void propertyWrite();
    void complexPropertyRead();
    void complexPropertyWrite();

    void delayedPropertyReadPeer();

    void propertyReadPeer();
    void propertyWritePeer();
    void complexPropertyReadPeer();
    void complexPropertyWritePeer();

    void interactiveAuthorizationRequired();

    void interfaceNameFallback_data();
    void interfaceNameFallback();

private:
    QProcess proc;
};

class WaitForQMyServer: public QObject
{
    Q_OBJECT
public:
    WaitForQMyServer();
    bool ok();
public Q_SLOTS:
    void ownerChange(const QString &name)
    {
        if (name == serviceName)
            loop.quit();
    }

private:
    QEventLoop loop;
};

WaitForQMyServer::WaitForQMyServer()
{
    QDBusConnection con = QDBusConnection::sessionBus();
    if (!ok()) {
        connect(con.interface(), SIGNAL(serviceOwnerChanged(QString,QString,QString)),
                SLOT(ownerChange(QString)));
        QTimer::singleShot(2000, &loop, SLOT(quit()));
        loop.exec();
    }
}

bool WaitForQMyServer::ok()
{
    return QDBusConnection::sessionBus().isConnected() &&
        QDBusConnection::sessionBus().interface()->isServiceRegistered(serviceName);
}

void tst_QDBusInterface::initTestCase()
{
    QDBusConnection con = QDBusConnection::sessionBus();
    QVERIFY(con.isConnected());
    QTest::qWait(500);

    con.registerObject("/", &obj, QDBusConnection::ExportAllProperties
                       | QDBusConnection::ExportAllSlots
                       | QDBusConnection::ExportAllInvokables);

#ifdef Q_OS_WIN
#  define EXE ".exe"
#else
#  define EXE ""
#endif
    proc.setProcessChannelMode(QProcess::ForwardedErrorChannel);
    proc.start(QFINDTESTDATA("qmyserver/qmyserver_qdbusinterface" EXE)); // FIXME CMake: This is most probably wrong now since the binary ends up in bin/ not in the build tree
    QVERIFY2(proc.waitForStarted(), qPrintable(proc.errorString()));
    QVERIFY(proc.waitForReadyRead());

    WaitForQMyServer w;
    QVERIFY(w.ok());
    //QTest::qWait(2000);

    // get peer server address
    QDBusMessage req = QDBusMessage::createMethodCall(serviceName, objectPath, interfaceName, "address");
    QDBusMessage rpl = con.call(req);
    QCOMPARE(rpl.type(), QDBusMessage::ReplyMessage);
    QString address = rpl.arguments().at(0).toString();

    // connect to peer server
    QDBusConnection peercon = QDBusConnection::connectToPeer(address, "peer");
    QVERIFY(peercon.isConnected());

    QDBusMessage req2 = QDBusMessage::createMethodCall(serviceName, objectPath, interfaceName, "waitForConnected");
    QDBusMessage rpl2 = con.call(req2);
    QCOMPARE(rpl2.type(), QDBusMessage::ReplyMessage);
    QVERIFY2(rpl2.type() == QDBusMessage::ReplyMessage, rpl2.errorMessage().toLatin1());
}

void tst_QDBusInterface::cleanupTestCase()
{
    QDBusMessage msg = QDBusMessage::createMethodCall(serviceName, objectPath, interfaceName, "quit");
    QDBusConnection::sessionBus().call(msg);
    proc.waitForFinished(200);
    proc.close();
}

void tst_QDBusInterface::notConnected()
{
    QDBusConnection connection("");
    QVERIFY(!connection.isConnected());

    QDBusInterface interface("org.freedesktop.DBus", "/", "org.freedesktop.DBus",
                             connection);

    QVERIFY(!interface.isValid());
    QVERIFY(!QMetaObject::invokeMethod(&interface, "ListNames", Qt::DirectConnection));
}

void tst_QDBusInterface::notValid()
{
    QDBusConnection connection("");
    QVERIFY(!connection.isConnected());

    QDBusInterface interface("com.example.Test", QString(), "org.example.Test",
                             connection);

    QVERIFY(!interface.isValid());
    QVERIFY(!QMetaObject::invokeMethod(&interface, "ListNames", Qt::DirectConnection));

    // With a connection, but empty/null service and path specified
    QDBusConnection con = QDBusConnection::sessionBus();
    QVERIFY(con.isConnected());
    QDBusInterface iface({}, {}, {}, con);
    QVERIFY(!iface.isValid());
    QVERIFY(!QMetaObject::invokeMethod(&interface, "ListNames", Qt::DirectConnection));
}

void tst_QDBusInterface::notValidDerived()
{
    DerivedFromQDBusInterface c;
    QVERIFY(!c.isValid());
    QMetaObject::invokeMethod(&c, "method", Qt::DirectConnection);
}

void tst_QDBusInterface::invalidAfterServiceOwnerChanged()
{
    // this test is technically the same as tst_QDBusAbstractInterface::followSignal
    QDBusConnection conn = QDBusConnection::sessionBus();
    QDBusConnectionInterface *connIface = conn.interface();

    QDBusInterface validInterface(conn.baseService(), "/");
    QVERIFY(validInterface.isValid());
    QDBusInterface invalidInterface("com.example.Test", "/");
    QVERIFY(!invalidInterface.isValid());

    QTestEventLoop::instance().connect(connIface, SIGNAL(serviceOwnerChanged(QString,QString,QString)),
                                       SLOT(exitLoop()));
    QCOMPARE(connIface->registerService("com.example.Test").value(), QDBusConnectionInterface::ServiceRegistered);

    QTestEventLoop::instance().enterLoop(5);

    QVERIFY(!QTestEventLoop::instance().timeout());
    QVERIFY(invalidInterface.isValid());
}

void tst_QDBusInterface::introspect()
{
    QDBusInterface iface(QDBusConnection::sessionBus().baseService(), QLatin1String("/"),
                         TEST_INTERFACE_NAME);

    const QMetaObject *mo = iface.metaObject();

    QCOMPARE(mo->methodCount() - mo->methodOffset(), 7);
    QVERIFY(mo->indexOfSignal(TEST_SIGNAL_NAME "(QString)") != -1);

    QCOMPARE(mo->propertyCount() - mo->propertyOffset(), 3);
    QVERIFY(mo->indexOfProperty("prop1") != -1);
    QVERIFY(mo->indexOfProperty("complexProp") != -1);
}

void tst_QDBusInterface::introspectUnknownTypes()
{
    QDBusConnection con = QDBusConnection::sessionBus();
    MyObjectUnknownType obj;
    con.registerObject("/unknownTypes", &obj, QDBusConnection::ExportAllContents);
    QDBusInterface iface(QDBusConnection::sessionBus().baseService(), QLatin1String("/unknownTypes"),
                         "org.qtproject.QtDBus.MyObjectUnknownTypes");

    const QMetaObject *mo = iface.metaObject();
    QVERIFY(mo->indexOfMethod("regularMethod()") != -1); // this is the control
    QVERIFY(mo->indexOfMethod("somethingHappened(QDBusRawType<0x7e>*)") != -1);

    QVERIFY(mo->indexOfMethod("ping(QDBusRawType<0x7e>*)") != -1);
    int midx = mo->indexOfMethod("ping(QDBusRawType<0x7e>*)");
    QCOMPARE(mo->method(midx).typeName(), "QDBusRawType<0x7e>*");

    QVERIFY(mo->indexOfProperty("prop1") != -1);
    int pidx = mo->indexOfProperty("prop1");
    QCOMPARE(mo->property(pidx).typeName(), "QDBusRawType<0x7e>*");



    QDBusMessage message = QDBusMessage::createMethodCall(con.baseService(), "/unknownTypes", "org.freedesktop.DBus.Introspectable", "Introspect");
    QDBusMessage reply = con.call(message, QDBus::Block, 5000);
    qDebug() << "REPL: " << reply.arguments();

}


class VirtualObject: public QDBusVirtualObject
{
    Q_OBJECT
public:
    VirtualObject() :success(true) {}

    QString introspect(const QString &path) const override
    {
        Q_ASSERT(QThread::currentThread() == thread());
        if (path == "/some/path/superNode")
            return "zitroneneis";
        if (path == "/some/path/superNode/foo")
            return  "  <interface name=\"org.qtproject.QtDBus.VirtualObject\">\n"
                    "    <method name=\"klingeling\" />\n"
                    "  </interface>\n" ;
        return QString();
    }

    bool handleMessage(const QDBusMessage &message, const QDBusConnection &connection) override
    {
        Q_ASSERT(QThread::currentThread() == thread());
        ++callCount;
        lastMessage = message;

        if (success) {
            QDBusMessage reply = message.createReply(replyArguments);
            connection.send(reply);
        }
        emit messageReceived(message);
        return success;
    }
signals:
    void messageReceived(const QDBusMessage &message) const;

public:
    mutable QDBusMessage lastMessage;
    QVariantList replyArguments;
    mutable int callCount;
    bool success;
};

void tst_QDBusInterface::introspectVirtualObject()
{
    QDBusConnection con = QDBusConnection::sessionBus();
    QVERIFY(con.isConnected());
    VirtualObject obj;

    obj.success = false;

    QString path = "/some/path/superNode";
    QVERIFY(con.registerVirtualObject(path, &obj, QDBusConnection::SubPath));

    QDBusMessage message = QDBusMessage::createMethodCall(con.baseService(), path, "org.freedesktop.DBus.Introspectable", "Introspect");
    QDBusMessage reply = con.call(message, QDBus::Block, 5000);
    QVERIFY(reply.arguments().at(0).toString().contains(
            QRegularExpression("<node>.*zitroneneis.*<interface name=",
                               QRegularExpression::DotMatchesEverythingOption)));

    QDBusMessage message2 = QDBusMessage::createMethodCall(con.baseService(), path + "/foo", "org.freedesktop.DBus.Introspectable", "Introspect");
    QDBusMessage reply2 = con.call(message2, QDBus::Block, 5000);
    QVERIFY(reply2.arguments().at(0).toString().contains(
            QRegularExpression("<node>.*<interface name=\"org.qtproject.QtDBus.VirtualObject\">"
                               ".*<method name=\"klingeling\" />\n"
                               ".*</interface>.*<interface name=",
                               QRegularExpression::DotMatchesEverythingOption)));
}

void tst_QDBusInterface::callMethod()
{
    QDBusConnection con = QDBusConnection::sessionBus();
    QDBusInterface iface(QDBusConnection::sessionBus().baseService(), QLatin1String("/"),
                         TEST_INTERFACE_NAME);

    MyObject::callCount = 0;

    // call a SLOT method
    QDBusMessage reply = iface.call("ping", QVariant::fromValue(QDBusVariant("foo")));
    QCOMPARE(MyObject::callCount, 1);
    QCOMPARE(reply.type(), QDBusMessage::ReplyMessage);

    // verify what the callee received
    QCOMPARE(MyObject::callArgs.size(), 1);
    QVariant v = MyObject::callArgs.at(0);
    QDBusVariant dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), QString("foo"));

    // verify reply
    QCOMPARE(reply.arguments().size(), 1);
    v = reply.arguments().at(0);
    dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), QString("foo"));

    // call an INVOKABLE method
    reply = iface.call("ping_invokable", QVariant::fromValue(QDBusVariant("bar")));
    QCOMPARE(MyObject::callCount, 2);
    QCOMPARE(reply.type(), QDBusMessage::ReplyMessage);

    // verify what the callee received
    QCOMPARE(MyObject::callArgs.size(), 1);
    v = MyObject::callArgs.at(0);
    dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), QString("bar"));

    // verify reply
    QCOMPARE(reply.arguments().size(), 1);
    v = reply.arguments().at(0);
    dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), QString("bar"));
}

void tst_QDBusInterface::invokeMethod()
{
    QDBusConnection con = QDBusConnection::sessionBus();
    QDBusInterface iface(QDBusConnection::sessionBus().baseService(), QLatin1String("/"),
                         TEST_INTERFACE_NAME);

    MyObject::callCount = 0;

    // make the SLOT call without a return type
    QDBusVariant arg("foo");
    QVERIFY(QMetaObject::invokeMethod(&iface, "ping", Q_ARG(QDBusVariant, arg)));
    QCOMPARE(MyObject::callCount, 1);

    // verify what the callee received
    QCOMPARE(MyObject::callArgs.size(), 1);
    QVariant v = MyObject::callArgs.at(0);
    QDBusVariant dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), QString("foo"));

    // make the INVOKABLE call without a return type
    QDBusVariant arg2("bar");
    QVERIFY(QMetaObject::invokeMethod(&iface, "ping_invokable", Q_ARG(QDBusVariant, arg2)));
    QCOMPARE(MyObject::callCount, 2);

    // verify what the callee received
    QCOMPARE(MyObject::callArgs.size(), 1);
    v = MyObject::callArgs.at(0);
    dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), QString("bar"));
}

void tst_QDBusInterface::invokeMethodWithReturn()
{
    QDBusConnection con = QDBusConnection::sessionBus();
    QDBusInterface iface(QDBusConnection::sessionBus().baseService(), QLatin1String("/"),
                         TEST_INTERFACE_NAME);

    MyObject::callCount = 0;
    QDBusVariant retArg;

    // make the SLOT call without a return type
    QDBusVariant arg("foo");
    QVERIFY(QMetaObject::invokeMethod(&iface, "ping", Q_RETURN_ARG(QDBusVariant, retArg), Q_ARG(QDBusVariant, arg)));
    QCOMPARE(MyObject::callCount, 1);

    // verify what the callee received
    QCOMPARE(MyObject::callArgs.size(), 1);
    QVariant v = MyObject::callArgs.at(0);
    QDBusVariant dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), arg.variant().toString());

    // verify that we got the reply as expected
    QCOMPARE(retArg.variant(), arg.variant());

    // make the INVOKABLE call without a return type
    QDBusVariant arg2("bar");
    QVERIFY(QMetaObject::invokeMethod(&iface, "ping_invokable", Q_RETURN_ARG(QDBusVariant, retArg), Q_ARG(QDBusVariant, arg2)));
    QCOMPARE(MyObject::callCount, 2);

    // verify what the callee received
    QCOMPARE(MyObject::callArgs.size(), 1);
    v = MyObject::callArgs.at(0);
    dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), arg2.variant().toString());

    // verify that we got the reply as expected
    QCOMPARE(retArg.variant(), arg2.variant());
}

void tst_QDBusInterface::invokeMethodWithMultiReturn()
{
    QDBusConnection con = QDBusConnection::sessionBus();
    QDBusInterface iface(QDBusConnection::sessionBus().baseService(), QLatin1String("/"),
                         TEST_INTERFACE_NAME);

    MyObject::callCount = 0;
    QDBusVariant retArg, retArg2;

    // make the SLOT call without a return type
    QDBusVariant arg("foo"), arg2("bar");
    QVERIFY(QMetaObject::invokeMethod(&iface, "ping",
                                      Q_RETURN_ARG(QDBusVariant, retArg),
                                      Q_ARG(QDBusVariant, arg),
                                      Q_ARG(QDBusVariant, arg2),
                                      Q_ARG(QDBusVariant&, retArg2)));
    QCOMPARE(MyObject::callCount, 1);

    // verify what the callee received
    QCOMPARE(MyObject::callArgs.size(), 2);
    QVariant v = MyObject::callArgs.at(0);
    QDBusVariant dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), arg.variant().toString());

    v = MyObject::callArgs.at(1);
    dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), arg2.variant().toString());

    // verify that we got the replies as expected
    QCOMPARE(retArg.variant(), arg.variant());
    QCOMPARE(retArg2.variant(), arg2.variant());

    // make the INVOKABLE call without a return type
    QDBusVariant arg3("hello"), arg4("world");
    QVERIFY(QMetaObject::invokeMethod(&iface, "ping_invokable",
                                      Q_RETURN_ARG(QDBusVariant, retArg),
                                      Q_ARG(QDBusVariant, arg3),
                                      Q_ARG(QDBusVariant, arg4),
                                      Q_ARG(QDBusVariant&, retArg2)));
    QCOMPARE(MyObject::callCount, 2);

    // verify what the callee received
    QCOMPARE(MyObject::callArgs.size(), 2);
    v = MyObject::callArgs.at(0);
    dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), arg3.variant().toString());

    v = MyObject::callArgs.at(1);
    dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), arg4.variant().toString());

    // verify that we got the replies as expected
    QCOMPARE(retArg.variant(), arg3.variant());
    QCOMPARE(retArg2.variant(), arg4.variant());
}

void tst_QDBusInterface::invokeMethodWithComplexReturn()
{
    QDBusConnection con = QDBusConnection::sessionBus();
    QDBusInterface iface(QDBusConnection::sessionBus().baseService(), QLatin1String("/"),
                         TEST_INTERFACE_NAME);

    MyObject::callCount = 0;
    QList<int> retArg;

    // make the SLOT call without a return type
    QList<int> arg = QList<int>() << 42 << -47;
    QVERIFY(QMetaObject::invokeMethod(&iface, "ping", Q_RETURN_ARG(QList<int>, retArg), Q_ARG(QList<int>, arg)));
    QCOMPARE(MyObject::callCount, 1);

    // verify what the callee received
    QCOMPARE(MyObject::callArgs.size(), 1);
    QVariant v = MyObject::callArgs.at(0);
    QCOMPARE(v.userType(), qMetaTypeId<QDBusArgument>());
    QCOMPARE(qdbus_cast<QList<int> >(v), arg);

    // verify that we got the reply as expected
    QCOMPARE(retArg, arg);

    // make the INVOKABLE call without a return type
    QList<int> arg2 = QList<int>() << 24 << -74;
    QVERIFY(QMetaObject::invokeMethod(&iface, "ping", Q_RETURN_ARG(QList<int>, retArg), Q_ARG(QList<int>, arg2)));
    QCOMPARE(MyObject::callCount, 2);

    // verify what the callee received
    QCOMPARE(MyObject::callArgs.size(), 1);
    v = MyObject::callArgs.at(0);
    QCOMPARE(v.userType(), qMetaTypeId<QDBusArgument>());
    QCOMPARE(qdbus_cast<QList<int> >(v), arg2);

    // verify that we got the reply as expected
    QCOMPARE(retArg, arg2);
}

void tst_QDBusInterface::introspectPeer()
{
    QDBusConnection con("peer");
    QDBusInterface iface(QString(), QLatin1String("/"),
                         TEST_INTERFACE_NAME, con);

    const QMetaObject *mo = iface.metaObject();

    QCOMPARE(mo->methodCount() - mo->methodOffset(), 7);
    QVERIFY(mo->indexOfSignal(TEST_SIGNAL_NAME "(QString)") != -1);

    QCOMPARE(mo->propertyCount() - mo->propertyOffset(), 3);
    QVERIFY(mo->indexOfProperty("prop1") != -1);
    QVERIFY(mo->indexOfProperty("complexProp") != -1);
}

void tst_QDBusInterface::callMethodPeer()
{
    QDBusConnection con("peer");
    QDBusInterface iface(QString(), QLatin1String("/"),
                         TEST_INTERFACE_NAME, con);

    resetPeer();

    // call a SLOT method
    QDBusMessage reply = iface.call("ping", QVariant::fromValue(QDBusVariant("foo")));
    QCOMPARE(callCountPeer(), 1);
    QCOMPARE(reply.type(), QDBusMessage::ReplyMessage);

    // verify what the callee received
    QVariantList callArgs = callArgsPeer();
    QCOMPARE(callArgs.size(), 1);
    QVariant v = callArgs.at(0);
    QDBusVariant dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), QString("foo"));

    // verify reply
    QCOMPARE(reply.arguments().size(), 1);
    v = reply.arguments().at(0);
    dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), QString("foo"));

    // call an INVOKABLE method
    reply = iface.call("ping_invokable", QVariant::fromValue(QDBusVariant("bar")));
    QCOMPARE(callCountPeer(), 2);
    QCOMPARE(reply.type(), QDBusMessage::ReplyMessage);

    // verify what the callee received
    callArgs = callArgsPeer();
    QCOMPARE(callArgs.size(), 1);
    v = callArgs.at(0);
    dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), QString("bar"));

    // verify reply
    QCOMPARE(reply.arguments().size(), 1);
    v = reply.arguments().at(0);
    dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), QString("bar"));
}

void tst_QDBusInterface::invokeMethodPeer()
{
    QDBusConnection con("peer");
    QDBusInterface iface(QString(), QLatin1String("/"),
                         TEST_INTERFACE_NAME, con);

    resetPeer();

    // make the SLOT call without a return type
    QDBusVariant arg("foo");
    QVERIFY(QMetaObject::invokeMethod(&iface, "ping", Q_ARG(QDBusVariant, arg)));
    QCOMPARE(callCountPeer(), 1);

    // verify what the callee received
    QVariantList callArgs = callArgsPeer();
    QCOMPARE(callArgs.size(), 1);
    QVariant v = callArgs.at(0);
    QDBusVariant dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), QString("foo"));

    // make the INVOKABLE call without a return type
    QDBusVariant arg2("bar");
    QVERIFY(QMetaObject::invokeMethod(&iface, "ping_invokable", Q_ARG(QDBusVariant, arg2)));
    QCOMPARE(callCountPeer(), 2);

    // verify what the callee received
    callArgs = callArgsPeer();
    QCOMPARE(callArgs.size(), 1);
    v = callArgs.at(0);
    dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), QString("bar"));
}

void tst_QDBusInterface::invokeMethodWithReturnPeer()
{
    QDBusConnection con("peer");
    QDBusInterface iface(QString(), QLatin1String("/"),
                         TEST_INTERFACE_NAME, con);

    resetPeer();
    QDBusVariant retArg;

    // make the SLOT call without a return type
    QDBusVariant arg("foo");
    QVERIFY(QMetaObject::invokeMethod(&iface, "ping", Q_RETURN_ARG(QDBusVariant, retArg), Q_ARG(QDBusVariant, arg)));
    QCOMPARE(callCountPeer(), 1);

    // verify what the callee received
    QVariantList callArgs = callArgsPeer();
    QCOMPARE(callArgs.size(), 1);
    QVariant v = callArgs.at(0);
    QDBusVariant dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), arg.variant().toString());

    // verify that we got the reply as expected
    QCOMPARE(retArg.variant(), arg.variant());

    // make the INVOKABLE call without a return type
    QDBusVariant arg2("bar");
    QVERIFY(QMetaObject::invokeMethod(&iface, "ping_invokable", Q_RETURN_ARG(QDBusVariant, retArg), Q_ARG(QDBusVariant, arg2)));
    QCOMPARE(callCountPeer(), 2);

    // verify what the callee received
    callArgs = callArgsPeer();
    QCOMPARE(callArgs.size(), 1);
    v = callArgs.at(0);
    dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), arg2.variant().toString());

    // verify that we got the reply as expected
    QCOMPARE(retArg.variant(), arg2.variant());
}

void tst_QDBusInterface::invokeMethodWithMultiReturnPeer()
{
    QDBusConnection con("peer");
    QDBusInterface iface(QString(), QLatin1String("/"),
                         TEST_INTERFACE_NAME, con);

    resetPeer();
    QDBusVariant retArg, retArg2;

    // make the SLOT call without a return type
    QDBusVariant arg("foo"), arg2("bar");
    QVERIFY(QMetaObject::invokeMethod(&iface, "ping",
                                      Q_RETURN_ARG(QDBusVariant, retArg),
                                      Q_ARG(QDBusVariant, arg),
                                      Q_ARG(QDBusVariant, arg2),
                                      Q_ARG(QDBusVariant&, retArg2)));
    QCOMPARE(callCountPeer(), 1);

    // verify what the callee received
    QVariantList callArgs = callArgsPeer();
    QCOMPARE(callArgs.size(), 2);
    QVariant v = callArgs.at(0);
    QDBusVariant dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), arg.variant().toString());

    v = callArgs.at(1);
    dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), arg2.variant().toString());

    // verify that we got the replies as expected
    QCOMPARE(retArg.variant(), arg.variant());
    QCOMPARE(retArg2.variant(), arg2.variant());

    // make the INVOKABLE call without a return type
    QDBusVariant arg3("hello"), arg4("world");
    QVERIFY(QMetaObject::invokeMethod(&iface, "ping_invokable",
                                      Q_RETURN_ARG(QDBusVariant, retArg),
                                      Q_ARG(QDBusVariant, arg3),
                                      Q_ARG(QDBusVariant, arg4),
                                      Q_ARG(QDBusVariant&, retArg2)));
    QCOMPARE(callCountPeer(), 2);

    // verify what the callee received
    callArgs = callArgsPeer();
    QCOMPARE(callArgs.size(), 2);
    v = callArgs.at(0);
    dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), arg3.variant().toString());

    v = callArgs.at(1);
    dv = qdbus_cast<QDBusVariant>(v);
    QCOMPARE(dv.variant().userType(), QMetaType::QString);
    QCOMPARE(dv.variant().toString(), arg4.variant().toString());

    // verify that we got the replies as expected
    QCOMPARE(retArg.variant(), arg3.variant());
    QCOMPARE(retArg2.variant(), arg4.variant());
}

void tst_QDBusInterface::invokeMethodWithComplexReturnPeer()
{
    QDBusConnection con("peer");
    QDBusInterface iface(QString(), QLatin1String("/"),
                         TEST_INTERFACE_NAME, con);

    resetPeer();
    QList<int> retArg;

    // make the SLOT call without a return type
    QList<int> arg = QList<int>() << 42 << -47;
    QVERIFY(QMetaObject::invokeMethod(&iface, "ping", Q_RETURN_ARG(QList<int>, retArg), Q_ARG(QList<int>, arg)));
    QCOMPARE(callCountPeer(), 1);

    // verify what the callee received
    QVariantList callArgs = callArgsPeer();
    QCOMPARE(callArgs.size(), 1);
    QVariant v = callArgs.at(0);
    QCOMPARE(v.userType(), qMetaTypeId<QDBusArgument>());
    QCOMPARE(qdbus_cast<QList<int> >(v), arg);

    // verify that we got the reply as expected
    QCOMPARE(retArg, arg);

    // make the INVOKABLE call without a return type
    QList<int> arg2 = QList<int>() << 24 << -74;
    QVERIFY(QMetaObject::invokeMethod(&iface, "ping", Q_RETURN_ARG(QList<int>, retArg), Q_ARG(QList<int>, arg2)));
    QCOMPARE(callCountPeer(), 2);

    // verify what the callee received
    callArgs = callArgsPeer();
    QCOMPARE(callArgs.size(), 1);
    v = callArgs.at(0);
    QCOMPARE(v.userType(), qMetaTypeId<QDBusArgument>());
    QCOMPARE(qdbus_cast<QList<int> >(v), arg2);

    // verify that we got the reply as expected
    QCOMPARE(retArg, arg2);
}

void tst_QDBusInterface::signal()
{
    QDBusConnection con = QDBusConnection::sessionBus();
    QDBusInterface iface(QDBusConnection::sessionBus().baseService(), QLatin1String("/"),
                         TEST_INTERFACE_NAME);

    QString arg = "So long and thanks for all the fish";
    {
        Spy spy;
        spy.connect(&iface, SIGNAL(somethingHappened(QString)), SLOT(spySlot(QString)));

        emitSignal(TEST_INTERFACE_NAME, TEST_SIGNAL_NAME, arg);
        QCOMPARE(spy.count, 1);
        QCOMPARE(spy.received, arg);
    }

    QDBusInterface iface2(QDBusConnection::sessionBus().baseService(), QLatin1String("/"),
                          TEST_INTERFACE_NAME);
    {
        Spy spy;
        spy.connect(&iface, SIGNAL(somethingHappened(QString)), SLOT(spySlot(QString)));
        spy.connect(&iface2, SIGNAL(somethingHappened(QString)), SLOT(spySlot(QString)));

        emitSignal(TEST_INTERFACE_NAME, TEST_SIGNAL_NAME, arg);
        QCOMPARE(spy.count, 2);
        QCOMPARE(spy.received, arg);
    }

    {
        Spy spy, spy2;
        spy.connect(&iface, SIGNAL(somethingHappened(QString)), SLOT(spySlot(QString)));
        spy2.connect(&iface2, SIGNAL(somethingHappened(QString)), SLOT(spySlot(QString)));

        emitSignal(TEST_INTERFACE_NAME, TEST_SIGNAL_NAME, arg);
        QCOMPARE(spy.count, 1);
        QCOMPARE(spy.received, arg);
        QCOMPARE(spy2.count, 1);
        QCOMPARE(spy2.received, arg);
    }
}

void tst_QDBusInterface::signalPeer()
{
    QDBusConnection con("peer");
    QDBusInterface iface(QString(), QLatin1String("/"),
                         TEST_INTERFACE_NAME, con);

    QString arg = "So long and thanks for all the fish";
    {
        Spy spy;
        spy.connect(&iface, SIGNAL(somethingHappened(QString)), SLOT(spySlot(QString)));

        emitSignalPeer(TEST_INTERFACE_NAME, TEST_SIGNAL_NAME, arg);
        QCOMPARE(spy.count, 1);
        QCOMPARE(spy.received, arg);
    }

    QDBusInterface iface2(QString(), QLatin1String("/"),
                          TEST_INTERFACE_NAME, con);
    {
        Spy spy;
        spy.connect(&iface, SIGNAL(somethingHappened(QString)), SLOT(spySlot(QString)));
        spy.connect(&iface2, SIGNAL(somethingHappened(QString)), SLOT(spySlot(QString)));

        emitSignalPeer(TEST_INTERFACE_NAME, TEST_SIGNAL_NAME, arg);
        QCOMPARE(spy.count, 2);
        QCOMPARE(spy.received, arg);
    }

    {
        Spy spy, spy2;
        spy.connect(&iface, SIGNAL(somethingHappened(QString)), SLOT(spySlot(QString)));
        spy2.connect(&iface2, SIGNAL(somethingHappened(QString)), SLOT(spySlot(QString)));

        emitSignalPeer(TEST_INTERFACE_NAME, TEST_SIGNAL_NAME, arg);
        QCOMPARE(spy.count, 1);
        QCOMPARE(spy.received, arg);
        QCOMPARE(spy2.count, 1);
        QCOMPARE(spy2.received, arg);
    }
}

void tst_QDBusInterface::propertyRead()
{
    QDBusConnection con = QDBusConnection::sessionBus();
    QDBusInterface iface(QDBusConnection::sessionBus().baseService(), QLatin1String("/"),
                         TEST_INTERFACE_NAME);

    int arg = obj.m_prop1 = 42;
    MyObject::callCount = 0;

    QVariant v = iface.property("prop1");
    QVERIFY(v.isValid());
    QCOMPARE(v.userType(), int(QMetaType::Int));
    QCOMPARE(v.toInt(), arg);
    QCOMPARE(MyObject::callCount, 1);
}

void tst_QDBusInterface::propertyWrite()
{
    QDBusConnection con = QDBusConnection::sessionBus();
    QDBusInterface iface(QDBusConnection::sessionBus().baseService(), QLatin1String("/"),
                         TEST_INTERFACE_NAME);

    int arg = 42;
    obj.m_prop1 = 0;
    MyObject::callCount = 0;

    QVERIFY(iface.setProperty("prop1", arg));
    QCOMPARE(MyObject::callCount, 1);
    QCOMPARE(obj.m_prop1, arg);
}

void tst_QDBusInterface::complexPropertyRead()
{
    QDBusConnection con = QDBusConnection::sessionBus();
    QDBusInterface iface(QDBusConnection::sessionBus().baseService(), QLatin1String("/"),
                         TEST_INTERFACE_NAME);

    QList<int> arg = obj.m_complexProp = QList<int>() << 42 << -47;
    MyObject::callCount = 0;

    QVariant v = iface.property("complexProp");
    QVERIFY(v.isValid());
    QCOMPARE(v.userType(), qMetaTypeId<QList<int> >());
    QCOMPARE(v.value<QList<int> >(), arg);
    QCOMPARE(MyObject::callCount, 1);
}

void tst_QDBusInterface::complexPropertyWrite()
{
    QDBusConnection con = QDBusConnection::sessionBus();
    QDBusInterface iface(QDBusConnection::sessionBus().baseService(), QLatin1String("/"),
                         TEST_INTERFACE_NAME);

    QList<int> arg = QList<int>() << -47 << 42;
    obj.m_complexProp.clear();
    MyObject::callCount = 0;

    QVERIFY(iface.setProperty("complexProp", QVariant::fromValue(arg)));
    QCOMPARE(MyObject::callCount, 1);
    QCOMPARE(obj.m_complexProp, arg);
}

void tst_QDBusInterface::delayedPropertyReadPeer()
{
    QDBusConnection con("peer");
    QDBusInterface iface(QString(), QLatin1String("/"), TEST_INTERFACE_NAME, con);

    resetPeer();

    QVariant v = iface.property("delayedProp");
    QVERIFY(v.isValid());
    QCOMPARE(v.userType(), int(QMetaType::Int));
    QCOMPARE(v.toInt(), 5);
}

void tst_QDBusInterface::propertyReadPeer()
{
    QDBusConnection con("peer");
    QDBusInterface iface(QString(), QLatin1String("/"),
                         TEST_INTERFACE_NAME, con);

    resetPeer();
    int arg = 42;
    setProp1Peer(42);

    QVariant v = iface.property("prop1");
    QVERIFY(v.isValid());
    QCOMPARE(v.userType(), int(QMetaType::Int));
    QCOMPARE(v.toInt(), arg);
    QCOMPARE(callCountPeer(), 1);
}

void tst_QDBusInterface::propertyWritePeer()
{
    QDBusConnection con("peer");
    QDBusInterface iface(QString(), QLatin1String("/"),
                         TEST_INTERFACE_NAME, con);

    resetPeer();
    int arg = 42;
    setProp1Peer(0);

    QVERIFY(iface.setProperty("prop1", arg));
    QCOMPARE(callCountPeer(), 1);
    QCOMPARE(prop1Peer(), arg);
}

void tst_QDBusInterface::complexPropertyReadPeer()
{
    QDBusConnection con("peer");
    QDBusInterface iface(QString(), QLatin1String("/"),
                         TEST_INTERFACE_NAME, con);

    resetPeer();
    QList<int> arg = QList<int>() << 42 << -47;
    setComplexPropPeer(arg);

    QVariant v = iface.property("complexProp");
    QVERIFY(v.isValid());
    QCOMPARE(v.userType(), qMetaTypeId<QList<int> >());
    QCOMPARE(v.value<QList<int> >(), arg);
    QCOMPARE(callCountPeer(), 1);
}

void tst_QDBusInterface::complexPropertyWritePeer()
{
    QDBusConnection con("peer");
    QDBusInterface iface(QString(), QLatin1String("/"),
                         TEST_INTERFACE_NAME, con);

    resetPeer();
    QList<int> arg = QList<int>() << -47 << 42;

    QVERIFY(iface.setProperty("complexProp", QVariant::fromValue(arg)));
    QCOMPARE(callCountPeer(), 1);
    QCOMPARE(complexPropPeer(), arg);
}

void tst_QDBusInterface::interactiveAuthorizationRequired()
{
    int major;
    int minor;
    int micro;
    q_dbus_get_version(&major, &minor, &micro);

    QVersionNumber dbusVersion(major, minor, micro);
    if (dbusVersion < QVersionNumber(1, 9, 2))
        QSKIP("Your DBus library is too old to support interactive authorization");

    QDBusMessage req = QDBusMessage::createMethodCall(serviceName, objectPath, interfaceName, "interactiveAuthorization");
    QDBusMessage reply = QDBusConnection::sessionBus().call(req);

    QCOMPARE(reply.type(), QDBusMessage::ErrorMessage);
    QCOMPARE(reply.errorName(), QStringLiteral("org.freedesktop.DBus.Error.InteractiveAuthorizationRequired"));

    req.setInteractiveAuthorizationAllowed(true);
    reply = QDBusConnection::sessionBus().call(req);

    QCOMPARE(reply.type(), QDBusMessage::ReplyMessage);
    QVERIFY(reply.arguments().at(0).toBool());
}

class TestObject : public QObject
{
    Q_OBJECT
public Q_SLOTS:
    void test() { }
};

void tst_QDBusInterface::interfaceNameFallback_data()
{
    QTest::addColumn<QString>("appName");
    QTest::addColumn<QString>("orgDomain");
    QTest::addColumn<QString>("interfaceName");

    QTest::addRow("empty.empty") << "" << "" << "local.tst_qdbusinterface";
    QTest::addRow("with-domain") << "" << "qt-project.org" << "org.qt_project.tst_qdbusinterface";
    QTest::addRow("numbers") << "prog42" << "7-zip.org" << "org._7_zip.prog42";
    QTest::addRow("non-latin1") << u"\u00e6"_s << u"\u00e5"_s << "xn__5ca.xn__6ca";
}

void tst_QDBusInterface::interfaceNameFallback()
{
    QFETCH(QString, appName);
    QFETCH(QString, orgDomain);
    QFETCH(QString, interfaceName);

    auto app = QCoreApplication::instance();
    auto oldApplicationName = app->applicationName();
    auto oldOrganizationDomain = app->organizationDomain();

    app->setApplicationName(appName);
    app->setOrganizationDomain(orgDomain);
    auto obj = new TestObject;

    auto cleanup = qScopeGuard([&] {
        obj->deleteLater();
        app->setApplicationName(oldApplicationName);
        app->setOrganizationDomain(oldOrganizationDomain);
    });

    auto con = QDBusConnection::sessionBus();
    const QString path = "/interfaceNameFallback"_L1;

    QVERIFY(con.registerObject(path, obj, QDBusConnection::ExportAllContents));

    QDBusInterface interface(con.baseService(), path, "org.freedesktop.DBus.Introspectable"_L1,
                             con);
    auto reply = interface.call("Introspect");

    QCOMPARE(reply.type(), QDBusMessage::ReplyMessage);
    QString tag = u"<interface name=\"%1.TestObject\">"_s.arg(interfaceName);
    auto result = reply.arguments().at(0).toString();
    QVERIFY2(result.contains(tag), qUtf8Printable(u"Tag '%1' not found\n%2"_s.arg(tag, result)));
}

QTEST_MAIN(tst_QDBusInterface)

#include "tst_qdbusinterface.moc"
