// SPDX-License-Identifier: LGPL-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 Lesser 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with nymea. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "nymeatestbase.h"
#include "nymeacore.h"
#include "nymeasettings.h"
#include "servers/mocktcpserver.h"
#include "usermanager/usermanager.h"
#include "logging/logengine.h"

using namespace nymeaserver;

Q_LOGGING_CATEGORY(dcTests, "Tests")

#include "../plugins/mock/plugininfo.h"

NymeaTestBase::NymeaTestBase(QObject *parent) :
    QObject(parent),
    m_commandId(0)
{
    qRegisterMetaType<QNetworkReply*>();
    std::srand(QDateTime::currentMSecsSinceEpoch());
    m_mockThing1Port = 1337 + (std::rand() % 10000);
    m_mockThing2Port = 7331 + (std::rand() % 10000);

    // Important for settings
    QCoreApplication::instance()->setOrganizationName("nymea-test");
}

void NymeaTestBase::initTestCase(const QString &loggingRules, bool disableLogEngine)
{
    qCDebug(dcTests) << "NymeaTestBase starting.";

    // Keep this over restart of core instance
    m_disableLogEngine = disableLogEngine;

    // If testcase asserts cleanup won't do. Lets clear any previous test run settings leftovers
    NymeaSettings rulesSettings(NymeaSettings::SettingsRoleRules);
    rulesSettings.clear();
    NymeaSettings thingSettings(NymeaSettings::SettingsRoleThings);
    thingSettings.clear();
    NymeaSettings pluginSettings(NymeaSettings::SettingsRolePlugins);
    pluginSettings.clear();
    QDir dir(NymeaSettings::cachePath() + "/thingstates/");
    dir.removeRecursively();

    // Reset to default settings
    NymeaSettings nymeadSettings(NymeaSettings::SettingsRoleGlobal);
    nymeadSettings.clear();

    nymeadSettings.beginGroup("Logs");
    nymeadSettings.setValue("logDBName", "nymeatest");
    nymeadSettings.endGroup();

    if (loggingRules.isEmpty()) {
        QLoggingCategory::setFilterRules("*.debug=false\nApplication.debug=true\nTests.debug=true\nMock.debug=true");
    } else {
        QLoggingCategory::setFilterRules(loggingRules);
    }

    // Start the server
    qCDebug(dcTests()) << "Setting up nymea core instance";
    NymeaCore::instance()->init(QStringList(), m_disableLogEngine);

    // Wait unitl the server is initialized
    QSignalSpy coreInitializedSpy(NymeaCore::instance(), &NymeaCore::initialized);
    QVERIFY(coreInitializedSpy.wait());
    qApp->processEvents();
    qCDebug(dcTests()) << "Nymea core instance initialized. Creating dummy user.";

    foreach (const UserInfo &userInfo, NymeaCore::instance()->userManager()->users()) {
        NymeaCore::instance()->userManager()->removeUser(userInfo.username());
    }
    NymeaCore::instance()->userManager()->removeUser("");

    // Yes, we're intentionally mixing upper/lower case email here... username should not be case sensitive
    NymeaCore::instance()->userManager()->removeUser("dummy");
    NymeaCore::instance()->userManager()->createUser("dummy", "DummyPW1!", "dummy@nymea.io", "Dummy", Types::PermissionScopeAdmin);
    m_apiToken = NymeaCore::instance()->userManager()->authenticate("Dummy", "DummyPW1!", "testcase");

    if (MockTcpServer::servers().isEmpty()) {
        qCWarning(dcTests) << "no mock tcp server found";
        exit(-1);
    }

    // Add the mock
    m_mockTcpServer = MockTcpServer::servers().first();
    m_clientId = QUuid::createUuid();
    m_mockTcpServer->clientConnected(m_clientId);

    qCDebug(dcTests()) << "Starting JSON handshake";
    QVariant response = injectAndWait("JSONRPC.Hello");

    createMock();

    response = injectAndWait("Integrations.GetThings", {});
    foreach (const QVariant &thing, response.toMap().value("params").toMap().value("things").toList()) {
        if (thing.toMap().value("thingClassId").toUuid() == autoMockThingClassId) {
            m_mockThingAutoId = ThingId(thing.toMap().value("id").toString());
        }
    }
}

void NymeaTestBase::cleanupTestCase()
{
    NymeaCore::instance()->destroy(NymeaCore::ShutdownReasonTerm);

}

void NymeaTestBase::cleanup()
{
    // In case a test deleted the mock, lets recreate it.
    if (NymeaCore::instance()->thingManager()->findConfiguredThings(mockThingClassId).count() == 0) {
        createMock();
    }
}

QVariant NymeaTestBase::injectAndWait(const QString &method, const QVariantMap &params, const QUuid &clientId, const QByteArray &tokenOverride)
{
    QVariantMap call;
    call.insert("id", m_commandId);
    call.insert("method", method);
    call.insert("params", params);
    call.insert("token", tokenOverride == "default" ? m_apiToken : tokenOverride);

    QJsonDocument jsonDoc = QJsonDocument::fromVariant(call);
    QSignalSpy spy(m_mockTcpServer, &MockTcpServer::outgoingData);

    m_mockTcpServer->injectData(clientId.isNull() ? m_clientId : clientId, jsonDoc.toJson(QJsonDocument::Compact) + "\n");

    int loop = 0;

    while (loop < 10) {

        if (spy.count() == 0 || loop > 0) {
            spy.wait();
        }

        for (int i = 0; i < spy.count(); i++) {
            // Make sure the response it a valid JSON string
            QJsonParseError error;
            jsonDoc = QJsonDocument::fromJson(spy.at(i).last().toByteArray(), &error);
            if (error.error != QJsonParseError::NoError) {
                qWarning() << "JSON parser error" << error.errorString() << spy.at(i).last().toByteArray();
                return QVariant();
            }
            QVariantMap response = jsonDoc.toVariant().toMap();

            // skip notifications
            if (response.contains("notification"))
                continue;

            if (response.value("id").toInt() == m_commandId) {
                m_commandId++;
                return jsonDoc.toVariant();
            }
        }
        loop++;
    }

    m_commandId++;
    return QVariant();
}

QVariant NymeaTestBase::checkNotification(const QSignalSpy &spy, const QString &notification)
{
    //qDebug() << "Got" << spy.count() << "notifications while waiting for" << notification;
    for (int i = 0; i < spy.count(); i++) {
        // Make sure the response it a valid JSON string
        QJsonParseError error;
        QJsonDocument jsonDoc = QJsonDocument::fromJson(spy.at(i).last().toByteArray(), &error);
        if (error.error != QJsonParseError::NoError) {
            qWarning() << "JSON parser error" << error.errorString();
            return QVariant();
        }

        QVariantMap response = jsonDoc.toVariant().toMap();
        if (response.value("notification").toString() == notification) {
            return jsonDoc.toVariant();
        }
    }
    return QVariant();
}

QVariantList NymeaTestBase::checkNotifications(const QSignalSpy &spy, const QString &notification)
{
//    qWarning() << "Got" << spy.count() << "notifications while waiting for" << notification;
    QVariantList notificationList;
    for (int i = 0; i < spy.count(); i++) {
        // Make sure the response it a valid JSON string
        QJsonParseError error;
        QJsonDocument jsonDoc = QJsonDocument::fromJson(spy.at(i).last().toByteArray(), &error);
//        qCDebug(dcTests()) << "Got packet:" << qUtf8Printable(jsonDoc.toJson());
        if (error.error != QJsonParseError::NoError) {
            qCWarning(dcTests()) << "JSON parser error" << error.errorString();
            return notificationList;
        }

        QVariantMap response = jsonDoc.toVariant().toMap();
        if (response.value("notification").toString() == notification) {
            notificationList.append(jsonDoc.toVariant());
        }
    }
    return notificationList;
}

QVariant NymeaTestBase::getAndWait(const QNetworkRequest &request, const int &expectedStatus)
{
    QNetworkAccessManager nam;
    connect(&nam, &QNetworkAccessManager::sslErrors, [](QNetworkReply *reply, const QList<QSslError> &) {
        reply->ignoreSslErrors();
    });
    QSignalSpy clientSpy(&nam, &QNetworkAccessManager::finished);

    QNetworkReply *reply = nam.get(request);

    if (clientSpy.count() == 0) {
        clientSpy.wait();
    }
    qCDebug(dcTests()) << "*** finished" << reply->isFinished() << reply->error() << reply->errorString();

    if (clientSpy.count() == 0) {
        qCWarning(dcTests()) << "Got no response for get request";
        reply->deleteLater();
        return QVariant();
    }

    QByteArray data = reply->readAll();
    verifyReply(reply, data, expectedStatus);

    qCDebug(dcTests()) << "Data is:" << data;

    reply->deleteLater();

    QJsonParseError error;
    QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
    if (error.error != QJsonParseError::NoError) {
        qWarning() << "JSON parser error" << error.errorString();
        return QVariant();
    }

    return jsonDoc.toVariant();
}

QVariant NymeaTestBase::deleteAndWait(const QNetworkRequest &request, const int &expectedStatus)
{
    QNetworkAccessManager nam;
    connect(&nam, &QNetworkAccessManager::sslErrors, [](QNetworkReply *reply, const QList<QSslError> &) {
        reply->ignoreSslErrors();
    });
    QSignalSpy clientSpy(&nam, &QNetworkAccessManager::finished);

    QNetworkReply *reply = nam.deleteResource(request);

    if (clientSpy.count() == 0) {
        clientSpy.wait();
    }

    if (clientSpy.count() == 0) {
        qWarning() << "Got no response for delete request";
        reply->deleteLater();
        return QVariant();
    }

    QByteArray data = reply->readAll();
    verifyReply(reply, data, expectedStatus);

    reply->deleteLater();

    QJsonParseError error;
    QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
    if (error.error != QJsonParseError::NoError) {
        qWarning() << "JSON parser error" << error.errorString();
        return QVariant();
    }

    return jsonDoc.toVariant();
}

QVariant NymeaTestBase::postAndWait(const QNetworkRequest &request, const QVariant &params, const int &expectedStatus)
{
    QNetworkAccessManager nam;
    connect(&nam, &QNetworkAccessManager::sslErrors, [](QNetworkReply *reply, const QList<QSslError> &) {
        reply->ignoreSslErrors();
    });
    QSignalSpy clientSpy(&nam, &QNetworkAccessManager::finished);

    QByteArray payload = QJsonDocument::fromVariant(params).toJson(QJsonDocument::Compact);

    QNetworkReply *reply = nam.post(request, payload);

    if (clientSpy.count() == 0) {
        clientSpy.wait();
    }

    if (clientSpy.count() == 0) {
        qWarning() << "Got no response for post request";
        reply->deleteLater();
        return QVariant();
    }



    QByteArray data = reply->readAll();
    verifyReply(reply, data, expectedStatus);

    reply->deleteLater();

    QJsonParseError error;
    QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
    if (error.error != QJsonParseError::NoError) {
        qWarning() << "JSON parser error" << error.errorString() << qUtf8Printable(data);
        return QVariant();
    }

    return jsonDoc.toVariant();
}


QVariant NymeaTestBase::putAndWait(const QNetworkRequest &request, const QVariant &params, const int &expectedStatus)
{
    QNetworkAccessManager nam;
    connect(&nam, &QNetworkAccessManager::sslErrors, [](QNetworkReply *reply, const QList<QSslError> &) {
        reply->ignoreSslErrors();
    });
    QSignalSpy clientSpy(&nam, &QNetworkAccessManager::finished);

    QByteArray payload = QJsonDocument::fromVariant(params).toJson(QJsonDocument::Compact);

    QNetworkReply *reply = nam.put(request, payload);

    if (clientSpy.count() == 0) {
        clientSpy.wait();
    }

    if (clientSpy.count() == 0) {
        qWarning() << "Got no response for put request";
        reply->deleteLater();
        return QVariant();
    }

    QByteArray data = reply->readAll();
    verifyReply(reply, data, expectedStatus);

    reply->deleteLater();

    QJsonParseError error;
    QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
    if (error.error != QJsonParseError::NoError) {
        qWarning() << "JSON parser error" << error.errorString();
        return QVariant();
    }

    return jsonDoc.toVariant();
}

void NymeaTestBase::verifyReply(QNetworkReply *reply, const QByteArray &data, const int &expectedStatus)
{
    int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    QCOMPARE(statusCode, expectedStatus);

    Q_UNUSED(data)
//    if (!data.isEmpty()) {
//        QJsonParseError error;
//        QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
//        QCOMPARE(error.error, QJsonParseError::NoError);
//        Q_UNUSED(jsonDoc);
//    }
}

void NymeaTestBase::enableNotifications(const QStringList &namespaces)
{
    QStringList variantList;
    foreach (const QString &ns, namespaces) {
        variantList << ns;
    }
    std::sort(variantList.begin(), variantList.end());
    QVariantMap notificationParams;
    notificationParams.insert("namespaces", variantList);
    QVariant response = injectAndWait("JSONRPC.SetNotificationStatus", notificationParams);
    QStringList resultList = response.toMap().value("params").toMap().value("namespaces").toStringList();
    std::sort(resultList.begin(), resultList.end());
    QCOMPARE(resultList, variantList);
}

bool NymeaTestBase::disableNotifications()
{
    QVariantMap notificationParams;
    notificationParams.insert("enabled", false);
    QVariant response = injectAndWait("JSONRPC.SetNotificationStatus", notificationParams);
    if (response.toMap().value("params").toMap().value("enabled").toBool() != false) {
        return false;
    }
    qDebug() << "Notifications disabled.";
    return true;
}

void NymeaTestBase::waitForDBSync()
{
    while (NymeaCore::instance()->logEngine()->jobsRunning()) {
        qApp->processEvents();
        QTest::qWait(500);
    }
}

void NymeaTestBase::restartServer()
{
    // Destroy and recreate the core instance...
    qCDebug(dcTests()) << "Tearing down server instance";
    NymeaCore::instance()->destroy(NymeaCore::ShutdownReasonRestart);
    qCDebug(dcTests()) << "Restarting server instance";
    NymeaCore::instance()->init(QStringList(), m_disableLogEngine);
    QSignalSpy coreSpy(NymeaCore::instance(), &NymeaCore::initialized);
    coreSpy.wait();
    m_mockTcpServer = MockTcpServer::servers().first();
    m_mockTcpServer->clientConnected(m_clientId);

    injectAndWait("JSONRPC.Hello");
}

void NymeaTestBase::clearLoggingDatabase(const QString &source)
{
    NymeaCore::instance()->logEngine()->clear(source);
    waitForDBSync();
}

void NymeaTestBase::createMock()
{
    QVariantMap params;
    params.insert("name", "Test Mock");
    params.insert("thingClassId", mockThingClassId.toString());

    QVariantList thingParams;
    QVariantMap httpPortParam;
    httpPortParam.insert("paramTypeId", mockThingHttpportParamTypeId.toString());
    httpPortParam.insert("value", m_mockThing1Port);
    thingParams.append(httpPortParam);
    params.insert("thingParams", thingParams);

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

    verifyError(response, "thingError", "ThingErrorNoError");

    m_mockThingId = ThingId(response.toMap().value("params").toMap().value("thingId").toString());
    QVERIFY2(!m_mockThingId.isNull(), "Newly created mock thing id must not be null.");
}
