// SPDX-License-Identifier: GPL-3.0-or-later

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea.
*
* nymea is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* nymea is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with nymea. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "nymeatestbase.h"
#include "nymeacore.h"
#include "nymeasettings.h"

#include "integrations/thingdiscoveryinfo.h"
#include "integrations/thingsetupinfo.h"
#include "integrations/thingutils.h"

#include "servers/mocktcpserver.h"
#include "jsonrpc/integrationshandler.h"
#include "../plugins/mock/extern-plugininfo.h"

#include <QtMath>

using namespace nymeaserver;

class TestIntegrations : public NymeaTestBase
{
    Q_OBJECT

private:
    ThingId m_mockThingAsyncId;

    inline void verifyThingError(const QVariant &response, Thing::ThingError error = Thing::ThingErrorNoError) {
        verifyError(response, "thingError", enumValueName(error));
    }

private slots:
    void initTestCase();

    void getPlugins();

    void getPluginConfig_data();
    void getPluginConfig();

    void setPluginConfig_data();
    void setPluginConfig();

    void getSupportedVendors();

    void getThingClasses_data();
    void getThingClasses();

    void verifyInterfaces();

    void addThing_data();
    void addThing();

    void thingAddedRemovedNotifications();

    void thingChangedNotifications();

    void getThings();

    void getThing_data();
    void getThing();

    void storedThings();

    void stateCache();

    void discoverThings_data();
    void discoverThings();

    void addPushButtonThings_data();
    void addPushButtonThings();

    void addDisplayPinThings_data();
    void addDisplayPinThings();

    void parentChildThings();

    void getActionTypes_data();
    void getActionTypes();

    void getEventTypes_data();
    void getEventTypes();

    void getStateTypes_data();
    void getStateTypes();

    void getStateValue_data();
    void getStateValue();

    void getStateValues_data();
    void getStateValues();

    void editThings_data();
    void editThings();

    void testThingSettings();

    void reconfigureThings_data();
    void reconfigureThings();

    void reconfigureByDiscovery_data();
    void reconfigureByDiscovery();

    void reconfigureByDiscoveryAndPair();
    void reconfigureAutoThing();

    void testBrowsing_data();
    void testBrowsing();

    void testExecuteBrowserItem_data();
    void testExecuteBrowserItem();

    void testExecuteBrowserItemAction_data();
    void testExecuteBrowserItemAction();

    void executeAction_data();
    void executeAction();

    void triggerEvent();
    void triggerStateChangeSignal();

    void params();
    void ensureValueClamping();

    void dynamicMinMax();

    void asyncSetupEmitsSetupStatusUpdate();

    void testTranslations();

    // Keep those at last as they will remove things
    void removeThing_data();
    void removeThing();

    void removeAutoThing();

    void discoverThingsParenting();
};

void TestIntegrations::initTestCase()
{
    NymeaTestBase::initTestCase();
    QLoggingCategory::setFilterRules("*.debug=false\n"
                                     "Tests.debug=true\n"
                                     "Mock.debug=true\n"
                                     "Translations.debug=true\n"
                                     "PythonIntegrations.debug=true\n"
                                     "LogEngine.debug=true\n"
                                     );

    // Adding an async mock to be used in tests below
    QVariantMap params;
    params.insert("thingClassId", mockThingClassId);
    params.insert("name", "Mocked Thing (Async)");

    QVariantList thingParams;

    QVariantMap asyncParam;
    asyncParam.insert("paramTypeId", mockThingAsyncParamTypeId);
    asyncParam.insert("value", true);
    thingParams.append(asyncParam);

    QVariantMap httpParam;
    httpParam.insert("paramTypeId", mockThingHttpportParamTypeId);
    httpParam.insert("value", 8765);
    thingParams.append(httpParam);

    params.insert("thingParams", thingParams);

    QVariant response = injectAndWait("Integrations.AddThing", params);

    m_mockThingAsyncId = ThingId(response.toMap().value("params").toMap().value("thingId").toString());
    QVERIFY2(!m_mockThingAsyncId.isNull(), "Creating an async mock failed");

    qCDebug(dcTests()) << "Created Async mock with ID" << m_mockThingAsyncId;
}

void TestIntegrations::getPlugins()
{
    QVariant response = injectAndWait("Integrations.GetPlugins");

    QVariantList plugins = response.toMap().value("params").toMap().value("plugins").toList();

    QCOMPARE(plugins.count() > 0, true);
    bool found = false;
    foreach (const QVariant &listEntry, plugins) {
        if (PluginId(listEntry.toMap().value("id").toString()) == mockPluginId) {
            found = true;
        }
    }
    QCOMPARE(found, true);
}

void TestIntegrations::getPluginConfig_data()
{
    QTest::addColumn<PluginId>("pluginId");
    QTest::addColumn<Thing::ThingError>("error");

    QTest::newRow("valid plugin") << mockPluginId << Thing::ThingErrorNoError;
    QTest::newRow("invalid plugin") << PluginId::createPluginId() << Thing::ThingErrorPluginNotFound;
}

void TestIntegrations::getPluginConfig()
{
    QFETCH(PluginId, pluginId);
    QFETCH(Thing::ThingError, error);

    QVariantMap params;
    params.insert("pluginId", pluginId);
    QVariant response = injectAndWait("Integrations.GetPluginConfiguration", params);
    verifyThingError(response, error);
}

void TestIntegrations::setPluginConfig_data()
{
    QTest::addColumn<PluginId>("pluginId");
    QTest::addColumn<QVariant>("value");
    QTest::addColumn<Thing::ThingError>("error");

    QTest::newRow("valid") << mockPluginId << QVariant(13) << Thing::ThingErrorNoError;
    QTest::newRow("invalid plugin") << PluginId::createPluginId() << QVariant(13) <<  Thing::ThingErrorPluginNotFound;
    QTest::newRow("too big") << mockPluginId << QVariant(130) << Thing::ThingErrorInvalidParameter;
    QTest::newRow("too small") << mockPluginId << QVariant(-13) << Thing::ThingErrorInvalidParameter;
    QTest::newRow("wrong type") << mockPluginId << QVariant("wrontType") << Thing::ThingErrorInvalidParameter;
}

void TestIntegrations::setPluginConfig()
{
    QFETCH(PluginId, pluginId);
    QFETCH(QVariant, value);
    QFETCH(Thing::ThingError, error);

    QVariantMap params;
    params.insert("pluginId", pluginId);

    QVariantList configuration;
    QVariantMap configParam;
    configParam.insert("paramTypeId", mockPluginConfigParamIntParamTypeId);
    configParam.insert("value", value);
    configuration.append(configParam);
    params.insert("configuration", configuration);
    QVariant response = injectAndWait("Integrations.SetPluginConfiguration", params);
    verifyThingError(response, error);

    if (error == Thing::ThingErrorNoError) {
        params.clear();
        params.insert("pluginId", pluginId);
        response = injectAndWait("Integrations.GetPluginConfiguration", params);
        verifyThingError(response);
        qDebug() << value << response.toMap().value("params").toMap().value("configuration").toList().first();
        QVERIFY2(ParamTypeId(response.toMap().value("params").toMap().value("configuration").toList().first().toMap().value("paramTypeId").toString()) == mockPluginConfigParamIntParamTypeId, "Value not set correctly");
        QVERIFY2(response.toMap().value("params").toMap().value("configuration").toList().first().toMap().value("value") == value, "Value not set correctly");
    }
}

void TestIntegrations::getSupportedVendors()
{
    QVariant supportedVendors = injectAndWait("Integrations.GetVendors");
    qDebug() << "response" << supportedVendors;

    // Make sure there is exactly 1 Vendor with nymea's id
    QVariantList vendorList = supportedVendors.toMap().value("params").toMap().value("vendors").toList();
    QCOMPARE(vendorList.count() > 0, true);
    bool found = false;
    foreach (const QVariant &listEntry, vendorList) {
        if (VendorId(listEntry.toMap().value("id").toString()) == nymeaVendorId) {
            found = true;
        }
    }
    QCOMPARE(found, true);
}

void TestIntegrations::getThingClasses_data()
{
    QTest::addColumn<VendorId>("vendorId");
    QTest::addColumn<QList<ThingClassId>>("thingClassIds");
    QTest::addColumn<int>("resultCount");
#ifdef WITH_PYTHON
    QTest::newRow("vendor nymea") << nymeaVendorId << QList<ThingClassId>() << 17;
    QTest::newRow("no filter") << VendorId() << QList<ThingClassId>() << 17;
#else
    QTest::newRow("vendor nymea") << nymeaVendorId << QList<ThingClassId>() << 14;
    QTest::newRow("no filter") << VendorId() << QList<ThingClassId>() << 14;
#endif
    QTest::newRow("invalid vendor") << VendorId("93e7d361-8025-4354-b17e-b68406c800bc") << QList<ThingClassId>() << 0;
    QTest::newRow("mockThingClassId") << VendorId() << (QList<ThingClassId>() << mockThingClassId) << 1;
    QTest::newRow("invalid thingClassId") << VendorId() << (QList<ThingClassId>() << ThingClassId("6c78ec28-09b6-476d-ac27-1d6966a45c57")) << 0;
}

void TestIntegrations::getThingClasses()
{
    QFETCH(VendorId, vendorId);
    QFETCH(QList<ThingClassId>, thingClassIds);
    QFETCH(int, resultCount);

    QVariantMap params;
    if (!vendorId.isNull()) {
        params.insert("vendorId", vendorId);
    }
    if (!thingClassIds.isEmpty()) {
        QStringList thingClassIdStrings;
        foreach (const ThingClassId &id, thingClassIds) {
            thingClassIdStrings.append(id.toString());
        }
        params.insert("thingClassIds", thingClassIdStrings);
    }
    QVariant result = injectAndWait("Integrations.GetThingClasses", params);

    QVariantList thingClasses = result.toMap().value("params").toMap().value("thingClasses").toList();
    // Make sure there are the right amount of thing classes
    QCOMPARE(thingClasses.count(), resultCount);
}

void TestIntegrations::verifyInterfaces()
{
    QVariantMap params;
    params.insert("vendorId", nymeaVendorId);
    QVariant result = injectAndWait("Integrations.GetThingClasses", params);
    QVariantList supportedThings = result.toMap().value("params").toMap().value("thingClasses").toList();

    QVariantMap mock;
    foreach (const QVariant &thingClass, supportedThings) {
        if (thingClass.toMap().value("id").toUuid() == mockThingClassId) {
            mock = thingClass.toMap();
        }
    }
    QVERIFY(!mock.isEmpty());

    QVariantList interfaces = mock.value("interfaces").toList();
    QVariantList expectedInterfaces = {"system", "light", "power", "battery", "wirelessconnectable", "connectable", "update", "multibutton", "button"};
    qCDebug(dcTests()) << interfaces;
    qCDebug(dcTests()) << expectedInterfaces;
    QCOMPARE(interfaces, expectedInterfaces);
}

void TestIntegrations::addThing_data()
{
    QTest::addColumn<ThingClassId>("thingClassId");
    QTest::addColumn<QVariantList>("thingParams");
    QTest::addColumn<bool>("jsonValidation");
    QTest::addColumn<Thing::ThingError>("thingError");

    QVariantMap httpportParam;
    httpportParam.insert("paramTypeId", mockThingHttpportParamTypeId.toString());
    httpportParam.insert("value", m_mockThing1Port - 1);
    QVariantMap asyncParam;
    asyncParam.insert("paramTypeId", mockThingAsyncParamTypeId);
    asyncParam.insert("value", true);
    QVariantMap brokenParam;
    brokenParam.insert("paramTypeId", mockThingBrokenParamTypeId);
    brokenParam.insert("value", true);

    QVariantList thingParams;

    thingParams.clear(); thingParams << httpportParam;
    QTest::newRow("User, JustAdd") << mockThingClassId << thingParams << true << Thing::ThingErrorNoError;
    thingParams.clear(); thingParams << httpportParam << asyncParam;
    QTest::newRow("User, JustAdd, Async") << mockThingClassId << thingParams << true << Thing::ThingErrorNoError;
    QTest::newRow("Invalid ThingClassId") << ThingClassId::createThingClassId() << thingParams << true << Thing::ThingErrorThingClassNotFound;
    thingParams.clear(); thingParams << httpportParam << brokenParam;
    QTest::newRow("Setup failure") << mockThingClassId << thingParams << true << Thing::ThingErrorSetupFailed;
    thingParams.clear(); thingParams << httpportParam << asyncParam << brokenParam;
    QTest::newRow("Setup failure, Async") << mockThingClassId << thingParams << true << Thing::ThingErrorSetupFailed;

    QVariantList invalidThingParams;
    QTest::newRow("User, JustAdd, missing params") << mockThingClassId << invalidThingParams << true << Thing::ThingErrorMissingParameter;

    QVariantMap fakeparam;
    fakeparam.insert("paramTypeId", ParamTypeId::createParamTypeId());
    invalidThingParams.append(fakeparam);
    QTest::newRow("User, JustAdd, invalid param") << mockThingClassId << invalidThingParams << false << Thing::ThingErrorMissingParameter;

    QVariantMap fakeparam2;
    fakeparam2.insert("paramTypeId", mockThingHttpportParamTypeId.toString());
    fakeparam2.insert("value", "blabla");
    invalidThingParams.clear();
    invalidThingParams.append(fakeparam2);
    QTest::newRow("User, JustAdd, wrong param") << mockThingClassId << invalidThingParams << true << Thing::ThingErrorInvalidParameter;

    thingParams.clear(); thingParams << httpportParam << fakeparam;
    QTest::newRow("USer, JustAdd, additional invalid param") << mockThingClassId << thingParams << false << Thing::ThingErrorInvalidParameter;

    thingParams.clear(); thingParams << httpportParam << fakeparam2;
    QTest::newRow("USer, JustAdd, duplicate param") << mockThingClassId << thingParams << true << Thing::ThingErrorInvalidParameter;

}

void TestIntegrations::addThing()
{
    QFETCH(ThingClassId, thingClassId);
    QFETCH(QVariantList, thingParams);
    QFETCH(bool, jsonValidation);
    QFETCH(Thing::ThingError, thingError);

    QVariantMap params;
    params.insert("thingClassId", thingClassId);
    params.insert("name", "Test Add Thing");
    params.insert("thingParams", thingParams);
    QVariant response = injectAndWait("Integrations.AddThing", params);

    if (!jsonValidation) {
        QCOMPARE(response.toMap().value("status").toString(), QString("error"));
        return;
    }
    verifyThingError(response, thingError);

    if (thingError == Thing::ThingErrorNoError) {
        QUuid thingId(response.toMap().value("params").toMap().value("thingId").toString());
        params.clear();
        params.insert("thingId", thingId.toString());
        response = injectAndWait("Integrations.RemoveThing", params);
        verifyThingError(response);
    }
}

void TestIntegrations::thingAddedRemovedNotifications()
{
    enableNotifications({"Integrations"});

    // Setup connection to mock client
    QSignalSpy clientSpy(m_mockTcpServer, &MockTcpServer::outgoingData);

    // add thing and wait for notification
    QVariantList thingParams;
    QVariantMap httpportParam;
    httpportParam.insert("paramTypeId", mockThingHttpportParamTypeId);
    httpportParam.insert("value", 5678);
    thingParams.append(httpportParam);

    QVariantMap params; clientSpy.clear();
    params.insert("thingClassId", mockThingClassId);
    params.insert("name", "Mocked thing");
    params.insert("thingParams", thingParams);
    QVariant response = injectAndWait("Integrations.AddThing", params);
    if (clientSpy.count() == 0) clientSpy.wait();
    verifyThingError(response);
    QVariantMap notificationThingMap = checkNotification(clientSpy, "Integrations.ThingAdded").toMap().value("params").toMap().value("thing").toMap();

    ThingId thingId = ThingId(response.toMap().value("params").toMap().value("thingId").toString());
    QVERIFY(!thingId.isNull());

    // check the ThingAdded notification
    QCOMPARE(notificationThingMap.value("thingClassId").toUuid(), QUuid(mockThingClassId));
    QCOMPARE(notificationThingMap.value("id").toUuid(), QUuid(thingId));
    foreach (const QVariant &param, notificationThingMap.value("params").toList()) {
        if (param.toMap().value("name").toString() == "httpport") {
            QCOMPARE(param.toMap().value("value").toInt(), httpportParam.value("value").toInt());
        }
    }

    // now remove the thong and check the thing removed notification
    params.clear(); response.clear(); clientSpy.clear();
    params.insert("thingId", thingId);
    response = injectAndWait("Integrations.RemoveThing", params);
    if (clientSpy.count() == 0) clientSpy.wait();
    verifyThingError(response);
    checkNotification(clientSpy, "Integrations.ThingRemoved");

    QCOMPARE(disableNotifications(), true);
}

void TestIntegrations::thingChangedNotifications()
{
    enableNotifications({"Integrations"});

    // Setup connection to mock client
    QSignalSpy clientSpy(m_mockTcpServer, &MockTcpServer::outgoingData);

    // ADD
    // add thing and wait for notification
    QVariantList thingParams;
    QVariantMap httpportParam;
    httpportParam.insert("paramTypeId", mockThingHttpportParamTypeId);
    httpportParam.insert("value", 23234);
    thingParams.append(httpportParam);

    clientSpy.clear();
    QVariantMap params;
    params.insert("thingClassId", mockThingClassId);
    params.insert("name", "Mock");
    params.insert("thingParams", thingParams);
    QVariant response = injectAndWait("Integrations.AddThing", params);
    ThingId thingId = ThingId(response.toMap().value("params").toMap().value("thingId").toString());
    QVERIFY(!thingId.isNull());
    if (clientSpy.count() == 0) clientSpy.wait();
    verifyThingError(response);
    QVariantMap notificationThingMap = checkNotification(clientSpy, "Integrations.ThingAdded").toMap().value("params").toMap().value("thing").toMap();

    QCOMPARE(notificationThingMap.value("thingClassId").toUuid(), QUuid(mockThingClassId));
    QCOMPARE(notificationThingMap.value("id").toUuid(), QUuid(thingId));
    foreach (const QVariant &param, notificationThingMap.value("params").toList()) {
        if (param.toMap().value("name").toString() == "httpport") {
            QCOMPARE(param.toMap().value("value").toInt(), httpportParam.value("value").toInt());
        }
    }

    // RECONFIGURE
    // now reconfigure the thing and check the thing changed notification
    QVariantList newThingParams;
    QVariantMap newHttpportParam;
    newHttpportParam.insert("paramTypeId", mockThingHttpportParamTypeId);
    newHttpportParam.insert("value", 45473);
    newThingParams.append(newHttpportParam);

    params.clear(); response.clear(); clientSpy.clear();
    params.insert("thingId", thingId);
    params.insert("thingParams", newThingParams);
    response = injectAndWait("Integrations.ReconfigureThing", params);
    if (clientSpy.count() == 0) clientSpy.wait();
    verifyThingError(response);
    QVariantMap reconfigureThingNotificationMap = checkNotification(clientSpy, "Integrations.ThingChanged").toMap().value("params").toMap().value("thing").toMap();
    QCOMPARE(reconfigureThingNotificationMap.value("thingClassId").toUuid(), QUuid(mockThingClassId));
    QCOMPARE(reconfigureThingNotificationMap.value("id").toUuid(), QUuid(thingId));
    foreach (const QVariant &param, reconfigureThingNotificationMap.value("params").toList()) {
        if (param.toMap().value("name").toString() == "httpport") {
            QCOMPARE(param.toMap().value("value").toInt(), newHttpportParam.value("value").toInt());
        }
    }

    // EDIT thing name
    QString thingName = "Test thing 1234";
    params.clear(); response.clear(); clientSpy.clear();
    params.insert("thingId", thingId);
    params.insert("name", thingName);
    response = injectAndWait("Integrations.EditThing", params);
    if (clientSpy.count() == 0) clientSpy.wait();
    verifyThingError(response);
    QVariantMap editThingNotificationMap = checkNotification(clientSpy, "Integrations.ThingChanged").toMap().value("params").toMap().value("thing").toMap();
    QCOMPARE(editThingNotificationMap.value("thingClassId").toUuid(), QUuid(mockThingClassId));
    QCOMPARE(editThingNotificationMap.value("id").toUuid(), QUuid(thingId));
    QCOMPARE(editThingNotificationMap.value("name").toString(), thingName);

    // REMOVE
    // now remove the thing and check the thing removed notification
    params.clear(); response.clear(); clientSpy.clear();
    params.insert("thingId", thingId);
    response = injectAndWait("Integrations.RemoveThing", params);
    if (clientSpy.count() == 0) clientSpy.wait();
    verifyThingError(response);
    checkNotification(clientSpy, "Integrations.ThingRemoved");
    checkNotification(clientSpy, "Logging.LogDatabaseUpdated");
}

void TestIntegrations::getThings()
{
    QVariant response = injectAndWait("Integrations.GetThings");

    QVariantList things = response.toMap().value("params").toMap().value("things").toList();
    QCOMPARE(things.count(), 3); // There should be: one auto created mock, one created in NymeaTestBase::initTestcase() and one created in TestIntegrations::initTestCase()
}

void TestIntegrations::getThing_data()
{
    QTest::addColumn<ThingId>("thingId");
    QTest::addColumn<Thing::ThingError>("expectedError");

    QTest::newRow("valid thingId") << ThingId(m_mockThingId) << Thing::ThingErrorNoError;
    QTest::newRow("invalid thingId") << ThingId::createThingId() << Thing::ThingErrorThingNotFound;
}

void TestIntegrations::getThing()
{
    QFETCH(ThingId, thingId);
    QFETCH(Thing::ThingError, expectedError);

    QVariantMap params;
    params.insert("thingId", thingId);
    QVariant response = injectAndWait("Integrations.GetThings", params);

    //    qCDebug(dcTests()) << qUtf8Printable(QJsonDocument::fromVariant(response).toJson());

    if (expectedError == Thing::ThingErrorNoError) {
        QVariantList things = response.toMap().value("params").toMap().value("things").toList();
        QCOMPARE(things.count(), 1);
    }
}

void TestIntegrations::storedThings()
{
    QVariantMap params;
    params.insert("thingClassId", mockThingClassId);
    params.insert("name", "Test stored thing");
    QVariantList thingParams;
    QVariantMap asyncParam;
    asyncParam.insert("paramTypeId", mockThingAsyncParamTypeId);
    asyncParam.insert("value", false);
    thingParams.append(asyncParam);
    QVariantMap brokenParam;
    brokenParam.insert("paramTypeId", mockThingBrokenParamTypeId);
    brokenParam.insert("value", false);
    thingParams.append(brokenParam);
    QVariantMap httpportParam;
    httpportParam.insert("paramTypeId", mockThingHttpportParamTypeId);
    httpportParam.insert("value", 8889);
    thingParams.append(httpportParam);
    params.insert("thingParams", thingParams);

    QVariant response = injectAndWait("Integrations.AddThing", params);
    verifyThingError(response);
    ThingId addedThingId = ThingId(response.toMap().value("params").toMap().value("thingId").toString());
    QVERIFY(!addedThingId.isNull());

    clearLoggingDatabase("state-" + addedThingId.toString() + "-int");

    // Restart the core instance to check if settings are loaded at startup
    restartServer();

    response = injectAndWait("Integrations.GetThings", QVariantMap());

    bool found = false;
    foreach (const QVariant &thing, response.toMap().value("params").toMap().value("things").toList()) {
        if (ThingId(thing.toMap().value("id").toString()) == addedThingId) {
            qCDebug(dcTests()) << "found added thing" << thing.toMap().value("params");
            qCDebug(dcTests()) << "expected thingParams:" << thingParams;
            verifyParams(thingParams, thing.toMap().value("params").toList());
            found = true;
            break;
        }
    }
    QVERIFY2(found, "thing missing in config!");

    // Wait for the DB to sync up and then verify that just restarting did not add state changes for this thing
    waitForDBSync();

    params.clear();
    params.insert("sources", QStringList{"state-" + addedThingId.toString() + "-int"});
    response = injectAndWait("Logging.GetLogEntries", params);
    QVERIFY2(response.toMap().value("params").toMap().contains("logEntries"), "Huh? GetLogEntries failed!");
    qCDebug(dcTests()) << "log response:" << response.toMap().value("params").toMap().value("logEntries");
    QVERIFY2(response.toMap().value("params").toMap().value("logEntries").toList().isEmpty(), "There are state changed events after a core restart even though states did not change!");

    params.clear();
    params.insert("thingId", addedThingId);
    response = injectAndWait("Integrations.RemoveThing", params);
    verifyThingError(response);
}

void TestIntegrations::stateCache()
{
    ThingClass mockThingClass = NymeaCore::instance()->thingManager()->findThingClass(mockThingClassId);

    QVERIFY2(mockThingClass.getStateType(mockIntStateTypeId).cached(), "Mock int state is not cached (required to be true for this test)");
    QVERIFY2(!mockThingClass.getStateType(mockBoolStateTypeId).cached(), "Mock bool state is cached (required to be false for this test)");

    Thing* thing = NymeaCore::instance()->thingManager()->findConfiguredThings(mockThingClassId).first();
    int port = thing->paramValue(mockThingHttpportParamTypeId).toInt();
    QNetworkAccessManager nam;
    QSignalSpy spy(&nam, &QNetworkAccessManager::finished);


    // First set the state values to something that is *not* the default
    int oldIntValue = mockThingClass.getStateType(mockIntStateTypeId).defaultValue().toInt();
    int newIntValue = oldIntValue + 1;
    bool oldBoolValue = mockThingClass.getStateType(mockBoolStateTypeId).defaultValue().toBool();
    bool newBoolValue = !oldBoolValue;

    QNetworkRequest request(QUrl(QString("http://localhost:%1/setstate?%2=%3").arg(port).arg(mockIntStateTypeId.toString()).arg(newIntValue)));
    QNetworkReply *reply = nam.get(request);
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
    spy.wait();

    spy.clear();
    request = QNetworkRequest(QUrl(QString("http://localhost:%1/setstate?%2=%3").arg(port).arg(mockBoolStateTypeId.toString()).arg(newBoolValue)));
    reply = nam.get(request);
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
    spy.wait();

    // For completeness, verify through JSONRPC that they were actually set.
    QVariantMap params;
    params.insert("thingId", thing->id());

    params["stateTypeId"] = mockIntStateTypeId;
    QVariant response = injectAndWait("Integrations.GetStateValue", params);
    QCOMPARE(response.toMap().value("params").toMap().value("value").toInt(), newIntValue);

    params["stateTypeId"] = mockBoolStateTypeId;
    response = injectAndWait("Integrations.GetStateValue", params);
    QCOMPARE(response.toMap().value("params").toMap().value("value").toBool(), newBoolValue);

    // Restart the server
    restartServer();

    // And check if the cached int state has successfully been restored
    params["stateTypeId"] = mockIntStateTypeId;
    response = injectAndWait("Integrations.GetStateValue", params);
    QCOMPARE(response.toMap().value("params").toMap().value("value").toInt(), newIntValue);

    // and that the non-cached bool state is back to its default
    params["stateTypeId"] = mockBoolStateTypeId;
    response = injectAndWait("Integrations.GetStateValue", params);
    QCOMPARE(response.toMap().value("params").toMap().value("value").toBool(), mockThingClass.getStateType(mockBoolStateTypeId).defaultValue().toBool());

    // Reset back to default values
    spy.clear();
    request = QNetworkRequest(QUrl(QString("http://localhost:%1/setstate?%2=%3").arg(port).arg(mockBoolStateTypeId.toString()).arg(oldIntValue)));
    reply = nam.get(request);
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
    spy.wait();
    spy.clear();
    request = QNetworkRequest(QUrl(QString("http://localhost:%1/setstate?%2=%3").arg(port).arg(mockBoolStateTypeId.toString()).arg(oldBoolValue)));
    reply = nam.get(request);
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
    spy.wait();
}

void TestIntegrations::discoverThings_data()
{
    QTest::addColumn<ThingClassId>("thingClassId");
    QTest::addColumn<int>("resultCount");
    QTest::addColumn<Thing::ThingError>("error");
    QTest::addColumn<QVariantList>("discoveryParams");

    QVariantList discoveryParams;
    QVariantMap resultCountParam;
    resultCountParam.insert("paramTypeId", mockDiscoveryResultCountParamTypeId);
    resultCountParam.insert("value", 1);
    discoveryParams.append(resultCountParam);

    QTest::newRow("valid thingClassId") << mockThingClassId << 2 << Thing::ThingErrorNoError << QVariantList();
    QTest::newRow("valid thingClassId with params") << mockThingClassId << 1 << Thing::ThingErrorNoError << discoveryParams;
    QTest::newRow("invalid thingClassId") << ThingClassId::createThingClassId() << 0 << Thing::ThingErrorThingClassNotFound << QVariantList();
}

void TestIntegrations::discoverThings()
{
    QFETCH(ThingClassId, thingClassId);
    QFETCH(int, resultCount);
    QFETCH(Thing::ThingError, error);
    QFETCH(QVariantList, discoveryParams);

    QVariantMap params;
    params.insert("thingClassId", thingClassId);
    params.insert("discoveryParams", discoveryParams);
    QVariant response = injectAndWait("Integrations.DiscoverThings", params);

    verifyThingError(response, error);
    if (error == Thing::ThingErrorNoError) {
        QCOMPARE(response.toMap().value("params").toMap().value("thingDescriptors").toList().count(), resultCount);
    }

    // If we found something, lets try to add it
    if (error == Thing::ThingErrorNoError) {
        ThingDescriptorId descriptorId = ThingDescriptorId(response.toMap().value("params").toMap().value("thingDescriptors").toList().first().toMap().value("id").toString());

        params.clear();
        params.insert("thingClassId", thingClassId);
        params.insert("name", "Discoverd mock");
        params.insert("thingDescriptorId", descriptorId.toString());
        response = injectAndWait("Integrations.AddThing", params);

        verifyThingError(response);

        ThingId thingId(response.toMap().value("params").toMap().value("thingId").toString());
        params.clear();
        params.insert("thingId", thingId.toString());
        response = injectAndWait("Integrations.RemoveThing", params);
        verifyThingError(response);
    }
}

void TestIntegrations::addPushButtonThings_data()
{
    QTest::addColumn<ThingClassId>("thingClassId");
    QTest::addColumn<Thing::ThingError>("error");
    QTest::addColumn<bool>("waitForButtonPressed");

    QTest::newRow("Valid: Add PushButton thing") << pushButtonMockThingClassId << Thing::ThingErrorNoError << true;
    QTest::newRow("Invalid: Add PushButton thing (press to early)") << pushButtonMockThingClassId << Thing::ThingErrorAuthenticationFailure << false;
}

void TestIntegrations::addPushButtonThings()
{
    QFETCH(ThingClassId, thingClassId);
    QFETCH(Thing::ThingError, error);
    QFETCH(bool, waitForButtonPressed);

    // Discover things
    QVariantList discoveryParams;
    QVariantMap resultCountParam;
    resultCountParam.insert("paramTypeId", pushButtonMockDiscoveryResultCountParamTypeId);
    resultCountParam.insert("value", 1);
    discoveryParams.append(resultCountParam);

    QVariantMap params;
    params.insert("thingClassId", thingClassId);
    params.insert("discoveryParams", discoveryParams);
    QVariant response = injectAndWait("Integrations.DiscoverThings", params);

    verifyThingError(response, Thing::ThingErrorNoError);
    QCOMPARE(response.toMap().value("params").toMap().value("thingDescriptors").toList().count(), 1);


    // Pair thing
    ThingDescriptorId descriptorId = ThingDescriptorId(response.toMap().value("params").toMap().value("thingDescriptors").toList().first().toMap().value("id").toString());
    params.clear();
    params.insert("thingClassId", thingClassId);
    params.insert("name", "Pushbutton mock");
    params.insert("thingDescriptorId", descriptorId.toString());
    response = injectAndWait("Integrations.PairThing", params);

    verifyThingError(response);

    PairingTransactionId pairingTransactionId(response.toMap().value("params").toMap().value("pairingTransactionId").toString());
    QString displayMessage = response.toMap().value("params").toMap().value("displayMessage").toString();

    qDebug() << "displayMessage" << displayMessage;

    if (waitForButtonPressed)
        QTest::qWait(3500);

    // Confirm pairing
    params.clear();
    params.insert("pairingTransactionId", pairingTransactionId.toString());
    response = injectAndWait("Integrations.ConfirmPairing", params);

    verifyThingError(response, error);

    if (error == Thing::ThingErrorNoError) {
        ThingId thingId(response.toMap().value("params").toMap().value("thingId").toString());
        params.clear();
        params.insert("thingId", thingId.toString());
        response = injectAndWait("Integrations.RemoveThing", params);
        verifyThingError(response);
    }
}

void TestIntegrations::addDisplayPinThings_data()
{
    QTest::addColumn<ThingClassId>("thingClassId");
    QTest::addColumn<Thing::ThingError>("error");
    QTest::addColumn<QString>("secret");

    QTest::newRow("Valid: Add DisplayPin mock") << displayPinMockThingClassId << Thing::ThingErrorNoError << "243681";
    QTest::newRow("Invalid: Add DisplayPin mock (wrong pin)") << displayPinMockThingClassId << Thing::ThingErrorAuthenticationFailure << "243682";
}

void TestIntegrations::addDisplayPinThings()
{
    QFETCH(ThingClassId, thingClassId);
    QFETCH(Thing::ThingError, error);
    QFETCH(QString, secret);

    // Discover things
    QVariantList discoveryParams;
    QVariantMap resultCountParam;
    resultCountParam.insert("paramTypeId", displayPinMockDiscoveryResultCountParamTypeId);
    resultCountParam.insert("value", 1);
    discoveryParams.append(resultCountParam);

    QVariantMap params;
    params.insert("thingClassId", thingClassId);
    params.insert("discoveryParams", discoveryParams);
    QVariant response = injectAndWait("Integrations.DiscoverThings", params);

    verifyThingError(response, Thing::ThingErrorNoError);
    QCOMPARE(response.toMap().value("params").toMap().value("thingDescriptors").toList().count(), 1);

    // Pair thing
    ThingDescriptorId descriptorId = ThingDescriptorId(response.toMap().value("params").toMap().value("thingDescriptors").toList().first().toMap().value("id").toString());
    params.clear();
    params.insert("thingClassId", thingClassId);
    params.insert("name", "Display pin mock");
    params.insert("thingDescriptorId", descriptorId.toString());
    response = injectAndWait("Integrations.PairThing", params);

    verifyThingError(response);

    PairingTransactionId pairingTransactionId(response.toMap().value("params").toMap().value("pairingTransactionId").toString());
    QString displayMessage = response.toMap().value("params").toMap().value("displayMessage").toString();

    qCDebug(dcTests()) << "displayMessage" << displayMessage;

    params.clear();
    params.insert("pairingTransactionId", pairingTransactionId.toString());
    params.insert("secret", secret);
    response = injectAndWait("Integrations.ConfirmPairing", params);

    verifyThingError(response, error);

    if (error == Thing::ThingErrorNoError) {
        ThingId thingId(response.toMap().value("params").toMap().value("thingId").toString());
        params.clear();
        params.insert("thingId", thingId.toString());
        response = injectAndWait("Integrations.RemoveThing", params);
        verifyThingError(response);
    }
}

void TestIntegrations::parentChildThings()
{
    // add parent
    QVariantMap params;
    params.insert("thingClassId", parentMockThingClassId);
    params.insert("name", "Parent");

    QSignalSpy thingAddedSpy(NymeaCore::instance()->thingManager(), &ThingManager::thingAdded);

    QVariant response = injectAndWait("Integrations.AddThing", params);
    verifyThingError(response);

    ThingId parentId = ThingId(response.toMap().value("params").toMap().value("thingId").toString());
    QVERIFY(!parentId.isNull());

    thingAddedSpy.wait();
    QCOMPARE(thingAddedSpy.count(), 2);

    // find child
    response = injectAndWait("Integrations.GetThings");

    QVariantList things = response.toMap().value("params").toMap().value("things").toList();

    ThingId childId;
    foreach (const QVariant thingVariant, things) {
        QVariantMap thingMap = thingVariant.toMap();

        if (thingMap.value("thingClassId").toUuid() == childMockThingClassId) {
            if (thingMap.value("parentId").toUuid() == parentId) {
                childId = ThingId(thingMap.value("id").toString());
                break;
            }
        }
    }
    QVERIFY2(!childId.isNull(), QString("Could not find child:\nParent ID:%1\nResponse:%2")
                                    .arg(parentId.toString())
                                    .arg(qUtf8Printable(QJsonDocument::fromVariant(response).toJson()))
                                    .toUtf8());

    // Try to remove the child
    params.clear();
    params.insert("thingId", childId.toString());
    response = injectAndWait("Integrations.RemoveThing", params);
    verifyThingError(response, Thing::ThingErrorThingIsChild);

    // check if the child is still there
    response = injectAndWait("Integrations.GetThings");
    things = response.toMap().value("params").toMap().value("things").toList();
    bool found = false;
    foreach (const QVariant thingVariant, things) {
        QVariantMap thingMap = thingVariant.toMap();
        if (thingMap.value("thingClassId").toUuid() == childMockThingClassId) {
            if (thingMap.value("id").toUuid() == childId) {
                found = true;
                break;
            }
        }
    }
    QVERIFY2(found, "Could not find child.");

    // remove the parent
    params.clear();
    params.insert("thingId", parentId.toString());
    response = injectAndWait("Integrations.RemoveThing", params);
    verifyThingError(response);

    // check if the child is still there
    response = injectAndWait("Integrations.GetThings");
    things = response.toMap().value("params").toMap().value("things").toList();
    found = false;
    foreach (const QVariant thingVariant, things) {
        QVariantMap thingMap = thingVariant.toMap();
        if (thingMap.value("thingClassId").toString() == childMockThingClassId.toString()) {
            if (thingMap.value("id") == childId.toString()) {
                found = true;
                break;
            }
        }
    }
    QVERIFY2(!found, "Could not find child.");
}

void TestIntegrations::getActionTypes_data()
{
    QTest::addColumn<ThingClassId>("thingClassId");
    QTest::addColumn<QList<ActionTypeId> >("actionTypeTestData");

    QTest::newRow("valid thingClass") << mockThingClassId
                                      << (QList<ActionTypeId>() << mockIntWithLimitsActionTypeId << mockAsyncActionTypeId << mockAsyncFailingActionTypeId << mockFailingActionTypeId << mockWithoutParamsActionTypeId << mockPowerActionTypeId << mockWithoutParamsActionTypeId << mockBatteryLevelActionTypeId << mockSignalStrengthActionTypeId << mockUpdateStatusActionTypeId << mockPerformUpdateActionTypeId << mockPressButtonActionTypeId);
    QTest::newRow("invalid thingClass") << ThingClassId("094f8024-5caa-48c1-ab6a-de486a92088f") << QList<ActionTypeId>();
}

void TestIntegrations::getActionTypes()
{
    QFETCH(ThingClassId, thingClassId);
    QFETCH(QList<ActionTypeId>, actionTypeTestData);

    QVariantMap params;
    params.insert("thingClassId", thingClassId);
    QVariant response = injectAndWait("Integrations.GetActionTypes", params);

    QVariantList actionTypes = response.toMap().value("params").toMap().value("actionTypes").toList();
    QCOMPARE(actionTypes.count(), actionTypeTestData.count());
    foreach (const ActionTypeId &testDataId, actionTypeTestData) {
        bool found = false;
        foreach (const QVariant &at, actionTypes) {
            if (testDataId == at.toMap().value("id").toUuid()) {
                found = true;
                break;
            }
        }
        QVERIFY(found);
    }
}

void TestIntegrations::getEventTypes_data()
{
    QTest::addColumn<ThingClassId>("thingClassId");
    QTest::addColumn<int>("resultCount");

    QTest::newRow("valid thingClass") << mockThingClassId << 3;
    QTest::newRow("invalid thingClass") << ThingClassId("094f8024-5caa-48c1-ab6a-de486a92088f") << 0;
}

void TestIntegrations::getEventTypes()
{
    QFETCH(ThingClassId, thingClassId);
    QFETCH(int, resultCount);

    QVariantMap params;
    params.insert("thingClassId", thingClassId);
    QVariant response = injectAndWait("Integrations.GetEventTypes", params);

    qDebug() << response;

    QVariantList eventTypes = response.toMap().value("params").toMap().value("eventTypes").toList();
    QCOMPARE(eventTypes.count(), resultCount);
}

void TestIntegrations::getStateTypes_data()
{
    QTest::addColumn<ThingClassId>("thingClassId");
    QTest::addColumn<int>("resultCount");

    QTest::newRow("valid thingClass") << mockThingClassId << 12;
    QTest::newRow("invalid thingClass") << ThingClassId("094f8024-5caa-48c1-ab6a-de486a92088f") << 0;
}

void TestIntegrations::getStateTypes()
{
    QFETCH(ThingClassId, thingClassId);
    QFETCH(int, resultCount);

    QVariantMap params;
    params.insert("thingClassId", thingClassId);
    QVariant response = injectAndWait("Integrations.GetStateTypes", params);

    QVariantList stateTypes = response.toMap().value("params").toMap().value("stateTypes").toList();
    QCOMPARE(stateTypes.count(), resultCount);
    if (resultCount > 0) {
        QCOMPARE(stateTypes.first().toMap().value("id").toUuid().toString(), mockIntStateTypeId.toString());
    }
}

void TestIntegrations::getStateValue_data()
{
    QTest::addColumn<ThingId>("thingId");
    QTest::addColumn<StateTypeId>("stateTypeId");
    QTest::addColumn<Thing::ThingError>("statusCode");

    QTest::newRow("valid thingId") << ThingId(m_mockThingId) << mockIntStateTypeId << Thing::ThingErrorNoError;
    QTest::newRow("invalid thingId") << ThingId("094f8024-5caa-48c1-ab6a-de486a92088f") << mockIntStateTypeId << Thing::ThingErrorThingNotFound;
    QTest::newRow("invalid statetypeId") << ThingId(m_mockThingId) << StateTypeId("120514f1-343e-4621-9bff-dac616169df9") << Thing::ThingErrorStateTypeNotFound;
}

void TestIntegrations::getStateValue()
{
    NymeaCore::instance()->thingManager()->findConfiguredThing(m_mockThingId)->setStateValue(mockIntStateTypeId, 10);
    QFETCH(ThingId, thingId);
    QFETCH(StateTypeId, stateTypeId);
    QFETCH(Thing::ThingError, statusCode);

    QVariantMap params;
    params.insert("thingId", thingId);
    params.insert("stateTypeId", stateTypeId);
    QVariant response = injectAndWait("Integrations.GetStateValue", params);

    QCOMPARE(response.toMap().value("params").toMap().value("thingError").toString(), enumValueName(statusCode));
    if (statusCode == Thing::ThingErrorNoError) {
        QVariant value = response.toMap().value("params").toMap().value("value");
        QCOMPARE(value.toInt(), 10); // Mock has value 10 by default...
    }
}

void TestIntegrations::getStateValues_data()
{
    QTest::addColumn<ThingId>("thingId");
    QTest::addColumn<Thing::ThingError>("statusCode");

    QTest::newRow("valid thingId") << ThingId(m_mockThingId) << Thing::ThingErrorNoError;
    QTest::newRow("invalid thingId") << ThingId("094f8024-5caa-48c1-ab6a-de486a92088f") << Thing::ThingErrorThingNotFound;
}

void TestIntegrations::getStateValues()
{
    QFETCH(ThingId, thingId);
    QFETCH(Thing::ThingError, statusCode);

    QVariantMap params;
    params.insert("thingId", thingId);
    QVariant response = injectAndWait("Integrations.GetStateValues", params);

    QCOMPARE(response.toMap().value("params").toMap().value("thingError").toString(), enumValueName(statusCode));
    if (statusCode == Thing::ThingErrorNoError) {
        QVariantList values = response.toMap().value("params").toMap().value("values").toList();
        QCOMPARE(values.count(), 12); // Mock has 12 states...
    }
}

void TestIntegrations::editThings_data()
{
    QTest::addColumn<QString>("name");

    QTest::newRow("change name") << "New name";
    QTest::newRow("change name") << "Foo";
    QTest::newRow("change name") << "Bar";
}

void TestIntegrations::editThings()
{
    QFETCH(QString, name);

    QString originalName = "Test thing";

    // add thing
    QVariantList thingParams;
    QVariantMap httpportParam;
    httpportParam.insert("paramTypeId", mockThingHttpportParamTypeId);
    httpportParam.insert("value", 8889);
    thingParams.append(httpportParam);

    QVariantMap params;
    params.insert("thingClassId", mockThingClassId);
    params.insert("name", originalName);
    params.insert("thingParams", thingParams);
    QVariant response = injectAndWait("Integrations.AddThing", params);
    verifyThingError(response);
    ThingId thingId = ThingId(response.toMap().value("params").toMap().value("thingId").toString());

    // edit thing
    params.clear();
    params.insert("thingId", thingId);
    params.insert("name", name);

    response = injectAndWait("Integrations.EditThing", params);
    verifyThingError(response);

    // verify changed
    QString newName;
    response = injectAndWait("Integrations.GetThings");
    QVariantList things = response.toMap().value("params").toMap().value("things").toList();

    foreach (const QVariant &thingVariant, things) {
        QVariantMap thing = thingVariant.toMap();
        if (ThingId(thing.value("id").toString()) == thingId) {
            newName = thing.value("name").toString();
        }
    }
    QCOMPARE(newName, name);

    restartServer();

    // check if the changed name is still there after loading
    response = injectAndWait("Integrations.GetThings");
    things = response.toMap().value("params").toMap().value("things").toList();
    foreach (const QVariant &thingVariant, things) {
        QVariantMap thing = thingVariant.toMap();
        if (ThingId(thing.value("id").toString()) == thingId) {
            newName = thing.value("name").toString();
            break;
        }
    }
    QCOMPARE(newName, name);

    params.clear();
    params.insert("thingId", thingId.toString());
    response = injectAndWait("Integrations.RemoveThing", params);
    verifyThingError(response);
}

void TestIntegrations::testThingSettings()
{
    // add thing
    QVariantList thingParams;
    QVariantMap httpportParam;
    httpportParam.insert("paramTypeId", mockThingHttpportParamTypeId);
    httpportParam.insert("value", 8889);
    thingParams.append(httpportParam);

    QVariantMap params;
    params.insert("thingClassId", mockThingClassId);
    params.insert("name", "Mock");
    params.insert("thingParams", thingParams);
    QVariant response = injectAndWait("Integrations.AddThing", params);
    verifyThingError(response);
    ThingId thingId = ThingId(response.toMap().value("params").toMap().value("thingId").toString());

    // check if default settings are loaded
    params.clear();
    params.insert("thingId", thingId);
    response = injectAndWait("Integrations.GetThings", params);
    QVariantList things = response.toMap().value("params").toMap().value("things").toList();
    QVERIFY2(things.count() == 1, "Error creating thing");

    QVariantMap thing = things.first().toMap();
    QVERIFY2(ThingId(thing.value("id").toString()) == thingId, "thingId not matching");

    QVariantList settings = thing.value("settings").toList();
    QCOMPARE(settings.count(), 3);

    QCOMPARE(settings.first().toMap().value("paramTypeId").toUuid(), QUuid(mockSettingsSetting1ParamTypeId));
    QVERIFY2(settings.first().toMap().value("value").toInt() == 5, "Setting 1 default value not matching");

    // change a setting
    params.clear();
    params.insert("thingId", thingId);
    settings.clear();
    QVariantMap setting;
    setting.insert("paramTypeId", mockSettingsSetting1ParamTypeId);
    setting.insert("value", 7);
    settings.append(setting);
    params.insert("settings", settings);
    response = injectAndWait("Integrations.SetThingSettings", params);

    // Check if the change happened
    params.clear();
    params.insert("thingId", thingId);
    response = injectAndWait("Integrations.GetThings", params);
    things = response.toMap().value("params").toMap().value("things").toList();
    QVERIFY2(things.count() == 1, "Error creating thing");

    thing = things.first().toMap();
    QVERIFY2(ThingId(thing.value("id").toString()) == thingId, "thingId not matching");

    settings = thing.value("settings").toList();
    QCOMPARE(settings.count(), 3);

    QCOMPARE(settings.first().toMap().value("paramTypeId").toUuid(), QUuid(mockSettingsSetting1ParamTypeId));
    QVERIFY2(settings.first().toMap().value("value").toInt() == 7, "Setting 1 changed value not matching");

    restartServer();

    // Check if the change persisted
    params.clear();
    params.insert("thingId", thingId);
    response = injectAndWait("Integrations.GetThings", params);
    things = response.toMap().value("params").toMap().value("things").toList();
    QVERIFY2(things.count() == 1, "Error creating thing");

    thing = things.first().toMap();
    QVERIFY2(ThingId(thing.value("id").toString()) == thingId, "thingId not matching");

    settings = thing.value("settings").toList();
    QCOMPARE(settings.count(), 3);

    QCOMPARE(settings.first().toMap().value("paramTypeId").toUuid(), QUuid(mockSettingsSetting1ParamTypeId));
    QVERIFY2(settings.first().toMap().value("value").toInt() == 7, "Setting 1 changed value not persisting restart");

}

void TestIntegrations::reconfigureThings_data()
{
    QVariantList asyncChangeThingParams;
    QVariantMap asyncParamDifferent;
    asyncParamDifferent.insert("paramTypeId", mockThingAsyncParamTypeId);
    asyncParamDifferent.insert("value", true);
    asyncChangeThingParams.append(asyncParamDifferent);

    QVariantList httpportChangeThingParams;
    QVariantMap httpportParamDifferent;
    httpportParamDifferent.insert("paramTypeId", mockThingHttpportParamTypeId);
    httpportParamDifferent.insert("value", 8893); // if changing this, change also newPort in reconfigureThings()
    httpportChangeThingParams.append(httpportParamDifferent);

    QVariantList brokenChangedThingParams;
    QVariantMap brokenParamDifferent;
    brokenParamDifferent.insert("paramTypeId", mockThingBrokenParamTypeId);
    brokenParamDifferent.insert("value", true);
    brokenChangedThingParams.append(brokenParamDifferent);

    QVariantList asyncAndPortChangeThingParams;
    asyncAndPortChangeThingParams.append(asyncParamDifferent);
    asyncAndPortChangeThingParams.append(httpportParamDifferent);


    QVariantList changeAllWritableThingParams;
    changeAllWritableThingParams.append(asyncParamDifferent);
    changeAllWritableThingParams.append(httpportParamDifferent);

    QTest::addColumn<bool>("broken");
    QTest::addColumn<QVariantList>("newThingParams");
    QTest::addColumn<Thing::ThingError>("thingError");

    QTest::newRow("valid - change async param") << false << asyncChangeThingParams << Thing::ThingErrorParameterNotWritable;
    QTest::newRow("valid - change httpport param") << false <<  httpportChangeThingParams << Thing::ThingErrorNoError;
    QTest::newRow("invalid - change httpport and async param") << false << asyncAndPortChangeThingParams << Thing::ThingErrorParameterNotWritable;
    QTest::newRow("invalid - change all params (except broken)") << false << changeAllWritableThingParams << Thing::ThingErrorParameterNotWritable;
}

void TestIntegrations::reconfigureThings()
{
    QFETCH(bool, broken);
    QFETCH(QVariantList, newThingParams);
    QFETCH(Thing::ThingError, thingError);

    // add thing
    QVariantMap params;
    params.insert("thingClassId", mockThingClassId);
    params.insert("name", "Thing to edit");
    QVariantList thingParams;
    QVariantMap asyncParam;
    asyncParam.insert("paramTypeId", mockThingAsyncParamTypeId);
    asyncParam.insert("value", false);
    thingParams.append(asyncParam);
    QVariantMap brokenParam;
    brokenParam.insert("paramTypeId", mockThingBrokenParamTypeId);
    brokenParam.insert("value", broken);
    thingParams.append(brokenParam);
    QVariantMap httpportParam;
    httpportParam.insert("paramTypeId", mockThingHttpportParamTypeId);
    httpportParam.insert("value", 8892);
    thingParams.append(httpportParam);
    params.insert("thingParams", thingParams);

    // add a mock
    QVariant response = injectAndWait("Integrations.AddThing", params);
    verifyThingError(response);

    ThingId thingId = ThingId(response.toMap().value("params").toMap().value("thingId").toString());
    QVERIFY(!thingId.isNull());

    // now EDIT the added mock
    response.clear();
    QVariantMap editParams;
    editParams.insert("thingId", thingId);
    editParams.insert("thingParams", newThingParams);
    response = injectAndWait("Integrations.ReconfigureThing", editParams);
    verifyThingError(response, thingError);

    // if the edit should have been successful
    if (thingError == Thing::ThingErrorNoError) {
        response = injectAndWait("Integrations.GetThings", QVariantMap());

        bool found = false;
        foreach (const QVariant &thing, response.toMap().value("params").toMap().value("things").toList()) {
            if (ThingId(thing.toMap().value("id").toString()) == thingId) {
                qDebug() << "found added thing" << thing.toMap().value("params");
                qDebug() << "expected thingParams:" << newThingParams;
                // check if the edit was ok
                verifyParams(newThingParams, thing.toMap().value("params").toList(), false);
                found = true;
                break;
            }
        }
        QVERIFY2(found, "Thing missing in config!");

        // Restart the core instance to check if settings are loaded at startup
        restartServer();

        response = injectAndWait("Integrations.GetThings", QVariantMap());

        found = false;
        foreach (const QVariant &thing, response.toMap().value("params").toMap().value("things").toList()) {
            if (ThingId(thing.toMap().value("id").toString()) == thingId) {
                qDebug() << "found added thing" << thing.toMap().value("params");
                qDebug() << "expected params:" << newThingParams;
                // check if the edit was ok
                verifyParams(newThingParams, thing.toMap().value("params").toList(), false);
                found = true;
                break;
            }
        }
        QVERIFY2(found, "Thing missing in config!");

        // delete it
        params.clear();
        params.insert("thingId", thingId);
        response.clear();
        response = injectAndWait("Integrations.RemoveThing", params);
        verifyThingError(response);
        return;
    } else {
        // The edit was not ok, check if the old params are still there
        response = injectAndWait("Integrations.GetThings", QVariantMap());

        bool found = false;
        foreach (const QVariant &thing, response.toMap().value("params").toMap().value("things").toList()) {
            if (ThingId(thing.toMap().value("id").toString()) == thingId) {
                qDebug() << "found added thing" << thing.toMap().value("params");
                qDebug() << "expected thingParams:" << newThingParams;
                // check if the params are unchanged
                verifyParams(thingParams, thing.toMap().value("params").toList());
                found = true;
                break;
            }
        }
        QVERIFY2(found, "Thing missing in config!");

        // Restart the core instance to check if settings are loaded at startup
        restartServer();

        response = injectAndWait("Integrations.GetThings", QVariantMap());

        found = false;
        foreach (const QVariant &thing, response.toMap().value("params").toMap().value("things").toList()) {
            if (ThingId(thing.toMap().value("id").toString()) == thingId) {
                qDebug() << "found added thing" << thing.toMap().value("params");
                qDebug() << "expected thingParams:" << newThingParams;
                // check if after the reboot the settings are unchanged
                verifyParams(thingParams, thing.toMap().value("params").toList());
                found = true;
                break;
            }
        }
        QVERIFY2(found, "Thing missing in config!");
    }

    // delete it
    params.clear();
    params.insert("thingId", thingId);
    response = injectAndWait("Integrations.RemoveThing", params);
    verifyThingError(response);
}


void TestIntegrations::reconfigureByDiscovery_data()
{
    QTest::addColumn<ThingClassId>("thingClassId");
    QTest::addColumn<int>("resultCount");
    QTest::addColumn<Thing::ThingError>("error");
    QTest::addColumn<QVariantList>("discoveryParams");

    QVariantList discoveryParams;
    QVariantMap resultCountParam;
    resultCountParam.insert("paramTypeId", mockDiscoveryResultCountParamTypeId);
    resultCountParam.insert("value", 2);
    discoveryParams.append(resultCountParam);

    QTest::newRow("discover 2 things with params") << mockThingClassId << 2 << Thing::ThingErrorNoError << discoveryParams;
}

void TestIntegrations::reconfigureByDiscovery()
{
    QFETCH(ThingClassId, thingClassId);
    QFETCH(int, resultCount);
    QFETCH(Thing::ThingError, error);
    QFETCH(QVariantList, discoveryParams);

    qCDebug(dcTests()) << "Discovering...";
    QVariantMap params;
    params.insert("thingClassId", thingClassId);
    params.insert("discoveryParams", discoveryParams);
    QVariant response = injectAndWait("Integrations.DiscoverThings", params);

    verifyThingError(response);
    if (error == Thing::ThingErrorNoError) {
        QCOMPARE(response.toMap().value("params").toMap().value("thingDescriptors").toList().count(), resultCount);
    }

    // add Discovered Thing 1 port 55555
    QVariantList thingDescriptors = response.toMap().value("params").toMap().value("thingDescriptors").toList();

    ThingDescriptorId descriptorId;
    foreach (const QVariant &descriptor, thingDescriptors) {
        // find the thing with port 55555
        if (descriptor.toMap().value("description").toString() == "55555") {
            descriptorId = ThingDescriptorId(descriptor.toMap().value("id").toString());
            qDebug() << descriptorId.toString();
            break;
        }
    }

    QVERIFY(!descriptorId.isNull());

    qCDebug(dcTests()) << "Adding...";

    params.clear();
    response.clear();
    params.insert("thingClassId", thingClassId);
    params.insert("name", "Discoverd mock");
    params.insert("thingDescriptorId", descriptorId);
    response = injectAndWait("Integrations.AddThing", params);

    ThingId thingId(response.toMap().value("params").toMap().value("thingId").toString());
    QVERIFY(!thingId.isNull());

    // and now rediscover and find the existing thing in the discovery results
    qCDebug(dcTests()) << "Re-Discovering...";

    params.clear();
    response.clear();
    params.insert("thingClassId", thingClassId);
    params.insert("discoveryParams", discoveryParams);
    response = injectAndWait("Integrations.DiscoverThings", params);

    verifyThingError(response, error);
    if (error == Thing::ThingErrorNoError) {
        QCOMPARE(response.toMap().value("params").toMap().value("thingDescriptors").toList().count(), resultCount);
    }

    thingDescriptors = response.toMap().value("params").toMap().value("thingDescriptors").toList();

    // find the already added thing
    descriptorId = ThingDescriptorId(); // reset it first
    foreach (const QVariant &descriptor, thingDescriptors) {
        if (descriptor.toMap().value("thingId").toUuid().toString() == thingId.toString()) {
            descriptorId = ThingDescriptorId(descriptor.toMap().value("id").toString());
            break;
        }
    }
    QVERIFY2(!descriptorId.isNull(), QString("Tjhing %1 not found in discovery results: %2").arg(thingId.toString()).arg(qUtf8Printable(QJsonDocument::fromVariant(response).toJson())).toUtf8());

    qCDebug(dcTests()) << "Reconfiguring...";

    response.clear();
    params.clear();
    params.insert("thingDescriptorId", descriptorId);
    // override port param
    QVariantMap portParam;
    portParam.insert("paramTypeId", mockThingHttpportParamTypeId);
    portParam.insert("value", "55556");
    params.insert("thingParams", QVariantList() << portParam);
    response = injectAndWait("Integrations.ReconfigureThing", params);
    verifyThingError(response, error);

    response.clear();
    response = injectAndWait("Integrations.GetThings", QVariantMap());

    QVariantMap thingMap;
    bool found = false;
    foreach (const QVariant &thing, response.toMap().value("params").toMap().value("things").toList()) {
        if (ThingId(thing.toMap().value("id").toString()) == thingId) {
            qDebug() << "found added thing" << thing.toMap().value("params");
            found = true;
            thingMap = thing.toMap();
            break;
        }
    }

    QVERIFY2(found, "Thing missing in config!");
    QCOMPARE(thingMap.value("id").toUuid(), QUuid(thingId));
    if (thingMap.contains("setupComplete"))
        QVERIFY2(thingMap.value("setupComplete").toBool(), "Setup not completed after edit");

    // Note: this shows that by discovery a not editable param (name) can be changed!
    foreach (QVariant param, thingMap.value("params").toList()) {
        if (param.toMap().value("paramTypeId") == mockThingHttpportParamTypeId) {
            QCOMPARE(param.toMap().value("value").toInt(), 55556);
        }
    }

    // check if the daemons are running
    QNetworkAccessManager nam;
    QSignalSpy spy(&nam, &QNetworkAccessManager::finished);

    // check if old daemon is still running (should not)
    QNetworkRequest request(QUrl(QString("http://localhost:%1").arg(55555)));
    QNetworkReply *reply = nam.get(request);
    spy.wait();
    QVERIFY2(reply->error(), "The old daemon is still running");
    reply->deleteLater();

    // check if the daemon is really running on the new port
    request = QNetworkRequest(QUrl(QString("http://localhost:%1").arg(55556)));
    reply = nam.get(request);
    spy.wait();
    QVERIFY2(reply->error() == QNetworkReply::NoError, "The new daemon is not running");
    reply->deleteLater();

    params.clear();
    params.insert("thingId", thingId.toString());
    response = injectAndWait("Integrations.RemoveThing", params);
    verifyThingError(response);
}

void TestIntegrations::reconfigureByDiscoveryAndPair()
{
    QVariantList discoveryParams;
    QVariantMap resultCountParam;
    resultCountParam.insert("paramTypeId", displayPinMockDiscoveryResultCountParamTypeId);
    resultCountParam.insert("value", 1);
    discoveryParams.append(resultCountParam);

    qCDebug(dcTests()) << "Discovering things...";

    QVariantMap params;
    params.insert("thingClassId", displayPinMockThingClassId);
    params.insert("discoveryParams", discoveryParams);
    QVariant response = injectAndWait("Integrations.DiscoverThings", params);

    verifyThingError(response);
    QVariantList thingDescriptors = response.toMap().value("params").toMap().value("thingDescriptors").toList();

    qCDebug(dcTests()) << "Discovery result:" << qUtf8Printable(QJsonDocument::fromVariant(thingDescriptors).toJson(QJsonDocument::Indented));
    QCOMPARE(response.toMap().value("params").toMap().value("thingDescriptors").toList().count(), 1);

    // add Discovered thing 1 port 55555

    QVariant descriptor = thingDescriptors.first();
    ThingDescriptorId descriptorId = ThingDescriptorId(descriptor.toMap().value("id").toString());
    QVERIFY2(!descriptorId.isNull(), "ThingDescriptorId is Null");

    qCDebug(dcTests()) << "Pairing descriptorId:" << descriptorId;

    params.clear();
    response.clear();
    params.insert("thingClassId", displayPinMockThingClassId);
    params.insert("name", "Discoverd mock");
    params.insert("thingDescriptorId", descriptorId);
    response = injectAndWait("Integrations.PairThing", params);
    verifyThingError(response);

    PairingTransactionId pairingTransactionId = PairingTransactionId(response.toMap().value("params").toMap().value("pairingTransactionId").toString());
    qCDebug(dcTests()) << "PairThing result:" << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented));

    qCDebug(dcTests()) << "Confirming pairing for transaction ID" << pairingTransactionId;
    params.clear();
    response.clear();
    params.insert("pairingTransactionId", pairingTransactionId.toString());
    params.insert("secret", "243681");
    response = injectAndWait("Integrations.ConfirmPairing", params);
    verifyThingError(response);

    ThingId thingId(response.toMap().value("params").toMap().value("thingId").toString());
    QVERIFY(!thingId.isNull());

    qCDebug(dcTests()) << "Discovering again...";

    // and now rediscover, and edit the first thing with the second
    params.clear();
    response.clear();
    params.insert("thingClassId", displayPinMockThingClassId);
    params.insert("discoveryParams", discoveryParams);
    response = injectAndWait("Integrations.DiscoverThings", params);

    thingDescriptors = response.toMap().value("params").toMap().value("thingDescriptors").toList();
    qCDebug(dcTests()) << "Discovery result:" << qUtf8Printable(QJsonDocument::fromVariant(thingDescriptors).toJson(QJsonDocument::Indented));

    verifyThingError(response, Thing::ThingErrorNoError);
    QCOMPARE(thingDescriptors.count(), 1);

    descriptor = thingDescriptors.first();
    QVERIFY2(ThingId(descriptor.toMap().value("thingId").toString()) == thingId, "thingId not set in descriptor");

    // get the descriptor again
    descriptorId = ThingDescriptorId(descriptor.toMap().value("id").toString());

    QVERIFY(!descriptorId.isNull());

    qDebug() << "Reconfiguring thing by pairing again" << descriptorId;

    params.clear();
    response.clear();
    params.insert("thingClassId", displayPinMockThingClassId);
    params.insert("name", "Discoverd mock");
    params.insert("thingDescriptorId", descriptorId);
    response = injectAndWait("Integrations.PairThing", params);
    verifyThingError(response);

    pairingTransactionId = PairingTransactionId(response.toMap().value("params").toMap().value("pairingTransactionId").toString());
    qCDebug(dcTests()) << "PairThing result:" << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented));


    qCDebug(dcTests()) << "Confirming pairing for transaction ID" << pairingTransactionId;
    params.clear();
    response.clear();
    params.insert("pairingTransactionId", pairingTransactionId.toString());
    params.insert("secret", "243681");
    response = injectAndWait("Integrations.ConfirmPairing", params);
    verifyThingError(response);

    thingId = ThingId(response.toMap().value("params").toMap().value("thingId").toString());
    QVERIFY(!thingId.isNull());

}

void TestIntegrations::reconfigureAutoThing()
{
    qCDebug(dcTests()) << "Reconfigure auto thing";

    // Get the auto mock
    QList<Thing*> things  = NymeaCore::instance()->thingManager()->findConfiguredThings(autoMockThingClassId);
    QVERIFY2(things.count() > 0, "There needs to be at least one auto-created Mock for this test");

    // Get current auto mock infos
    Thing *currentThing = things.first();
    ThingId thingId = currentThing->id();
    int currentPort = currentThing->paramValue(autoMockThingHttpportParamTypeId).toInt();

    // Trigger reconfigure signal in mock
    QNetworkAccessManager *nam = new QNetworkAccessManager(this);
    QSignalSpy spy(nam, &QNetworkAccessManager::finished);
    QNetworkReply *reply = nam->get(QNetworkRequest(QUrl(QString("http://localhost:%1/reconfigureautodevice").arg(currentPort))));
    spy.wait();
    QCOMPARE(spy.count(), 1);
    reply->deleteLater();

    Thing *thing = NymeaCore::instance()->thingManager()->findConfiguredThing(thingId);
    QVERIFY(thing);
    int newPort = thing->paramValue(autoMockThingHttpportParamTypeId).toInt();
    // Note: reconfigure auto mock increases the http port by 1
    QCOMPARE(newPort, currentPort + 1);
}


void TestIntegrations::removeThing_data()
{
    QTest::addColumn<ThingId>("thingId");
    QTest::addColumn<Thing::ThingError>("thingError");

    QTest::newRow("Existing thing") << ThingId(m_mockThingId) << Thing::ThingErrorNoError;
    QTest::newRow("Not existing thing") << ThingId::createThingId() << Thing::ThingErrorThingNotFound;
    //    QTest::newRow("Auto device") << m_mockThingAutoId << Thing::ThingErrorCreationMethodNotSupported;
}

void TestIntegrations::removeThing()
{
    QFETCH(ThingId, thingId);
    QFETCH(Thing::ThingError, thingError);

    NymeaSettings settings(NymeaSettings::SettingsRoleThings);
    settings.beginGroup("ThingConfig");
    if (thingError == Thing::ThingErrorNoError) {
        settings.beginGroup(m_mockThingId.toString());
        // Make sure we have some config values for this device
        QVERIFY(settings.allKeys().count() > 0);
    }

    QVariantMap params;
    params.insert("thingId", thingId);

    QVariant response = injectAndWait("Integrations.RemoveThing", params);

    verifyThingError(response, thingError);

    if (Thing::ThingErrorNoError) {
        // Make sure the device is gone from settings too
        QCOMPARE(settings.allKeys().count(), 0);
    }
}

void TestIntegrations::removeAutoThing()
{
    // Setup connection to mock client
    QNetworkAccessManager *nam = new QNetworkAccessManager(this);
    QSignalSpy spy(nam, &QNetworkAccessManager::finished);

    // First try to make a manually created device disappear. It must not go away

    QList<Thing*> things = NymeaCore::instance()->thingManager()->findConfiguredThings(mockThingClassId);
    int oldCount = things.count();
    QVERIFY2(oldCount > 0, "There needs to be at least one configured Mock Device for this test");
    Thing *thing = things.first();

    // trigger disappear signal in mock device
    int port = thing->paramValue(autoMockThingHttpportParamTypeId).toInt();
    QNetworkRequest request(QUrl(QString("http://localhost:%1/disappear").arg(port)));
    QNetworkReply *reply = nam->get(request);
    spy.wait();
    QCOMPARE(spy.count(), 1);
    reply->deleteLater();
    QVERIFY2(NymeaCore::instance()->thingManager()->findConfiguredThings(mockThingClassId).count() == oldCount, "Mocked thing has disappeared even though it shouldn't");

    // Ok, now do the same with an autocreated one. It should go away

    things = NymeaCore::instance()->thingManager()->findConfiguredThings(autoMockThingClassId);
    oldCount = things.count();
    QVERIFY2(oldCount > 0, "There needs to be at least one auto-created Mock Device for this test");
    thing = things.first();

    // trigger disappear signal in mock device
    spy.clear();
    port = thing->paramValue(autoMockThingHttpportParamTypeId).toInt();
    request.setUrl(QUrl(QString("http://localhost:%1/disappear").arg(port)));
    reply = nam->get(request);

    spy.wait();
    QCOMPARE(spy.count(), 1);
    reply->deleteLater();

    // Make sure one mock device has disappeared
    QCOMPARE(NymeaCore::instance()->thingManager()->findConfiguredThings(autoMockThingClassId).count(), oldCount - 1);
}

void TestIntegrations::testBrowsing_data()
{
    QTest::addColumn<ThingId>("thingId");

    QTest::newRow("regular mock") << ThingId(m_mockThingId);
    QTest::newRow("async mock") << ThingId(m_mockThingAsyncId);
}

void TestIntegrations::testBrowsing()
{
    QFETCH(ThingId, thingId);

    // Check if mockdevice is browsable
    QVariant response = injectAndWait("Integrations.GetThingClasses");

    QVariantMap mockThingClass;
    foreach (const QVariant &thingClassVariant, response.toMap().value("params").toMap().value("thingClasses").toList()) {
        if (ThingClassId(thingClassVariant.toMap().value("id").toString()) == mockThingClassId) {
            mockThingClass = thingClassVariant.toMap();
        }
    }

    QVERIFY2(ThingClassId(mockThingClass.value("id").toString()) == mockThingClassId, "Could not find mock device");
    QCOMPARE(mockThingClass.value("browsable").toBool(), true);


    // Browse it
    QVariantMap params;
    params.insert("thingId", thingId);
    response = injectAndWait("Integrations.BrowseThing", params);
    QCOMPARE(response.toMap().value("params").toMap().value("thingError").toString(), QString("ThingErrorNoError"));
    QVariantList browserEntries = response.toMap().value("params").toMap().value("items").toList();
    QVERIFY2(browserEntries.count() > 0, "BrowseThing did not return any items.");

    // Browse item 001, it should be a folder with 2 items
    params.insert("itemId", "001");
    response = injectAndWait("Integrations.BrowseThing", params);
    QCOMPARE(response.toMap().value("params").toMap().value("thingError").toString(), QString("ThingErrorNoError"));
    browserEntries = response.toMap().value("params").toMap().value("items").toList();
    QVERIFY2(browserEntries.count() == 2, "BrowseThing did not return 2 items as childs in folder with id 001.");

    // Browse a non-existent item
    params["itemId"] = "this-does-not-exist";
    response = injectAndWait("Integrations.BrowseThing", params);
    browserEntries = response.toMap().value("params").toMap().value("items").toList();
    QCOMPARE(response.toMap().value("params").toMap().value("thingError").toString(), QString("ThingErrorItemNotFound"));
    QCOMPARE(browserEntries.count(), 0);


}

void TestIntegrations::discoverThingsParenting()
{
    // Try to discover a mock child device. We don't have a mockParent yet, so it should fail
    ThingDiscoveryInfo *discoveryInfo = NymeaCore::instance()->thingManager()->discoverThings(childMockThingClassId, ParamList());
    {
        QSignalSpy spy(discoveryInfo, &ThingDiscoveryInfo::finished);
        spy.wait();
    }
    QVERIFY(discoveryInfo->thingDescriptors().count() == 0);


    // Now create a mock parent by discovering...
    discoveryInfo = NymeaCore::instance()->thingManager()->discoverThings(parentMockThingClassId, ParamList());
    {
        QSignalSpy spy(discoveryInfo, &ThingDiscoveryInfo::finished);
        spy.wait();
    }
    QVERIFY(discoveryInfo->thingDescriptors().count() == 1);
    ThingDescriptorId descriptorId = discoveryInfo->thingDescriptors().first().id();

    QSignalSpy addSpy(NymeaCore::instance()->thingManager(), &ThingManager::thingAdded);
    ThingSetupInfo *setupInfo = NymeaCore::instance()->thingManager()->addConfiguredThing(descriptorId, ParamList(), "Mock Parent (Discovered)");
    {
        QSignalSpy spy(setupInfo, &ThingSetupInfo::finished);
        spy.wait();
    }
    QCOMPARE(setupInfo->status(), Thing::ThingErrorNoError);

    addSpy.wait();
    QCOMPARE(addSpy.count(), 2); // Mock parent will also auto-create a child instantly

    Thing *parentThing = addSpy.at(0).first().value<Thing*>();
    qCDebug(dcTests()) << "Added parent:" << parentThing->name();
    QVERIFY(parentThing->thingClassId() == parentMockThingClassId);


    // Ok we have our parent device, let's discover for childs again
    discoveryInfo = NymeaCore::instance()->thingManager()->discoverThings(childMockThingClassId, ParamList());
    {
        QSignalSpy spy(discoveryInfo, &ThingDiscoveryInfo::finished);
        spy.wait();
    }
    QVERIFY(discoveryInfo->thingDescriptors().count() == 1);
    descriptorId = discoveryInfo->thingDescriptors().first().id();

    // Found one! Adding it...
    addSpy.clear();
    setupInfo = NymeaCore::instance()->thingManager()->addConfiguredThing(descriptorId, ParamList(), "Mock Child (Discovered)");
    {
        QSignalSpy spy(setupInfo, &ThingSetupInfo::finished);
        spy.wait();
    }
    QCOMPARE(setupInfo->status(), Thing::ThingErrorNoError);

    QCOMPARE(addSpy.count(), 1);

    Thing *childThing = addSpy.at(0).first().value<Thing*>();
    qCDebug(dcTests()) << "Added child:" << childThing->name();
    QVERIFY(childThing->thingClassId() == childMockThingClassId);

    // Now delete the parent and make sure the child will be deleted too
    QSignalSpy removeSpy(NymeaCore::instance()->thingManager(), &ThingManager::thingRemoved);
    Thing::ThingError ret = NymeaCore::instance()->thingManager()->removeConfiguredThing(parentThing->id());
    QCOMPARE(ret, Thing::ThingErrorNoError);
    QCOMPARE(removeSpy.count(), 3); // The parent, the auto-mock and the discovered mock

}

void TestIntegrations::testExecuteBrowserItem_data()
{
    QTest::addColumn<ThingId>("thingId");
    QTest::addColumn<QString>("itemId");
    QTest::addColumn<QString>("thingError");

    QTest::newRow("regular mock - good item") << ThingId(m_mockThingId) << "002" << "ThingErrorNoError";
    QTest::newRow("regular mock - bad item") << ThingId(m_mockThingId) << "001" << "ThingErrorItemNotExecutable";
    QTest::newRow("async mock - good item") << ThingId(m_mockThingAsyncId) << "002" << "ThingErrorNoError";
}

void TestIntegrations::testExecuteBrowserItem()
{
    QFETCH(ThingId, thingId);
    QFETCH(QString, itemId);
    QFETCH(QString, thingError);

    QVariantMap params;
    params.insert("thingId", thingId);
    params.insert("itemId", itemId);
    QVariant response = injectAndWait("Integrations.ExecuteBrowserItem", params);
    qCDebug(dcTests()) << "resp" << response;

    QCOMPARE(response.toMap().value("params").toMap().value("thingError").toString(), thingError);
}

void TestIntegrations::testExecuteBrowserItemAction_data()
{
    QTest::addColumn<ThingId>("thingId");

    QTest::newRow("regular mock") << ThingId(m_mockThingId);
    QTest::newRow("async mock") << ThingId(m_mockThingAsyncId);
}

void TestIntegrations::testExecuteBrowserItemAction()
{
    QFETCH(ThingId, thingId);

    QVariantMap getItemsParams;
    getItemsParams.insert("thingId", thingId);
    QVariant response = injectAndWait("Integrations.BrowseThing", getItemsParams);
    QCOMPARE(response.toMap().value("status").toString(), QString("success"));

    QVariantList browserEntries = response.toMap().value("params").toMap().value("items").toList();
    QVERIFY(browserEntries.count() > 2);

    QVariantMap item002; // Find the item we need for this test
    foreach (const QVariant &item, browserEntries) {
        if (item.toMap().value("id").toString() == "002") {
            item002 = item.toMap();
            break;
        }
    }
    QVERIFY2(item002.value("id").toString() == QString("002"), "Item with context actions not found");
    QVERIFY2(item002.value("actionTypeIds").toList().count() > 0, "Item doesn't have actionTypeIds");
    QVERIFY2(ActionTypeId(item002.value("actionTypeIds").toList().first().toString()) == mockAddToFavoritesBrowserItemActionTypeId, "AddToFavorites action type id not found in item");


    // Browse favorites
    // ID is "favorites" in mock
    // It should be ampty at this point
    getItemsParams.insert("itemId", "favorites");
    response = injectAndWait("Integrations.BrowseThing", getItemsParams);
    QCOMPARE(response.toMap().value("status").toString(), QString("success"));

    browserEntries = response.toMap().value("params").toMap().value("items").toList();
    QVERIFY2(browserEntries.count() == 0, "Favorites should be empty at this point");

    // Now add an item to the favorites
    QVariantMap actionParams;
    actionParams.insert("thingId", thingId);
    actionParams.insert("itemId", "002");
    actionParams.insert("actionTypeId", mockAddToFavoritesBrowserItemActionTypeId);
    response = injectAndWait("Integrations.ExecuteBrowserItemAction", actionParams);
    QCOMPARE(response.toMap().value("status").toString(), QString("success"));
    QCOMPARE(response.toMap().value("params").toMap().value("thingError").toString(), QString("ThingErrorNoError"));

    // Fetch the list again
    response = injectAndWait("Integrations.BrowseThing", getItemsParams);
    QCOMPARE(response.toMap().value("status").toString(), QString("success"));

    browserEntries = response.toMap().value("params").toMap().value("items").toList();
    QCOMPARE(browserEntries.count(), 1);

    QString favoriteItemId = browserEntries.first().toMap().value("id").toString();
    QVERIFY2(!favoriteItemId.isEmpty(), "ItemId is empty in favorites list");

    // Now remove the again from favorites
    actionParams.clear();
    actionParams.insert("thingId", thingId);
    actionParams.insert("itemId", favoriteItemId);
    actionParams.insert("actionTypeId", mockRemoveFromFavoritesBrowserItemActionTypeId);
    response = injectAndWait("Integrations.ExecuteBrowserItemAction", actionParams);
    QCOMPARE(response.toMap().value("status").toString(), QString("success"));
    QCOMPARE(response.toMap().value("params").toMap().value("thingError").toString(), QString("ThingErrorNoError"));

    // Fetch the list again
    response = injectAndWait("Integrations.BrowseThing", getItemsParams);
    QCOMPARE(response.toMap().value("status").toString(), QString("success"));

    browserEntries = response.toMap().value("params").toMap().value("items").toList();
    QCOMPARE(browserEntries.count(), 0);

}

void TestIntegrations::executeAction_data()
{
    QTest::addColumn<ThingId>("thingId");
    QTest::addColumn<ActionTypeId>("actionTypeId");
    QTest::addColumn<QVariantList>("actionParams");
    QTest::addColumn<Thing::ThingError>("error");

    QVariantList params;
    QVariantMap param1;
    param1.insert("paramTypeId", mockWithParamsActionParam1ParamTypeId);
    param1.insert("value", 5);
    params.append(param1);
    QVariantMap param2;
    param2.insert("paramTypeId", mockWithParamsActionParam2ParamTypeId);
    param2.insert("value", true);
    params.append(param2);

    QTest::newRow("valid action") << ThingId(m_mockThingId) << mockWithParamsActionTypeId << params << Thing::ThingErrorNoError;
    QTest::newRow("invalid thingId") << ThingId::createThingId() << mockWithParamsActionTypeId << params << Thing::ThingErrorThingNotFound;
    QTest::newRow("invalid actionTypeId") << ThingId(m_mockThingId) << ActionTypeId::createActionTypeId() << params << Thing::ThingErrorActionTypeNotFound;
    QTest::newRow("missing params") << ThingId(m_mockThingId) << mockWithParamsActionTypeId << QVariantList() << Thing::ThingErrorMissingParameter;
    QTest::newRow("async action") << ThingId(m_mockThingId) << mockAsyncActionTypeId << QVariantList() << Thing::ThingErrorNoError;
    QTest::newRow("broken action") << ThingId(m_mockThingId) << mockFailingActionTypeId << QVariantList() << Thing::ThingErrorSetupFailed;
    QTest::newRow("async broken action") << ThingId(m_mockThingId) << mockAsyncFailingActionTypeId << QVariantList() << Thing::ThingErrorSetupFailed;
}

void TestIntegrations::executeAction()
{
    QFETCH(ThingId, thingId);
    QFETCH(ActionTypeId, actionTypeId);
    QFETCH(QVariantList, actionParams);
    QFETCH(Thing::ThingError, error);

    QVariantMap params;
    params.insert("actionTypeId", actionTypeId);
    params.insert("thingId", thingId);
    params.insert("params", actionParams);
    QVariant response = injectAndWait("Integrations.ExecuteAction", params);
    qDebug() << "executeActionresponse" << response;
    verifyError(response, "thingError", enumValueName(error));

    // Fetch action execution history from mock device
    QNetworkAccessManager nam;
    QSignalSpy spy(&nam, &QNetworkAccessManager::finished);

    QNetworkRequest request(QUrl(QString("http://localhost:%1/actionhistory").arg(m_mockThing1Port)));
    QNetworkReply *reply = nam.get(request);
    spy.wait();
    QCOMPARE(spy.count(), 1);
    reply->deleteLater();
    QByteArray data = reply->readAll();

    if (error == Thing::ThingErrorNoError) {
        QVERIFY2(actionTypeId == ActionTypeId(data), QString("ActionTypeId mismatch. Got %1, Expected: %2")
                     .arg(ActionTypeId(data).toString()).arg(actionTypeId.toString()).toLatin1().data());
    } else {
        QVERIFY2(data.length() == 0, QString("Data is %1, should be empty.").arg(QString(data)).toLatin1().data());
    }

    // cleanup for the next run
    spy.clear();
    request.setUrl(QUrl(QString("http://localhost:%1/clearactionhistory").arg(m_mockThing1Port)));
    reply = nam.get(request);
    spy.wait();
    QCOMPARE(spy.count(), 1);
    reply->deleteLater();

    spy.clear();
    request.setUrl(QUrl(QString("http://localhost:%1/actionhistory").arg(m_mockThing1Port)));
    reply = nam.get(request);
    spy.wait();
    QCOMPARE(spy.count(), 1);
    reply->deleteLater();
    data = reply->readAll();
    qDebug() << "cleared data:" << data;

}

void TestIntegrations::triggerEvent()
{
    enableNotifications({"Integrations"});
    QList<Thing*> things = NymeaCore::instance()->thingManager()->findConfiguredThings(mockThingClassId);
    QVERIFY2(things.count() > 0, "There needs to be at least one configured Mock Device for this test");
    Thing *thing = things.first();

    QSignalSpy spy(NymeaCore::instance()->thingManager(), &ThingManager::eventTriggered);
    QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);

    // Setup connection to mock client
    QNetworkAccessManager nam;

    // trigger event in mock device
    int port = thing->paramValue(mockThingHttpportParamTypeId).toInt();
    QNetworkRequest request(QUrl(QString("http://localhost:%1/generateevent?eventtypeid=%2").arg(port).arg(mockEvent1EventTypeId.toString())));
    QNetworkReply *reply = nam.get(request);
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);

    // Lets wait for the notification
    spy.wait();
    QVERIFY(spy.count() > 0);
    for (int i = 0; i < spy.count(); i++ ){
        Event event = spy.at(i).at(0).value<Event>();
        if (event.thingId() == thing->id()) {
            // Make sure the event contains all the stuff we expect
            QCOMPARE(event.eventTypeId(), mockEvent1EventTypeId);
        }
    }

    // Check for the notification on JSON API
    QVariantList notifications;
    notifications = checkNotifications(notificationSpy, "Integrations.EventTriggered");
    QVERIFY2(notifications.count() == 1, "Should get Integrations.EventTriggered notification");
    QVariantMap notificationContent = notifications.first().toMap().value("params").toMap();

    QCOMPARE(notificationContent.value("event").toMap().value("thingId").toUuid().toString(), thing->id().toString());
    QCOMPARE(notificationContent.value("event").toMap().value("eventTypeId").toUuid().toString(), mockEvent1EventTypeId.toString());
}

void TestIntegrations::triggerStateChangeSignal()
{
    enableNotifications({"Integrations"});

    QList<Thing*> things = NymeaCore::instance()->thingManager()->findConfiguredThings(mockThingClassId);
    QVERIFY2(things.count() > 0, "There needs to be at least one configured Mock for this test");
    Thing *thing = things.first();

    QSignalSpy spy(NymeaCore::instance()->thingManager(), &ThingManager::thingStateChanged);
    QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);

    // Setup connection to mock client
    QNetworkAccessManager nam;

    // trigger state changed event in mock device
    int port = thing->paramValue(mockThingHttpportParamTypeId).toInt();
    QNetworkRequest request(QUrl(QString("http://localhost:%1/setstate?%2=%3").arg(port).arg(mockIntStateTypeId.toString()).arg(37)));
    QNetworkReply *reply = nam.get(request);
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);

    // Lets wait for the notification
    spy.wait();
    QVERIFY(spy.count() == 1);
    Thing *t = spy.at(0).at(0).value<Thing*>();
    QCOMPARE(t->id(), thing->id());
    StateTypeId stId = spy.at(0).at(1).value<StateTypeId>();
    QCOMPARE(stId, mockIntStateTypeId);
    QVariant value = spy.at(0).at(2);
    QCOMPARE(value.toInt(), 37);

    // Check for the notification on JSON API
    QVariantList notifications;
    notifications = checkNotifications(notificationSpy, "Integrations.StateChanged");
    QVERIFY2(notifications.count() == 1, "Should get Integrations.StateChanged notification");
    QVariantMap notificationContent = notifications.first().toMap().value("params").toMap();

    QCOMPARE(notificationContent.value("thingId").toUuid().toString(), thing->id().toString());
    QCOMPARE(notificationContent.value("stateTypeId").toUuid().toString(), mockIntStateTypeId.toString());
    QCOMPARE(notificationContent.value("value").toInt(), 37);
}

void TestIntegrations::params()
{
    Event event;
    ParamList params;
    ParamTypeId id = ParamTypeId::createParamTypeId();
    Param p(id, "foo bar");
    params.append(p);
    event.setParams(params);

    QVERIFY(event.param(id).value().toString() == "foo bar");
    QVERIFY(!event.param(ParamTypeId::createParamTypeId()).value().isValid());
}

void TestIntegrations::ensureValueClamping()
{
    const double tolerance = 1e-6;

    QVariant result = ThingUtils::ensureValueClamping(5.789, QMetaType::Double, 0.0, 10.0, 0.1);
    QVERIFY(qAbs(result.toDouble() - 5.8) < tolerance);

    result = ThingUtils::ensureValueClamping(1.3, QMetaType::Double, 1.0, 5.0, 0.5);
    QVERIFY(qAbs(result.toDouble() - 1.5) < tolerance);

    result = ThingUtils::ensureValueClamping(11.0, QMetaType::Double, 0.0, 10.0, 3.0);
    QVERIFY(qAbs(result.toDouble() - 9.0) < tolerance);

    result = ThingUtils::ensureValueClamping(7, QMetaType::Int, 0, 20, 5);
    QCOMPARE(result.toInt(), 5);

    result = ThingUtils::ensureValueClamping(-5, QMetaType::Int, 0, 20, 0);
    QCOMPARE(result.toInt(), 0);
}

void TestIntegrations::dynamicMinMax()
{
    enableNotifications({"Integrations"});

    QList<Thing*> things = NymeaCore::instance()->thingManager()->findConfiguredThings(mockThingClassId);
    QVERIFY2(things.count() > 0, "There needs to be at least one configured Mock for this test");
    Thing *thing = things.first();

    QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);

    // Setup connection to mock client
    QNetworkAccessManager nam;

    // trigger state changed event in mock device
    qCDebug(dcTests()) << "Changing state in mock thing to 11";
    int port = thing->paramValue(mockThingHttpportParamTypeId).toInt();
    QNetworkRequest request(QUrl(QString("http://localhost:%1/setstate?%2=%3").arg(port).arg(mockIntWithLimitsStateTypeId.toString()).arg(11)));
    QNetworkReply *reply = nam.get(request);
    connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);

    // Check for the notification on JSON API
    notificationSpy.wait();
    QVariantList notifications;
    notifications = checkNotifications(notificationSpy, "Integrations.StateChanged");
    QVERIFY2(notifications.count() == 1, QString("Expected 1 Integrations.StateChanged notification. Received: %1").arg(notifications.count()).toUtf8());
    QVariantMap notificationContent = notifications.first().toMap().value("params").toMap();

    QCOMPARE(notificationContent.value("thingId").toUuid().toString(), thing->id().toString());
    QCOMPARE(notificationContent.value("stateTypeId").toUuid().toString(), mockIntWithLimitsStateTypeId.toString());
    QCOMPARE(notificationContent.value("value").toInt(), 11);
    QCOMPARE(notificationContent.value("minValue").toInt(), 0);
    QCOMPARE(notificationContent.value("maxValue").toInt(), 50);

    // set the max to 8
    qCDebug(dcTests()) << "Changing state max value to 8";
    notificationSpy.clear();
    thing->setStateMaxValue(mockIntWithLimitsStateTypeId, 8);

    // Check for the notification on JSON API, state chould adapt to new max
    notificationSpy.wait();
    notifications = checkNotifications(notificationSpy, "Integrations.StateChanged");
    QVERIFY2(notifications.count() == 1, "Should get Integrations.StateChanged notification");
    notificationContent = notifications.first().toMap().value("params").toMap();

    QCOMPARE(notificationContent.value("thingId").toUuid().toString(), thing->id().toString());
    QCOMPARE(notificationContent.value("stateTypeId").toUuid().toString(), mockIntWithLimitsStateTypeId.toString());
    QCOMPARE(notificationContent.value("value").toInt(), 8);

    // Try to execute an action on the api that exceeds the max value
    qCDebug(dcTests()) << "Executing action with invalid max value (40)";
    QVariantMap actionParams;
    actionParams.insert("thingId", thing->id());
    actionParams.insert("actionTypeId", mockIntWithLimitsActionTypeId);
    QVariantMap valueParam;
    valueParam.insert("paramTypeId", mockIntWithLimitsActionIntWithLimitsParamTypeId);
    // intentionally between thingClass max and dynamic max
    valueParam.insert("value", 40);
    actionParams.insert("params", QVariantList() << valueParam);
    QVariant response = injectAndWait("Integrations.ExecuteAction", actionParams);
    verifyThingError(response, Thing::ThingErrorInvalidParameter);

    // Set the max to 100
    qCDebug(dcTests()) << "Changing max state value to 100";
    thing->setStateMaxValue(mockIntWithLimitsStateTypeId, 100);

    // And try to execute the action again
    // intentionally greater than thingClass max
    qCDebug(dcTests()) << "Executing action with valid max (52)";
    valueParam.insert("value", 52);
    actionParams.insert("params", QVariantList() << valueParam);
    response = injectAndWait("Integrations.ExecuteAction", actionParams);
    verifyThingError(response, Thing::ThingErrorNoError);

}

void TestIntegrations::asyncSetupEmitsSetupStatusUpdate()
{
    QVariantMap configuredDevices = injectAndWait("Integrations.GetThings").toMap();
    foreach (const QVariant &deviceVariant, configuredDevices.value("params").toMap().value("things").toList()) {
        QVariantMap device = deviceVariant.toMap();
        qCDebug(dcTests()) << "configured thing" << device.value("setupStatus");
    }

    // Restart the core instance to check if settings are loaded at startup
    restartServer();
    enableNotifications({"Integrations"});

    QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);

    configuredDevices = injectAndWait("Integrations.GetThings").toMap();
    QList<QUuid> thingsWithSetupInProgress;
    foreach (const QVariant &deviceVariant, configuredDevices.value("params").toMap().value("things").toList()) {
        QVariantMap thing = deviceVariant.toMap();
        qCDebug(dcTests()) << "Configured thing" << thing.value("name").toString() << "with setup status" << thing.value("setupStatus").toString();
        if (thing.value("setupStatus").toString() == "ThingSetupStatusInProgress") {
            thingsWithSetupInProgress << thing.value("id").toUuid();
        }
    }
    QVERIFY2(thingsWithSetupInProgress.count() > 0, "This test requires at least one device that is still being set up at this point.");

    QDateTime maxTime = QDateTime::currentDateTime().addSecs(10);
    while (QDateTime::currentDateTime() < maxTime && thingsWithSetupInProgress.count() > 0) {
        QList<QList<QVariant>> notifications = notificationSpy;
        while (notifications.count() > 0) {
            QByteArray notificationData = notifications.takeFirst().at(1).toByteArray();
            QVariantMap notification = QJsonDocument::fromJson(notificationData).toVariant().toMap();
            if (notification.value("notification").toString() == "Integrations.ThingChanged") {
                QString setupStatus = notification.value("params").toMap().value("thing").toMap().value("setupStatus").toString();
                if (setupStatus == "ThingSetupStatusComplete") {
                    qCDebug(dcTests()) << "Device setup completed for" << notification.value("params").toMap().value("thing").toMap().value("name").toString();
                    ThingId thingId = notification.value("params").toMap().value("thing").toMap().value("id").toUuid();
                    thingsWithSetupInProgress.removeAll(thingId);
                }
            }
        }
        notificationSpy.clear();
        if (thingsWithSetupInProgress.count() > 0) {
            notificationSpy.wait();
        }
    }

    QVERIFY2(thingsWithSetupInProgress.isEmpty(), "Some things did not finish the setup!");
}

void TestIntegrations::testTranslations()
{
    // switch language to de_AT
    QVariantMap params;
    params.insert("locale", "de_AT");
    QVariantMap handShake = injectAndWait("JSONRPC.Hello", params).toMap();
    QCOMPARE(handShake.value("params").toMap().value("locale").toString(), QString("de_AT"));

    QVariantMap thingClasses = injectAndWait("Integrations.GetThingClasses").toMap();
    bool found = false;
    foreach (const QVariant &tcVariant, thingClasses.value("params").toMap().value("thingClasses").toList()) {
        QVariantMap tcMap = tcVariant.toMap();
        if (tcMap.value("id").toUuid() == autoMockThingClassId) {
            found = true;

            // Verify thingClass' displayName is translated
            QCOMPARE(tcMap.value("displayName").toString(), QString("Mock \"Thing\" (automatisch erstellt)"));

            // Verify paramTypes are translated
            bool ptFound = false;
            foreach (const QVariant &ptVariant, tcMap.value("paramTypes").toList()) {
                QVariantMap ptMap = ptVariant.toMap();
                if (ptMap.value("id").toUuid() == autoMockThingAsyncParamTypeId) {
                    ptFound = true;
                    QCOMPARE(ptMap.value("displayName").toString(), QString("asynchron"));
                }
            }
            QVERIFY2(ptFound, "ParamType not found in mock thing class.");


            // Verify settings are translated
            bool sFound = false;
            foreach (const QVariant &sVariant, tcMap.value("settingsTypes").toList()) {
                QVariantMap sMap = sVariant.toMap();
                if (sMap.value("id").toUuid() == autoMockSettingsMockSettingParamTypeId) {
                    sFound = true;
                    QCOMPARE(sMap.value("displayName").toString(), QString("Mock-Einstellung"));
                }
            }
            QVERIFY2(sFound, "SettingsType not found in mock thing class.");

            // Verify stateTypes are translated
            bool stFound = false;
            foreach (const QVariant &stVariant, tcMap.value("stateTypes").toList()) {
                QVariantMap stMap = stVariant.toMap();
                if (stMap.value("id").toUuid() == autoMockIntStateTypeId) {
                    stFound = true;
                    QCOMPARE(stMap.value("displayName").toString(), QString("Simulierter Integer Zustand"));
                }
            }
            QVERIFY2(stFound, "StateType not found in mock thing class.");

            // Verify eventTypes are translated
            bool etFound = false;
            foreach (const QVariant &etVariant, tcMap.value("eventTypes").toList()) {
                QVariantMap etMap = etVariant.toMap();
                if (etMap.value("id").toUuid() == autoMockEvent1EventTypeId) {
                    etFound = true;
                    QCOMPARE(etMap.value("displayName").toString(), QString("Mock Ereignis 1"));
                }
            }
            QVERIFY2(etFound, "EventType not found in mock thing class.");

            // Verify actionTypes are translated
            bool atFound = false;
            foreach (const QVariant &atVariant, tcMap.value("actionTypes").toList()) {
                QVariantMap atMap = atVariant.toMap();
                if (atMap.value("id").toUuid() == autoMockWithParamsActionTypeId) {
                    atFound = true;
                    QCOMPARE(atMap.value("displayName").toString(), QString("Mock Aktion 1 (mit Parameter)"));
                }
            }
            QVERIFY2(atFound, "ActionType not found in mock thing class.");
        }
    }
    QVERIFY2(found, "Mock thing class not found.");

}

#include "testintegrations.moc"
QTEST_MAIN(TestIntegrations)
