// 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-energy-plugin-nymea.
*
* nymea-energy-plugin-nymea.s 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-energy-plugin-nymea.s 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-energy-plugin-nymea. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "testspotmarket.h"

#include <QElapsedTimer>
#include <hardware/electricity.h>

#include "../../../energyplugin/smartchargingmanager.h"
#include "../../mocks/spotmarketprovider/spotmarketdataprovidermock.h"

TestSpotmarket::TestSpotmarket(QObject *parent):
    EnergyTestBase(parent)
{
    qRegisterMetaType<TimeFrames>();
}

void TestSpotmarket::getAvailableProviders()
{
    QVariant response = injectAndWait("NymeaEnergy.GetAvailableSpotMarketProviders");
    QVariantList providerList = response.toMap().value("params").toMap().value("providers").toList();
    QVERIFY2(providerList.count() > 0, "Available providers list is empty.");
    foreach (const QVariant &providerVariant, providerList) {
        QVariantMap providerMap = providerVariant.toMap();
        QVERIFY(providerMap.contains("providerId"));
        QVERIFY(!providerMap.value("providerId").toUuid().isNull());
        QVERIFY(providerMap.contains("name"));
        QVERIFY(providerMap.contains("website"));
    }
}

void TestSpotmarket::getSpotmarketEntriesAndChargingSchedules()
{
    QVariant response, notification;
    QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);;

    // Initially disable
    response = injectAndWait("NymeaEnergy.SetSpotMarketConfiguration", {{"enabled", false}});
    verifyEnergyError(response);

    // Make sure initially everything is disabled and unconfigured
    response = injectAndWait("NymeaEnergy.GetSpotMarketConfiguration");
    //qCDebug(dcTests()) << qUtf8Printable(QJsonDocument::fromVariant(response).toJson());
    QCOMPARE(response.toMap().value("params").toMap().value("enabled").toBool(), false);
    QCOMPARE(response.toMap().value("params").toMap().value("available").toBool(), false);

    // Get empty score entries
    response = injectAndWait("NymeaEnergy.GetSpotMarketScoreEntries");
    QVariantList emptyScoreEnrties = response.toMap().value("params").toMap().value("spotMarketScoreEntries").toList();
    QVERIFY2(emptyScoreEnrties.isEmpty(), "Fetched spotmarket entries list is empty.");

    // Get providers
    response = injectAndWait("NymeaEnergy.GetAvailableSpotMarketProviders");
    QVariantList providerList = response.toMap().value("params").toMap().value("providers").toList();
    QVERIFY2(providerList.count() > 0, "Available providers list is empty.");
    QUuid providerId = providerList.first().toMap().value("providerId").toUuid();

    notificationSpy.clear();

    // Enable spot market provider
    QVariantMap params;
    params.insert("enabled", true);
    params.insert("providerId", providerId);
    response = injectAndWait("NymeaEnergy.SetSpotMarketConfiguration", params);
    verifyEnergyError(response);

    // Verify notifications
    QVariantList scoreEnrties;

    // First notification is empty, since the entries are cleard
    if (notificationSpy.count() == 0) notificationSpy.wait();
    notification = checkNotification(notificationSpy, "NymeaEnergy.SpotMarketScoreEntriesChanged");
    //qCDebug(dcTests()) << qUtf8Printable(QJsonDocument::fromVariant(notification).toJson());
    QVERIFY(notificationSpy.count() >= 1);
    scoreEnrties = notification.toMap().value("params").toMap().value("spotMarketScoreEntries").toList();
    QVERIFY2(scoreEnrties.count() == 0, "Expected empty score entries notification");

    notificationSpy.clear();

    // Second notifications should contain actual data...
    if (notificationSpy.count() == 0) notificationSpy.wait();
    notification = checkNotification(notificationSpy, "NymeaEnergy.SpotMarketScoreEntriesChanged");
    //qCDebug(dcTests()) << qUtf8Printable(QJsonDocument::fromVariant(notification).toJson());
    QVERIFY(notificationSpy.count() >= 1);
    scoreEnrties = notification.toMap().value("params").toMap().value("spotMarketScoreEntries").toList();
    QVERIFY2(scoreEnrties.count() > 0, "Expected score entries in notification");

    notificationSpy.clear();

    // Get score entries
    response = injectAndWait("NymeaEnergy.GetSpotMarketScoreEntries");
    QVariantList fetchedScoreEnrties = response.toMap().value("params").toMap().value("spotMarketScoreEntries").toList();
    QVERIFY2(fetchedScoreEnrties.count() > 0, "Fetched spotmarket entries list is empty.");
    QCOMPARE(scoreEnrties.count(), fetchedScoreEnrties.count());

    // =====================================================================================

    // Now add a car and plan some charging schedules for today based on the available schedules

    // Set phase power limit
    params.clear(); response.clear(); notification.clear(); notificationSpy.clear();
    response = injectAndWait("NymeaEnergy.SetPhasePowerLimit", QVariantMap({{"phasePowerLimit", 50}}));
    QVERIFY(response.toMap().value("params").toMap().value("energyError").toString() == "EnergyErrorNoError");

    // Add mock meter
    QUuid meterThingId = addMeter();
    QVERIFY2(!meterThingId.isNull(), "Did not receive valid ThingId");
    if (notificationSpy.count() == 0) notificationSpy.wait();
    notification = checkNotification(notificationSpy, "Energy.RootMeterChanged");
    QCOMPARE(notification.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);

    // Make sure this is our root meter now
    response = injectAndWait("Energy.GetRootMeter");
    QCOMPARE(response.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);
    notificationSpy.clear();

    // Add the charger
    QUuid evChargerId = addCharger("ABC", 16);
    QVERIFY2(!evChargerId.isNull(), "Did not receive valid ThingId");
    if (notificationSpy.count() == 0) notificationSpy.wait();
    checkNotification(notificationSpy, "Integrations.ThingAdded");

    // Add the car
    QUuid assignedCarId = addCar();
    QVERIFY2(!assignedCarId.isNull(), "Did not receive valid ThingId");
    if (notificationSpy.count() == 0) notificationSpy.wait();
    checkNotification(notificationSpy, "Integrations.ThingAdded");


    // Set charger states
    QNetworkReply *reply = nullptr;

    reply = setChargerStates(true, false, true, "ABC", 6, 32);
    QSignalSpy setChargerStatesReplySpy(reply, &QNetworkReply::finished);
    if (setChargerStatesReplySpy.count() == 0) setChargerStatesReplySpy.wait();
    QCOMPARE(reply->error(), QNetworkReply::NoError);

    // Set charging configuration with our charger and car
    QVariantMap chargingConfigurationMap;
    chargingConfigurationMap.insert("evChargerId", evChargerId);
    chargingConfigurationMap.insert("assignedCarId", assignedCarId);
    chargingConfigurationMap.insert("chargingMode", "ChargingModeEco");
    chargingConfigurationMap.insert("dailySpotMarketPercentage", 20);

    notificationSpy.clear();

    qCDebug(dcTests()) << "Sending charging configuration:" << qUtf8Printable(QJsonDocument::fromVariant(chargingConfigurationMap).toJson(QJsonDocument::Indented));
    response = injectAndWait("NymeaEnergy.SetChargingConfiguration", QVariantMap({{"chargingConfiguration", chargingConfigurationMap}}));
    QCOMPARE(response.toMap().value("status").toString(), QString("success"));
    verifyEnergyError(response);

    // Verify notifications
    QVariantList chargingSchedules;

    if (notificationSpy.count() == 0) notificationSpy.wait();
    notification = checkNotification(notificationSpy, "NymeaEnergy.ChargingSchedulesChanged");
    qCDebug(dcTests()) << qUtf8Printable(QJsonDocument::fromVariant(notification).toJson());
    QVERIFY(notificationSpy.count() >= 1);
    chargingSchedules = notification.toMap().value("params").toMap().value("chargingSchedules").toList();
    QVERIFY2(!chargingSchedules.isEmpty(), "No score entries in the notification");

    // Get score entries
    response = injectAndWait("NymeaEnergy.GetChargingSchedules");
    QVariantList fetchedChargingSchedules = response.toMap().value("params").toMap().value("chargingSchedules").toList();
    QVERIFY2(fetchedChargingSchedules.count() > 0, "Fetched spotmarket entries list is empty.");
    QCOMPARE(chargingSchedules.count(), fetchedChargingSchedules.count());
}

void TestSpotmarket::clearChargingSchedulesWhenTargetPercentageReached()
{
    QVariantMap params;
    QVariant response, notification;
    QNetworkReply *reply = nullptr;
    QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);

    response = injectAndWait("NymeaEnergy.GetChargingSchedules");
    QVariantList chargingSchedules = response.toMap().value("params").toMap().value("chargingSchedules").toList();
    QVERIFY2(chargingSchedules.isEmpty(), "Expected no charging schedules before setting up the test devices.");

    response = injectAndWait("NymeaEnergy.SetSpotMarketConfiguration", {{"enabled", false}});
    verifyEnergyError(response);

    response = injectAndWait("NymeaEnergy.GetAvailableSpotMarketProviders");
    QVariantList providerList = response.toMap().value("params").toMap().value("providers").toList();
    QVERIFY2(providerList.count() > 0, "Available providers list is empty.");
    QUuid providerId = providerList.first().toMap().value("providerId").toUuid();

    params.clear();
    params.insert("enabled", true);
    params.insert("providerId", providerId);
    notificationSpy.clear();
    response = injectAndWait("NymeaEnergy.SetSpotMarketConfiguration", params);
    verifyEnergyError(response);

    QVariantList spotMarketScoreEntries;
    QElapsedTimer scoreEntriesTimer;
    scoreEntriesTimer.start();
    while (spotMarketScoreEntries.isEmpty() && scoreEntriesTimer.elapsed() < 5000) {
        if (notificationSpy.count() == 0) {
            notificationSpy.wait(200);
        }

        response = injectAndWait("NymeaEnergy.GetSpotMarketScoreEntries");
        spotMarketScoreEntries = response.toMap().value("params").toMap().value("spotMarketScoreEntries").toList();

        if (spotMarketScoreEntries.isEmpty()) {
            QTest::qWait(100);
        }
    }

    QVERIFY2(!spotMarketScoreEntries.isEmpty(), "Expected score entries after enabling the spot-market provider.");

    response = injectAndWait("NymeaEnergy.SetPhasePowerLimit", QVariantMap({{"phasePowerLimit", 50}}));
    verifyEnergyError(response);

    notificationSpy.clear();
    QUuid meterThingId = addMeter();
    QVERIFY2(!meterThingId.isNull(), "Did not receive valid ThingId");
    if (notificationSpy.count() == 0) notificationSpy.wait();
    notification = checkNotification(notificationSpy, "Energy.RootMeterChanged");
    QCOMPARE(notification.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);

    notificationSpy.clear();
    QUuid evChargerId = addCharger("ABC", 16);
    QVERIFY2(!evChargerId.isNull(), "Did not receive valid ThingId");
    if (notificationSpy.count() == 0) notificationSpy.wait();
    checkNotification(notificationSpy, "Integrations.ThingAdded");

    notificationSpy.clear();
    QUuid assignedCarId = addCar();
    QVERIFY2(!assignedCarId.isNull(), "Did not receive valid ThingId");
    if (notificationSpy.count() == 0) notificationSpy.wait();
    checkNotification(notificationSpy, "Integrations.ThingAdded");

    reply = setCarStates(20, 48, 6, 3);
    QSignalSpy setCarStatesReplySpy(reply, &QNetworkReply::finished);
    if (setCarStatesReplySpy.count() == 0) setCarStatesReplySpy.wait();
    QCOMPARE(reply->error(), QNetworkReply::NoError);

    reply = setChargerStates(true, false, true, "ABC", 6, 16);
    QSignalSpy setChargerStatesReplySpy(reply, &QNetworkReply::finished);
    if (setChargerStatesReplySpy.count() == 0) setChargerStatesReplySpy.wait();
    QCOMPARE(reply->error(), QNetworkReply::NoError);

    QVariantMap chargingConfigurationMap;
    chargingConfigurationMap.insert("evChargerId", evChargerId);
    chargingConfigurationMap.insert("assignedCarId", assignedCarId);
    chargingConfigurationMap.insert("chargingMode", "ChargingModeEcoWithTargetTime");
    chargingConfigurationMap.insert("endDateTime", QDateTime::currentDateTime().addSecs(2 * 3600).toMSecsSinceEpoch() / 1000);
    chargingConfigurationMap.insert("targetPercentage", 80);
    chargingConfigurationMap.insert("stopOnTargetPercentage", true);

    response = injectAndWait("NymeaEnergy.SetChargingConfiguration", QVariantMap({{"chargingConfiguration", chargingConfigurationMap}}));
    verifyEnergyError(response);

    response = injectAndWait("NymeaEnergy.GetChargingSchedules");
    chargingSchedules = response.toMap().value("params").toMap().value("chargingSchedules").toList();
    QVERIFY2(!chargingSchedules.isEmpty(), "Expected spot-market charging schedules before the target is reached.");

    reply = setCarStates(80, 48, 6, 3);
    QSignalSpy updateCarStatesReplySpy(reply, &QNetworkReply::finished);
    if (updateCarStatesReplySpy.count() == 0) updateCarStatesReplySpy.wait();
    QCOMPARE(reply->error(), QNetworkReply::NoError);

    response = injectAndWait("NymeaEnergy.GetChargingStates");
    QVariantList chargingStates = response.toMap().value("params").toMap().value("chargingStates").toList();
    QCOMPARE(chargingStates.count(), 1);
    QCOMPARE(chargingStates.first().toMap().value("targetPercentageReached").toBool(), true);

    QElapsedTimer chargingSchedulesTimer;
    chargingSchedulesTimer.start();
    while (!chargingSchedules.isEmpty() && chargingSchedulesTimer.elapsed() < 5000) {
        response = injectAndWait("NymeaEnergy.GetChargingSchedules");
        chargingSchedules = response.toMap().value("params").toMap().value("chargingSchedules").toList();

        if (!chargingSchedules.isEmpty()) {
            QTest::qWait(100);
        }
    }

    if (!chargingSchedules.isEmpty()) {
        qCDebug(dcTests()) << "Remaining schedules after target reached:"
                           << qUtf8Printable(QJsonDocument::fromVariant(chargingSchedules).toJson(QJsonDocument::Indented));
    }
    QVERIFY2(chargingSchedules.isEmpty(), "Charging schedules were not cleared after the target percentage was reached.");

    response = injectAndWait("NymeaEnergy.SetPhasePowerLimit", QVariantMap({{"phasePowerLimit", 51}}));
    verifyEnergyError(response);

    response = injectAndWait("NymeaEnergy.GetChargingSchedules");
    chargingSchedules = response.toMap().value("params").toMap().value("chargingSchedules").toList();
    QVERIFY2(chargingSchedules.isEmpty(), "Charging schedules were recreated after the target percentage was reached.");
}

void TestSpotmarket::addCharingInfo_data()
{
    QTest::addColumn<bool>("addingCharger");
    QTest::addColumn<bool>("addingCar");
    QTest::addColumn<bool>("carValid");
    QTest::addColumn<QString>("chargingMode");
    QTest::addColumn<QDateTime>("endDateTime");
    QTest::addColumn<QList<int>>("repeatDays");
    QTest::addColumn<int>("targetPercentage");
    QTest::addColumn<bool>("spotMarketEnabled");
    QTest::addColumn<int>("dailySpotMarketPercentage");
    QTest::addColumn<EnergyManager::EnergyError>("expectedError");

    QDateTime endDateTime = QDateTime::currentDateTime().addDays(1);
    endDateTime.setTime(QTime(7, 0));

    QTest::newRow("Default ON") << true << true << true << "ChargingModeEco" << endDateTime << QList<int>{} << 100 << true << 0 << EnergyManager::EnergyErrorNoError;
    QTest::newRow("Default OFF") << true << true << true << "ChargingModeEco" << endDateTime << QList<int>{} << 100 << false << 0 << EnergyManager::EnergyErrorNoError;
    QTest::newRow("Spot market enabled, charging disabled") << true << true << true << "ChargingModeEco" << endDateTime << QList<int>{} << 100 << false << 0 << EnergyManager::EnergyErrorNoError;
    QTest::newRow("Daily percentage out of range") << true << true << true << "ChargingModeEco" << endDateTime << QList<int>{} << 100 << true << 101 << EnergyManager::EnergyErrorInvalidParameter;
}

void TestSpotmarket::addCharingInfo()
{
    QFETCH(bool, addingCharger);
    QFETCH(bool, addingCar);
    QFETCH(bool, carValid);
    QFETCH(QString, chargingMode);
    QFETCH(QDateTime, endDateTime);
    QFETCH(QList<int>, repeatDays);
    QFETCH(int, targetPercentage);
    QFETCH(bool, spotMarketEnabled);
    QFETCH(int, dailySpotMarketPercentage);
    QFETCH(EnergyManager::EnergyError, expectedError);

    QVariant response, notification;
    QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);;

    // Set phase power limit
    uint phasePowerLimit = 25000;
    response = injectAndWait("NymeaEnergy.SetPhasePowerLimit", QVariantMap({{"phasePowerLimit", phasePowerLimit}}));
    verifyEnergyError(response);

    removeDevices();

    // Add mock meter
    QUuid meterThingId = addMeter();
    QVERIFY2(!meterThingId.isNull(), "Did not receive valid ThingId");
    if (notificationSpy.count() == 0) notificationSpy.wait();
    notification = checkNotification(notificationSpy, "Energy.RootMeterChanged");
    QCOMPARE(notification.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);

    // Make sure this is our root meter now
    response = injectAndWait("Energy.GetRootMeter");
    QCOMPARE(response.toMap().value("params").toMap().value("rootMeterThingId").toUuid(), meterThingId);
    notificationSpy.clear();

    // Add the charger
    QUuid evChargerId;
    if (addingCharger) {
        evChargerId = addCharger("ABC", 16);
        QVERIFY2(!evChargerId.isNull(), "Did not receive valid ThingId");
        if (notificationSpy.count() == 0) notificationSpy.wait();
        checkNotification(notificationSpy, "Integrations.ThingAdded");
    } else {
        evChargerId = QUuid::createUuid();
    }

    // Add the car
    QUuid assignedCarId;
    if (addingCar) {
        assignedCarId = addCar();
        QVERIFY2(!assignedCarId.isNull(), "Did not receive valid ThingId");
        if (notificationSpy.count() == 0) notificationSpy.wait();
        checkNotification(notificationSpy, "Integrations.ThingAdded");
    }

    if (!carValid)
        assignedCarId = QUuid::createUuid();


    // Get the list of providers and set the first one...
    response = injectAndWait("NymeaEnergy.GetAvailableSpotMarketProviders");
    QVariantList providerList = response.toMap().value("params").toMap().value("providers").toList();
    QVERIFY2(providerList.count() > 0, "Available providers list is empty.");
    QUuid providerId = providerList.first().toMap().value("providerId").toUuid();

    notificationSpy.clear();

    QVariantMap params;
    params.insert("enabled", spotMarketEnabled);
    if (!providerId.isNull())
        params.insert("providerId", providerId);

    // Configure spot market if desired
    response = injectAndWait("NymeaEnergy.SetSpotMarketConfiguration", params);
    verifyEnergyError(response);

    // Make sure it is enabled/disabled
    response = injectAndWait("NymeaEnergy.GetSpotMarketConfiguration");
    qCDebug(dcTests()) << qUtf8Printable(QJsonDocument::fromVariant(response).toJson());
    QCOMPARE(response.toMap().value("params").toMap().value("enabled").toBool(), spotMarketEnabled);

    // Set charging configuration with our charger and car
    QVariantMap chargingConfigurationMap;
    chargingConfigurationMap.insert("evChargerId", evChargerId);
    if (!assignedCarId.isNull())
        chargingConfigurationMap.insert("assignedCarId", assignedCarId);

    chargingConfigurationMap.insert("chargingMode", chargingMode);
    chargingConfigurationMap.insert("dailySpotMarketPercentage", dailySpotMarketPercentage);
    chargingConfigurationMap.insert("endDateTime", endDateTime.toMSecsSinceEpoch() / 1000);
    QVariantList repDaysList;
    foreach (int day, repeatDays) {
        repDaysList.append(day);
    }
    chargingConfigurationMap.insert("repeatDays", repDaysList);
    chargingConfigurationMap.insert("targetPercentage", targetPercentage);

    //qCDebug(dcTests()) << "Sending charging configuration:" << qUtf8Printable(QJsonDocument::fromVariant(chargingConfigurationMap).toJson(QJsonDocument::Indented));
    response = injectAndWait("NymeaEnergy.SetChargingConfiguration", QVariantMap({{"chargingConfiguration", chargingConfigurationMap}}));

    QCOMPARE(response.toMap().value("status").toString(), QString("success"));
    verifyEnergyError(response, expectedError);

}

void TestSpotmarket::setConfiguration_data()
{
    QTest::addColumn<bool>("enabled");
    QTest::addColumn<bool>("useValidProvider");
    QTest::addColumn<bool>("useNullProvider");
    QTest::addColumn<EnergyManager::EnergyError>("expectedError");

    QTest::newRow("Enable valid") << true << true << false << EnergyManager::EnergyErrorNoError;
    QTest::newRow("Disable valid") << false << true << false << EnergyManager::EnergyErrorNoError;
    QTest::newRow("Enable with invalid provider") << true << false << false << EnergyManager::EnergyErrorInvalidParameter;
    QTest::newRow("Enable with null provider id") << true << false << true << EnergyManager::EnergyErrorInvalidParameter;
}

void TestSpotmarket::setConfiguration()
{
    QFETCH(bool, enabled);
    QFETCH(bool, useValidProvider);
    QFETCH(bool, useNullProvider);
    QFETCH(EnergyManager::EnergyError, expectedError);

    QVariant response, notification;
    QSignalSpy notificationSpy(m_mockTcpServer, &MockTcpServer::outgoingData);;

    // Initially disable
    response = injectAndWait("NymeaEnergy.SetSpotMarketConfiguration", {{"enabled", false}});
    verifyEnergyError(response);

    // Make sure initially everything is disabled and unconfigured
    response = injectAndWait("NymeaEnergy.GetSpotMarketConfiguration");
    //qCDebug(dcTests()) << qUtf8Printable(QJsonDocument::fromVariant(response).toJson());
    QCOMPARE(response.toMap().value("params").toMap().value("enabled").toBool(), false);
    QCOMPARE(response.toMap().value("params").toMap().value("available").toBool(), false);

    // Get the list of providers and set the first one...
    QUuid providerId;
    if (useValidProvider) {
        response = injectAndWait("NymeaEnergy.GetAvailableSpotMarketProviders");
        QVariantList providerList = response.toMap().value("params").toMap().value("providers").toList();
        QVERIFY2(providerList.count() > 0, "Available providers list is empty.");
        providerId = providerList.first().toMap().value("providerId").toUuid();
    }

    if (!useValidProvider)
        providerId = QUuid::createUuid();

    if (useNullProvider)
        providerId = QUuid();

    notificationSpy.clear();

    QVariantMap params;
    params.insert("enabled", enabled);
    if (!providerId.isNull())
        params.insert("providerId", providerId);

    response = injectAndWait("NymeaEnergy.SetSpotMarketConfiguration", params);
    verifyEnergyError(response, expectedError);

    if (expectedError == EnergyManager::EnergyErrorNoError) {
        // Verify notification
        if (notificationSpy.count() == 0) notificationSpy.wait();
        notification = checkNotification(notificationSpy, "NymeaEnergy.SpotMarketConfigurationChanged");
        qCDebug(dcTests()) << qUtf8Printable(QJsonDocument::fromVariant(notification).toJson());
        QVERIFY(notificationSpy.count() >= 1);

        response = injectAndWait("NymeaEnergy.GetSpotMarketConfiguration");
        qCDebug(dcTests()) << qUtf8Printable(QJsonDocument::fromVariant(response).toJson());
        QCOMPARE(response.toMap().value("params").toMap().value("enabled").toBool(), enabled);
    }
}


void TestSpotmarket::testMockProvider_data()
{
    QTest::addColumn<QDateTime>("currentDateTime");
    QTest::addColumn<int>("expectedScheduleCount");
    QTest::addColumn<bool>("nextDayAvailableWeighted");

    QTest::newRow("Mock provider 11:00") << EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(11, 0, 0)) << 13 << false;
    QTest::newRow("Mock provider 11:15") << EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(11, 15, 0)) << 13 << false;
    QTest::newRow("Mock provider 12:00") << EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(12, 0, 0)) << 36 << true;
    QTest::newRow("Mock provider 12:15") << EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(12, 15, 0)) << 36 << true;
    QTest::newRow("Mock provider 13:00") << EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(13, 0, 0)) << 35 << true;
    QTest::newRow("Mock provider 13:15") << EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(13, 15, 0)) << 35 << true;
    QTest::newRow("Mock provider 00:00") << EnergyTestBase::utcDateTime(QDate::currentDate().addDays(1), QTime(00, 00, 0)) << 24 << false;
}

void TestSpotmarket::testMockProvider()
{
    QFETCH(QDateTime, currentDateTime);
    QFETCH(int, expectedScheduleCount);
    QFETCH(bool, nextDayAvailableWeighted);

    SpotMarketManager *manager = new SpotMarketManager(m_networkAccessManager, this);
    QVERIFY2(!manager->enabled(), "SpotMarketManager initally enabled");

    SpotMarketDataProviderMock *mockProvider = new SpotMarketDataProviderMock(m_networkAccessManager, manager);
    QVERIFY2(mockProvider->prepareResourceData(":/resources/dataset-1.json", currentDateTime), "Could not load resource data for testing the spotmarket manager.");
    QVERIFY2(manager->registerProvider(mockProvider), "Failed to register mock provider");
    QVERIFY2(manager->changeProvider(mockProvider->providerId()), "failed to change provider to mock provider");

    mockProvider->setCurrentDataTime(currentDateTime);

    QCOMPARE(manager->currentProvider(), qobject_cast<SpotMarketDataProvider*>(mockProvider));
    manager->setEnabled(true);

    QSignalSpy dataChangedSpy(manager, &SpotMarketManager::scoreEntriesUpdated);
    mockProvider->refreshData();

    if (dataChangedSpy.count() == 0) dataChangedSpy.wait();
    QCOMPARE(dataChangedSpy.count(), 1);
    QCOMPARE(manager->currentProvider()->scoreEntries().count(), expectedScheduleCount);
    QCOMPARE(manager->weightedScoreEntries(currentDateTime.date()).count(), (expectedScheduleCount > 24 ? expectedScheduleCount - 24 : expectedScheduleCount));
    if (nextDayAvailableWeighted) {
        QCOMPARE(manager->weightedScoreEntries(currentDateTime.date().addDays(1)).count(), 24);
    }

    manager->setEnabled(false);
    delete manager;
}

void TestSpotmarket::testTimeFrameFusing_data()
{
    QTest::addColumn<TimeFrames>("timeFrames");
    QTest::addColumn<TimeFrames>("fusedTimeFrames");

    QTest::newRow("2 hours") << TimeFrames({ TimeFrame(QDateTime(QDate::currentDate(), QTime(4, 0, 0)), QDateTime(QDate::currentDate(), QTime(5, 0, 0))),
                                             TimeFrame(QDateTime(QDate::currentDate(), QTime(5, 0, 0)), QDateTime(QDate::currentDate(), QTime(6, 0, 0))) })
                             << TimeFrames({ TimeFrame(QDateTime(QDate::currentDate(), QTime(4, 0, 0)), QDateTime(QDate::currentDate(), QTime(6, 0, 0))) });

    QTest::newRow("4 hours") << TimeFrames({ TimeFrame(QDateTime(QDate::currentDate(), QTime(4, 0, 0)), QDateTime(QDate::currentDate(), QTime(5, 0, 0))),
                                             TimeFrame(QDateTime(QDate::currentDate(), QTime(5, 0, 0)), QDateTime(QDate::currentDate(), QTime(6, 0, 0))),
                                             TimeFrame(QDateTime(QDate::currentDate(), QTime(10, 0, 0)), QDateTime(QDate::currentDate(), QTime(11, 0, 0))),
                                             TimeFrame(QDateTime(QDate::currentDate(), QTime(11, 0, 0)), QDateTime(QDate::currentDate(), QTime(12, 0, 0))) })
                             << TimeFrames({ TimeFrame(QDateTime(QDate::currentDate(), QTime(4, 0, 0)), QDateTime(QDate::currentDate(), QTime(6, 0, 0))),
                                             TimeFrame(QDateTime(QDate::currentDate(), QTime(10, 0, 0)), QDateTime(QDate::currentDate(), QTime(12, 0, 0))) });

    QTest::newRow("partial alone") << TimeFrames({ TimeFrame(QDateTime(QDate::currentDate(), QTime(4, 0, 0)), QDateTime(QDate::currentDate(), QTime(4, 30, 0))),
                                                   TimeFrame(QDateTime(QDate::currentDate(), QTime(6, 0, 0)), QDateTime(QDate::currentDate(), QTime(7, 0, 0))) })
                                   << TimeFrames({ TimeFrame(QDateTime(QDate::currentDate(), QTime(4, 0, 0)), QDateTime(QDate::currentDate(), QTime(4, 30, 0))),
                                                   TimeFrame(QDateTime(QDate::currentDate(), QTime(6, 0, 0)), QDateTime(QDate::currentDate(), QTime(7, 0, 0))) });

    QTest::newRow("2 partial fused") << TimeFrames({ TimeFrame(QDateTime(QDate::currentDate(), QTime(4, 0, 0)), QDateTime(QDate::currentDate(), QTime(4, 30, 0))),
                                                     TimeFrame(QDateTime(QDate::currentDate(), QTime(5, 0, 0)), QDateTime(QDate::currentDate(), QTime(5, 30, 0))) })
                                     << TimeFrames({ TimeFrame(QDateTime(QDate::currentDate(), QTime(4, 30, 0)), QDateTime(QDate::currentDate(), QTime(5, 30, 0))) });


    QTest::newRow("3 partial fused") << TimeFrames({ TimeFrame(QDateTime(QDate::currentDate(), QTime(4, 0, 0)), QDateTime(QDate::currentDate(), QTime(4, 30, 0))),
                                                     TimeFrame(QDateTime(QDate::currentDate(), QTime(5, 0, 0)), QDateTime(QDate::currentDate(), QTime(5, 30, 0))),
                                                     TimeFrame(QDateTime(QDate::currentDate(), QTime(6, 0, 0)), QDateTime(QDate::currentDate(), QTime(6, 30, 0))) })
                                     << TimeFrames({ TimeFrame(QDateTime(QDate::currentDate(), QTime(4, 30, 0)), QDateTime(QDate::currentDate(), QTime(5, 30, 0))),
                                                     TimeFrame(QDateTime(QDate::currentDate(), QTime(6, 0, 0)), QDateTime(QDate::currentDate(), QTime(6, 30, 0)))});



}

void TestSpotmarket::testTimeFrameFusing()
{
    QFETCH(TimeFrames, timeFrames);
    QFETCH(TimeFrames, fusedTimeFrames);

    TimeFrames resultFrames = SpotMarketManager::fuseTimeFrames(timeFrames);
    QVERIFY2(resultFrames.count() == fusedTimeFrames.count(), "Expected frame count does not match the resulting frame count.");
    QCOMPARE(resultFrames, fusedTimeFrames);
}

void TestSpotmarket::testScheduleTime_data()
{
    QTest::addColumn<QString>("dataset");
    QTest::addColumn<QDateTime>("currentDateTime");
    QTest::addColumn<int>("minutes");
    QTest::addColumn<int>("minimumScheduleDuration");
    QTest::addColumn<bool>("currentFrameLocked");
    QTest::addColumn<TimeFrames>("expectedTimeFrames");

    QTest::newRow("Schedule at 04:00 dataset-1 120 min") << ":/resources/dataset-1.json" << EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(4, 0, 0)) << 120 << 1 << false
                                                         << TimeFrames({ TimeFrame(EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(4, 0, 0)), EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(5, 0, 0))),
                                                                         TimeFrame(EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(13, 0, 0)), EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(14, 0, 0))) });

    QTest::newRow("Schedule at 04:00 dataset-1 260 min") << ":/resources/dataset-1.json" << EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(4, 0, 0)) << 260 << 1 << false
                                                         << TimeFrames({ TimeFrame(EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(4, 0, 0)), EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(5, 20, 0))),
                                                                         TimeFrame(EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(12, 0, 0)), EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(14, 0, 0))),
                                                                         TimeFrame(EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(23, 0, 0)), EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(23, 0, 0)).addSecs(3600))});

    QTest::newRow("Schedule at 04:20 dataset-1 260 min") << ":/resources/dataset-1.json" << EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(4, 20, 0)) << 260 << 1 << false
                                                         << TimeFrames({ TimeFrame(EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(4, 20, 0)), EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(5, 40, 0))),
                                                                         TimeFrame(EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(12, 0, 0)), EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(14, 0, 0))),
                                                                         TimeFrame(EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(23, 0, 0)), EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(23, 0, 0)).addSecs(3600))});

    QTest::newRow("Schedule at 04:40 dataset-1 260 min") << ":/resources/dataset-1.json" << EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(4, 40, 0)) << 260 << 1 << false
                                                         << TimeFrames({ TimeFrame(EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(4, 40, 0)), EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(6, 0, 0))),
                                                                         TimeFrame(EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(12, 0, 0)), EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(14, 0, 0))),
                                                                         TimeFrame(EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(23, 0, 0)), EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(23, 0, 0)).addSecs(3600))});

    QTest::newRow("Schedule at 04:41 dataset-1 260 min window 1") << ":/resources/dataset-1.json" << EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(4, 41, 0)) << 260 << 1 << false
                                                         << TimeFrames({ TimeFrame(EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(4, 41, 0)), EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(6, 0, 0))),
                                                                         TimeFrame(EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(10, 0, 0)), EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(10, 1, 0))),
                                                                         TimeFrame(EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(12, 0, 0)), EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(14, 0, 0))),
                                                                         TimeFrame(EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(23, 0, 0)), EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(23, 0, 0)).addSecs(3600))});

    QTest::newRow("Schedule at 04:41 dataset-1 260 min window 10") << ":/resources/dataset-1.json" << EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(4, 41, 0)) << 260 << 10 << false
                                                         << TimeFrames({ TimeFrame(EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(4, 41, 0)), EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(6, 0, 0))),
                                                                         TimeFrame(EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(12, 0, 0)), EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(14, 0, 0))),
                                                                         TimeFrame(EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(23, 0, 0)), EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(23, 1, 0)).addSecs(3600))});


    QTest::newRow("Schedule at 09:25 dataset-3 346 min window 10") << ":/resources/dataset-3.json" << EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(8, 41, 0)) << 331 << 10 << false
                                                                   << TimeFrames({ TimeFrame(EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(8, 41, 0)), EnergyTestBase::utcDateTime(QDate::currentDate(), QTime(14, 12, 0)))});

}

void TestSpotmarket::testScheduleTime()
{
    QFETCH(QString, dataset);
    QFETCH(QDateTime, currentDateTime);
    QFETCH(int, minutes);
    QFETCH(int, minimumScheduleDuration);
    QFETCH(bool, currentFrameLocked);
    QFETCH(TimeFrames, expectedTimeFrames);

    SpotMarketManager *manager = new SpotMarketManager(m_networkAccessManager, this);
    QVERIFY2(!manager->enabled(), "SpotMarketManager initally enabled");

    SpotMarketDataProviderMock *mockProvider = new SpotMarketDataProviderMock(m_networkAccessManager, manager);
    QVERIFY2(mockProvider->prepareResourceData(dataset, currentDateTime), "Could not load resource data for testing the spotmarket manager.");
    QVERIFY2(manager->registerProvider(mockProvider), "Failed to register mock provider");
    QVERIFY2(manager->changeProvider(mockProvider->providerId()), "failed to change provider to mock provider");

    mockProvider->setCurrentDataTime(currentDateTime);

    QCOMPARE(manager->currentProvider(), qobject_cast<SpotMarketDataProvider*>(mockProvider));
    manager->setEnabled(true);
    QVERIFY(manager->enabled());

    QSignalSpy dataChangedSpy(manager, &SpotMarketManager::scoreEntriesUpdated);
    mockProvider->refreshData();
    if (dataChangedSpy.count() == 0)
        dataChangedSpy.wait();

    ScoreEntries weigthedScorings = manager->weightedScoreEntries(currentDateTime.date());
    weigthedScorings.sortByWeighting();
    qCDebug(dcTests()) << "Best timeframes" << weigthedScorings;

    TimeFrames resultingTimeFrames = manager->scheduleCharingTimeForToday(currentDateTime, minutes, minimumScheduleDuration, currentFrameLocked);
    qCDebug(dcTests()) << "Resulting timeframes" << resultingTimeFrames;

    int summ = 0;
    foreach (const TimeFrame &frame, resultingTimeFrames) {
        summ += frame.durationMinutes();
    }
    QCOMPARE(summ, minutes);
    QCOMPARE(resultingTimeFrames, expectedTimeFrames);

    manager->setEnabled(false);
    delete mockProvider;
    delete manager;
}


QTEST_MAIN(TestSpotmarket)
