/****************************************************************************
**
** Copyright (C) 2017 Ford Motor Company
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtRemoteObjects module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "rep_engine_merged.h"
#include "rep_subclass_merged.h"
#include "../shared/model_utilities.h"

#include <QtTest/QtTest>
#include <QRemoteObjectReplica>
#include <QRemoteObjectNode>

const QUrl localHostUrl = QUrl(QLatin1String("local:testHost"));
const QUrl tcpHostUrl = QUrl(QLatin1String("tcp://127.0.0.1:9989"));
const QUrl proxyNodeUrl = QUrl(QLatin1String("tcp://127.0.0.1:12123"));
const QUrl remoteNodeUrl = QUrl(QLatin1String("tcp://127.0.0.1:23234"));
const QUrl registryUrl = QUrl(QLatin1String("local:testRegistry"));
const QUrl proxyHostUrl = QUrl(QLatin1String("local:fromProxy"));

#define SET_NODE_NAME(obj) (obj).setName(QLatin1String(#obj))

class ProxyTest : public QObject
{
    Q_OBJECT
private Q_SLOTS:
    void cleanup()
    {
        // wait for delivery of RemoveObject events to the source
        QTest::qWait(200);
    }

    void testProxy_data();
    void testProxy();
    void testForwardProxy();
    void testReverseProxy();
    // The following should fail to compile, verifying the SourceAPI templates work
    // for subclasses
    /*
    void testSubclass()
    {
        QRemoteObjectHost host(localHostUrl);

        struct invalidchild {
            MyPOD myPOD() { return MyPOD(12, 13.f, QStringLiteral("Yay!")); }
        };
        struct badparent {
            invalidchild *subClass() { return new invalidchild; }
        } parent;
        host.enableRemoting<ParentClassSourceAPI>(&parent);
    }
    */

    void testTopLevelModel();
};

void ProxyTest::testProxy_data()
{
    QTest::addColumn<bool>("sourceApi");
    QTest::addColumn<bool>("useProxy");
    QTest::addColumn<bool>("dynamic");

    QTest::newRow("dynamicApi, no proxy") << false << false << false;
    QTest::newRow("sourceApi, no proxy") << true << false << false;
    QTest::newRow("dynamicApi, with proxy") << false << true << false;
    QTest::newRow("sourceApi, with proxy") << true << true << false;
    QTest::newRow("dynamicApi, no proxy, dynamicRep") << false << false << true;
    QTest::newRow("sourceApi, no proxy, dynamicRep") << true << false << true;
    QTest::newRow("dynamicApi, with proxy, dynamicRep") << false << true << true;
    QTest::newRow("sourceApi, with proxy, dynamicRep") << true << true << true;
}

void ProxyTest::testProxy()
{
    QFETCH(bool, sourceApi);
    QFETCH(bool, useProxy);
    QFETCH(bool, dynamic);

    //Setup Local Registry
    QRemoteObjectRegistryHost registry(registryUrl);
    SET_NODE_NAME(registry);
    //Setup Local Host
    QRemoteObjectHost host(localHostUrl);
    SET_NODE_NAME(host);
    host.setRegistryUrl(registryUrl);
    EngineSimpleSource engine;
    engine.setRpm(1234);
    engine.setType(EngineSimpleSource::Gas);
    if (sourceApi)
        host.enableRemoting<EngineSourceAPI>(&engine);
    else
        host.enableRemoting(&engine);

    QRemoteObjectHost proxyNode;
    SET_NODE_NAME(proxyNode);
    if (useProxy) {
        proxyNode.setHostUrl(tcpHostUrl);
        proxyNode.proxy(registryUrl);
    }

    //Setup Local Replica
    QRemoteObjectNode client;
    SET_NODE_NAME(client);
    if (useProxy)
        client.connectToNode(tcpHostUrl);
    else
        client.setRegistryUrl(registryUrl);

    QScopedPointer<QRemoteObjectReplica> replica;
    if (!dynamic) {
        //QLoggingCategory::setFilterRules("qt.remoteobjects*=true");
        replica.reset(client.acquire<EngineReplica>());
        QVERIFY(replica->waitForSource(1000));

        EngineReplica *rep = qobject_cast<EngineReplica *>(replica.data());

        //Compare Replica to Source
        QCOMPARE(rep->rpm(), engine.rpm());
        QCOMPARE((EngineReplica::EngineType)rep->type(), EngineReplica::Gas);

        //Change Replica and make sure change propagates to source
        QSignalSpy sourceSpy(&engine, SIGNAL(rpmChanged(int)));
        QSignalSpy replicaSpy(rep, SIGNAL(rpmChanged(int)));
        rep->pushRpm(42);
        sourceSpy.wait();
        QCOMPARE(sourceSpy.count(), 1);
        QCOMPARE(engine.rpm(), 42);

        // ... and the change makes it back to the replica
        replicaSpy.wait();
        QCOMPARE(replicaSpy.count(), 1);
        QCOMPARE(rep->rpm(), 42);
    } else {
        replica.reset(client.acquireDynamic(QStringLiteral("Engine")));
        QVERIFY(replica->waitForSource(1000));

        //Compare Replica to Source
        const QMetaObject *metaObject = replica->metaObject();
        const int rpmIndex = metaObject->indexOfProperty("rpm");
        Q_ASSERT(rpmIndex != -1);
        const QMetaProperty rpmMeta =  metaObject->property(rpmIndex);
        QCOMPARE(rpmMeta.read(replica.data()).value<int>(), engine.rpm());
        const int typeIndex = metaObject->indexOfProperty("type");
        Q_ASSERT(typeIndex != -1);
        const QMetaProperty typeMeta =  metaObject->property(typeIndex);
        QCOMPARE(typeMeta.read(replica.data()).value<EngineReplica::EngineType>(), EngineReplica::Gas);

        //Change Replica and make sure change propagates to source
        QSignalSpy sourceSpy(&engine, SIGNAL(rpmChanged(int)));
        QSignalSpy replicaSpy(replica.data(), QByteArray(QByteArrayLiteral("2")+rpmMeta.notifySignal().methodSignature().constData()));

        const int rpmPushIndex = metaObject->indexOfMethod("pushRpm(int)");
        Q_ASSERT(rpmPushIndex != -1);
        QMetaMethod pushMethod = metaObject->method(rpmPushIndex);
        Q_ASSERT(pushMethod.isValid());
        QVERIFY(pushMethod.invoke(replica.data(), Q_ARG(int, 42)));

        sourceSpy.wait();
        QCOMPARE(sourceSpy.count(), 1);
        QCOMPARE(engine.rpm(), 42);

        // ... and the change makes it back to the replica
        replicaSpy.wait();
        QCOMPARE(replicaSpy.count(), 1);
        QCOMPARE(rpmMeta.read(replica.data()).value<int>(), engine.rpm());
    }

    // Make sure disabling the Source cascades the state change
    bool res = host.disableRemoting(&engine);
    Q_ASSERT(res);
    QSignalSpy stateSpy(replica.data(), SIGNAL(stateChanged(QRemoteObjectReplica::State,QRemoteObjectReplica::State)));
    stateSpy.wait();
    QCOMPARE(stateSpy.count(), 1);
    QCOMPARE(replica->state(), QRemoteObjectReplica::Suspect);

    // Now test subclass Source
    ParentClassSimpleSource parent;
    SubClassSimpleSource subclass;
    const MyPOD initialValue(42, 3.14, QStringLiteral("SubClass"));
    subclass.setMyPOD(initialValue);
    QStringListModel model;
    model.setStringList(QStringList() << "Track1" << "Track2" << "Track3");
    parent.setSubClass(&subclass);
    parent.setTracks(&model);
    QCOMPARE(subclass.myPOD(), initialValue);
    if (sourceApi)
        host.enableRemoting<ParentClassSourceAPI>(&parent);
    else
        host.enableRemoting(&parent);
    if (!dynamic) {
        replica.reset(client.acquire<ParentClassReplica>());
        ParentClassReplica *rep = qobject_cast<ParentClassReplica *>(replica.data());
        QSignalSpy tracksSpy(rep->tracks(), &QAbstractItemModelReplica::initialized);
        QVERIFY(replica->waitForSource(1000));
        //QLoggingCategory::setFilterRules("qt.remoteobjects*=false");

        //Compare Replica to Source
        QVERIFY(rep->subClass() != nullptr);
        QCOMPARE(rep->subClass()->myPOD(), parent.subClass()->myPOD());
        QVERIFY(rep->tracks() != nullptr);
        QVERIFY(tracksSpy.count() || tracksSpy.wait());
        // Rep file only uses display role, but proxy doesn't forward that yet
        if (!useProxy)
            QCOMPARE(rep->tracks()->availableRoles(), QVector<int>{Qt::DisplayRole});
        else {
            const auto &availableRolesVec = rep->tracks()->availableRoles();
            QSet<int> availableRoles;
            for (int r : availableRolesVec)
                availableRoles.insert(r);
            const auto &rolesHash = model.roleNames();
            QSet<int> roles;
            for (auto it = rolesHash.cbegin(), end = rolesHash.cend(); it != end; ++it)
                roles.insert(it.key());
            QCOMPARE(availableRoles, roles);
        }
        QSignalSpy dataSpy(rep->tracks(), SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>)));
        QVector<QModelIndex> pending;
        QTest::qWait(100);
        QCOMPARE(rep->tracks()->rowCount(), model.rowCount());
        for (int i = 0; i < rep->tracks()->rowCount(); i++)
        {
            // We haven't received any data yet
            const auto index = rep->tracks()->index(i, 0);
            QCOMPARE(rep->tracks()->data(index), QVariant());
            pending.append(index);
        }
        if (useProxy) { // A first batch of updates will be the empty proxy values
            WaitForDataChanged w(pending, &dataSpy);
            QVERIFY(w.wait());
        }
        WaitForDataChanged w(pending, &dataSpy);
        QVERIFY(w.wait());
        for (int i = 0; i < rep->tracks()->rowCount(); i++)
        {
            QCOMPARE(rep->tracks()->data(rep->tracks()->index(i, 0)), model.data(model.index(i), Qt::DisplayRole));
        }

        //Change SubClass and make sure change propagates
        SubClassSimpleSource updatedSubclass;
        const MyPOD updatedValue(-1, 123.456, QStringLiteral("Updated"));
        updatedSubclass.setMyPOD(updatedValue);
        QSignalSpy replicaSpy(rep, SIGNAL(subClassChanged(SubClassReplica*)));
        parent.setSubClass(&updatedSubclass);
        replicaSpy.wait();
        QCOMPARE(replicaSpy.count(), 1);
        QCOMPARE(rep->subClass()->myPOD(), parent.subClass()->myPOD());
        QCOMPARE(rep->subClass()->myPOD(), updatedValue);
    } else {
        replica.reset(client.acquireDynamic(QStringLiteral("ParentClass")));
        QVERIFY(replica->waitForSource(1000));

        const QMetaObject *metaObject = replica->metaObject();
        // Verify subClass pointer
        const int subclassIndex = metaObject->indexOfProperty("subClass");
        QVERIFY(subclassIndex != -1);
        const QMetaProperty subclassMeta =  metaObject->property(subclassIndex);
        QObject *subclassQObjectPtr = subclassMeta.read(replica.data()).value<QObject *>();
        QVERIFY(subclassQObjectPtr != nullptr);
        QRemoteObjectDynamicReplica *subclassReplica = qobject_cast<QRemoteObjectDynamicReplica *>(subclassQObjectPtr);
        QVERIFY(subclassReplica != nullptr);
        // Verify tracks pointer
        const int tracksIndex = metaObject->indexOfProperty("tracks");
        QVERIFY(tracksIndex != -1);
        const QMetaProperty tracksMeta =  metaObject->property(tracksIndex);
        QObject *tracksQObjectPtr = tracksMeta.read(replica.data()).value<QObject *>();
        QVERIFY(tracksQObjectPtr != nullptr);
        QAbstractItemModelReplica *tracksReplica = qobject_cast<QAbstractItemModelReplica *>(tracksQObjectPtr);
        QVERIFY(tracksReplica != nullptr);

        // Verify subClass data
        const int podIndex = subclassReplica->metaObject()->indexOfProperty("myPOD");
        QVERIFY(podIndex != -1);
        const QMetaProperty podMeta = subclassReplica->metaObject()->property(podIndex);
        MyPOD pod = podMeta.read(subclassReplica).value<MyPOD>();
        QCOMPARE(pod, parent.subClass()->myPOD());

        // Verify tracks data
        // Rep file only uses display role, but proxy doesn't forward that yet
        if (!useProxy)
            QCOMPARE(tracksReplica->availableRoles(), QVector<int>{Qt::DisplayRole});
        else {
            const auto &availableRolesVec = tracksReplica->availableRoles();
            QSet<int> availableRoles;
            for (int r : availableRolesVec)
                availableRoles.insert(r);
            const auto &rolesHash = model.roleNames();
            QSet<int> roles;
            for (auto it = rolesHash.cbegin(), end = rolesHash.cend(); it != end; ++it)
                roles.insert(it.key());
            QCOMPARE(availableRoles, roles);
        }
        QTRY_COMPARE(tracksReplica->isInitialized(), true);
        QSignalSpy dataSpy(tracksReplica, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>)));
        QVector<QModelIndex> pending;
        QTest::qWait(100);
        QCOMPARE(tracksReplica->rowCount(), model.rowCount());
        for (int i = 0; i < tracksReplica->rowCount(); i++)
        {
            // We haven't received any data yet
            const auto index = tracksReplica->index(i, 0);
            QCOMPARE(tracksReplica->data(index), QVariant());
            pending.append(index);
        }
        if (useProxy) { // A first batch of updates will be the empty proxy values
            WaitForDataChanged w(pending, &dataSpy);
            QVERIFY(w.wait());
        }
        WaitForDataChanged w(pending, &dataSpy);
        QVERIFY(w.wait());
        for (int i = 0; i < tracksReplica->rowCount(); i++)
        {
            QCOMPARE(tracksReplica->data(tracksReplica->index(i, 0)), model.data(model.index(i), Qt::DisplayRole));
        }

        //Change SubClass and make sure change propagates
        SubClassSimpleSource updatedSubclass;
        const MyPOD updatedValue(-1, 123.456, QStringLiteral("Updated"));
        updatedSubclass.setMyPOD(updatedValue);
        QSignalSpy replicaSpy(replica.data(), QByteArray(QByteArrayLiteral("2")+subclassMeta.notifySignal().methodSignature().constData()));
        parent.setSubClass(&updatedSubclass);
        replicaSpy.wait();
        QCOMPARE(replicaSpy.count(), 1);
        subclassQObjectPtr = subclassMeta.read(replica.data()).value<QObject *>();
        QVERIFY(subclassQObjectPtr != nullptr);
        subclassReplica = qobject_cast<QRemoteObjectDynamicReplica *>(subclassQObjectPtr);
        QVERIFY(subclassReplica != nullptr);

        pod = podMeta.read(subclassReplica).value<MyPOD>();
        QCOMPARE(pod, parent.subClass()->myPOD());
    }
    replica.reset();
}

void ProxyTest::testForwardProxy()
{
    // Setup Local Registry
    QRemoteObjectRegistryHost registry(registryUrl);
    SET_NODE_NAME(registry);

    // Setup Local Host
    QRemoteObjectHost host(localHostUrl, registryUrl);
    SET_NODE_NAME(host);

    // Setup Proxy
    QRemoteObjectRegistryHost proxyNode(proxyNodeUrl);
    SET_NODE_NAME(proxyNode);
    proxyNode.proxy(registryUrl, proxyHostUrl);
    // Include the reverseProxy to make sure we don't try to send back
    // proxied objects.
    proxyNode.reverseProxy();

    // Setup Source
    EngineSimpleSource engine;
    engine.setRpm(1234);

    // Setup Remote Node
    QRemoteObjectHost remoteNode(remoteNodeUrl);
    SET_NODE_NAME(remoteNode);
    remoteNode.connectToNode(proxyNodeUrl);

    // Add source
    host.enableRemoting(&engine);

    // Setup Replica
    const QScopedPointer<EngineReplica> replica(remoteNode.acquire<EngineReplica>());
    QVERIFY(replica->waitForSource(1000));

    // Compare Replica to Source
    QCOMPARE(replica->rpm(), engine.rpm());
}

void ProxyTest::testReverseProxy()
{
    // Setup Local Registry
    QRemoteObjectRegistryHost registry(registryUrl);
    SET_NODE_NAME(registry);

    // Setup Local Host
    QRemoteObjectHost host(localHostUrl, registryUrl);
    SET_NODE_NAME(host);

    // Setup Proxy
    // QRemoteObjectRegistryHost proxyNode(proxyNodeUrl);
    QRemoteObjectRegistryHost proxyNode(proxyNodeUrl);
    SET_NODE_NAME(proxyNode);
    proxyNode.proxy(registryUrl, proxyHostUrl);
    proxyNode.reverseProxy();

    // Setup Source
    EngineSimpleSource engine;
    engine.setRpm(1234);

    // Setup Remote Node
    QRemoteObjectHost remoteNode(remoteNodeUrl, proxyNodeUrl);
    SET_NODE_NAME(remoteNode);

    // Add source
    remoteNode.enableRemoting(&engine);

    // Setup Replica
    const QScopedPointer<EngineReplica> replica(host.acquire<EngineReplica>());
    QVERIFY(replica->waitForSource(1000));

    //Compare Replica to Source
    QCOMPARE(replica->rpm(), engine.rpm());
}

void ProxyTest::testTopLevelModel()
{
    QRemoteObjectRegistryHost registry(registryUrl);

    //Setup Local Host
    QRemoteObjectHost host(localHostUrl);
    SET_NODE_NAME(host);
    host.setRegistryUrl(registryUrl);

    QStringListModel model;
    model.setStringList(QStringList() << "Track1" << "Track2" << "Track3");
    host.enableRemoting(&model, "trackList", QVector<int>() << Qt::DisplayRole);

    QRemoteObjectHost proxyNode;
    SET_NODE_NAME(proxyNode);
    proxyNode.setHostUrl(tcpHostUrl);
    proxyNode.proxy(registryUrl);

    //Setup Local Replica
    QRemoteObjectNode client;
    SET_NODE_NAME(client);
    client.connectToNode(tcpHostUrl);
    QAbstractItemModelReplica *replica = client.acquireModel("trackList");
    QSignalSpy tracksSpy(replica, &QAbstractItemModelReplica::initialized);
    QVERIFY(tracksSpy.wait());
    QTest::qWait(100);
    QCOMPARE(replica->rowCount(), model.rowCount());
}

QTEST_MAIN(ProxyTest)

#include "tst_proxy.moc"
