/**
 * Copyright 2015 Holger Kaelberer <holger.k@elberer.de>
 *
 * This program 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 2 of
 * the License or (at your option) version 3 or any later version
 * accepted by the membership of KDE e.V. (or its successor approved
 * by the membership of KDE e.V.), which shall act as a proxy
 * defined in Section 14 of version 3 of the license.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <QSocketNotifier>
#include <QApplication>
#include <QNetworkAccessManager>
#include <QTest>
#include <QBuffer>
#include <QStandardPaths>
#include <QImage>
#include <QColor>

#include <kiconloader.h>

#include "core/daemon.h"
#include "core/device.h"
#include "core/kdeconnectplugin.h"
#include "kdeconnect-version.h"
#include "plugins/sendnotifications/sendnotificationsplugin.h"
#include "plugins/sendnotifications/notificationslistener.h"
#include "plugins/sendnotifications/notifyingapplication.h"

// Tweaked NotificationsPlugin for testing
class TestNotificationsPlugin : public SendNotificationsPlugin
{
    Q_OBJECT
public:
    explicit TestNotificationsPlugin(QObject* parent, const QVariantList& args)
    : SendNotificationsPlugin(parent, args)
    {
    }

    ~TestNotificationsPlugin() override = default;

    // allow to access notificationsListener for testing:
    NotificationsListener* getNotificationsListener() const
    {
        return notificationsListener;
    }

    void setNotificationsListener(NotificationsListener* value)
    {
        notificationsListener = value;
    }

};

// Tweaked Device for testing:
class TestDevice: public Device
{
    Q_OBJECT
private:
    int sentPackets;
    NetworkPacket* lastPacket;

public:
    explicit TestDevice(QObject* parent, const QString& id)
        : Device (parent, id)
        , sentPackets(0)
        , lastPacket(nullptr)
    {}

    ~TestDevice() override
    {
        delete lastPacket;
    }

    int getSentPackets() const
    {
        return sentPackets;
    }

    NetworkPacket* getLastPacket()
    {
        return lastPacket;
    }

    void deleteLastPacket()
    {
        delete lastPacket;
        lastPacket = nullptr;
    }

public Q_SLOTS:
    bool sendPacket(NetworkPacket& np) override
    {
        ++sentPackets;
        // copy packet manually to allow for inspection (can't use copy-constructor)
        deleteLastPacket();
        lastPacket = new NetworkPacket(np.type());
        Q_ASSERT(lastPacket);
        for (QVariantMap::ConstIterator iter = np.body().constBegin();
             iter != np.body().constEnd(); iter++)
            lastPacket->set(iter.key(), iter.value());
        lastPacket->setPayload(np.payload(), np.payloadSize());
        return true;
    }
};

// Tweaked NotificationsListener for testing:
class TestedNotificationsListener: public NotificationsListener
{
    Q_OBJECT
public:
    explicit TestedNotificationsListener(KdeConnectPlugin* aPlugin)
        : NotificationsListener(aPlugin)
    {}

    ~TestedNotificationsListener() override
    {}

    QHash<QString, NotifyingApplication>& getApplications()
    {
        return m_applications;
    }

    void setApplications(const QHash<QString, NotifyingApplication>& value)
    {
        m_applications = value;
    }

protected:
    bool parseImageDataArgument(const QVariant& argument, int& width,
                                int& height, int& rowStride, int& bitsPerSample,
                                int& channels, bool& hasAlpha,
                                QByteArray& imageData) const override
    {
        width = argument.toMap().value(QStringLiteral("width")).toInt();
        height = argument.toMap().value(QStringLiteral("height")).toInt();
        rowStride = argument.toMap().value(QStringLiteral("rowStride")).toInt();
        bitsPerSample = argument.toMap().value(QStringLiteral("bitsPerSample")).toInt();
        channels = argument.toMap().value(QStringLiteral("channels")).toInt();
        hasAlpha = argument.toMap().value(QStringLiteral("hasAlpha")).toBool();
        imageData = argument.toMap().value(QStringLiteral("imageData")).toByteArray();
        return true;
    }

};

class TestNotificationListener : public QObject
{
    Q_OBJECT
    public:
        TestNotificationListener()
            : plugin(nullptr)
        {
            QStandardPaths::setTestModeEnabled(true);
        }

    private Q_SLOTS:
        void testNotify();

    private:
        TestNotificationsPlugin* plugin;
};

void TestNotificationListener::testNotify()
{
    //
    // set things up:
    //

    QString dId(QStringLiteral("testid"));
    TestDevice* d = new TestDevice(nullptr, dId);

    int proxiedNotifications = 0;
    QCOMPARE(proxiedNotifications, d->getSentPackets());
    plugin = new TestNotificationsPlugin(this,
                                         QVariantList({ QVariant::fromValue<Device*>(d),
                                                        "notifications_plugin",
                                                        {"kdeconnect.notification"}}));
    QVERIFY(plugin->getNotificationsListener());
    delete plugin->getNotificationsListener();

    // inject our tweaked NotificationsListener:
    TestedNotificationsListener* listener = new TestedNotificationsListener(plugin);
    QVERIFY(listener);
    plugin->setNotificationsListener(listener);
    QCOMPARE(listener, plugin->getNotificationsListener());

    // make sure config is default:
    plugin->config()->set(QStringLiteral("generalPersistent"), false);
    plugin->config()->set(QStringLiteral("generalIncludeBody"), true);
    plugin->config()->set(QStringLiteral("generalUrgency"), 0);
    QCOMPARE(plugin->config()->get<bool>("generalPersistent"), false);
    QCOMPARE(plugin->config()->get<bool>("generalIncludeBody"), true);
    QCOMPARE(plugin->config()->get<bool>("generalUrgency"), false);

    // applications are modified directly:
    listener->getApplications().clear();
    QCOMPARE(listener->getApplications().count(), 0);

    //
    // Go !!!
    //

    uint replacesId = 99;
    uint retId;
    QString appName(QStringLiteral("some-appName"));
    QString body(QStringLiteral("some-body"));
    QString icon(QStringLiteral("some-icon"));
    QString summary(QStringLiteral("some-summary"));

    // regular Notify call that is synchronized ...
    retId = listener->Notify(appName, replacesId, icon, summary, body, {}, {{}}, 0);
    // ... should return replacesId,
    QCOMPARE(retId, replacesId);
    // ... have triggered sending a packet
    QCOMPARE(++proxiedNotifications, d->getSentPackets());
    // ... with our properties,
    QCOMPARE(d->getLastPacket()->get<uint>("id"), replacesId);
    QCOMPARE(d->getLastPacket()->get<QString>("appName"), appName);
    QCOMPARE(d->getLastPacket()->get<QString>("ticker"), summary + ": " + body);
    QCOMPARE(d->getLastPacket()->get<bool>("isClearable"), true);
    QCOMPARE(d->getLastPacket()->hasPayload(), false);

    // ... and create a new application internally that is initialized correctly:
    QCOMPARE(listener->getApplications().count(), 1);
    QVERIFY(listener->getApplications().contains(appName));
    QVERIFY(listener->getApplications()[appName].active);
    QCOMPARE(listener->getApplications()[appName].name, appName);
    QVERIFY(listener->getApplications()[appName].blacklistExpression.pattern().isEmpty());
    QCOMPARE(listener->getApplications()[appName].name, appName);
    QCOMPARE(listener->getApplications()[appName].icon, icon);

    // another one, with other timeout and urgency values:
    QString appName2(QStringLiteral("some-appName2"));
    QString body2(QStringLiteral("some-body2"));
    QString icon2(QStringLiteral("some-icon2"));
    QString summary2(QStringLiteral("some-summary2"));

    retId = listener->Notify(appName2, replacesId+1, icon2, summary2, body2, {}, {{"urgency", 2}}, 10);
    QCOMPARE(retId, replacesId+1);
    QCOMPARE(++proxiedNotifications, d->getSentPackets());
    QCOMPARE(d->getLastPacket()->get<uint>("id"), replacesId+1);
    QCOMPARE(d->getLastPacket()->get<QString>("appName"), appName2);
    QCOMPARE(d->getLastPacket()->get<QString>("ticker"), summary2 + ": " + body2);
    QCOMPARE(d->getLastPacket()->get<bool>("isClearable"), false);  // timeout != 0
    QCOMPARE(d->getLastPacket()->hasPayload(), false);
    QCOMPARE(listener->getApplications().count(), 2);
    QVERIFY(listener->getApplications().contains(appName2));
    QVERIFY(listener->getApplications().contains(appName));

    // if persistent-only is set, timeouts > 0 are not synced:
    plugin->config()->set(QStringLiteral("generalPersistent"), true);
    retId = listener->Notify(appName, replacesId, icon, summary, body, {}, {{}}, 1);
    QCOMPARE(retId, 0U);
    QCOMPARE(proxiedNotifications, d->getSentPackets());
    retId = listener->Notify(appName2, replacesId, icon2, summary2, body2, {}, {{}}, 3);
    QCOMPARE(retId, 0U);
    QCOMPARE(proxiedNotifications, d->getSentPackets());
    // but timeout == 0 is
    retId = listener->Notify(appName, replacesId, icon, summary, body, {}, {{}}, 0);
    QCOMPARE(retId, replacesId);
    QCOMPARE(++proxiedNotifications, d->getSentPackets());
    plugin->config()->set(QStringLiteral("generalPersistent"), false);

    // if min-urgency is set, lower urgency levels are not synced:
    plugin->config()->set(QStringLiteral("generalUrgency"), 1);
    retId = listener->Notify(appName, replacesId, icon, summary, body, {}, {{"urgency", 0}}, 0);
    QCOMPARE(retId, 0U);
    QCOMPARE(proxiedNotifications, d->getSentPackets());
    // equal urgency is
    retId = listener->Notify(appName, replacesId, icon, summary, body, {}, {{"urgency", 1}}, 0);
    QCOMPARE(retId, replacesId);
    QCOMPARE(++proxiedNotifications, d->getSentPackets());
    // higher urgency as well
    retId = listener->Notify(appName, replacesId, icon, summary, body, {}, {{"urgency", 2}}, 0);
    QCOMPARE(retId, replacesId);
    QCOMPARE(++proxiedNotifications, d->getSentPackets());
    plugin->config()->set(QStringLiteral("generalUrgency"), 0);

    // notifications for a deactivated application are not synced:
    QVERIFY(listener->getApplications().contains(appName));
    listener->getApplications()[appName].active = false;
    QVERIFY(!listener->getApplications()[appName].active);
    retId = listener->Notify(appName, replacesId, icon, summary, body, {}, {{"urgency", 0}}, 0);
    QCOMPARE(retId, 0U);
    QCOMPARE(proxiedNotifications, d->getSentPackets());
    // others are still:
    retId = listener->Notify(appName2, replacesId+1, icon2, summary2, body2, {}, {{}}, 0);
    QCOMPARE(retId, replacesId+1);
    QCOMPARE(++proxiedNotifications, d->getSentPackets());
    // back to normal:
    listener->getApplications()[appName].active = true;
    QVERIFY(listener->getApplications()[appName].active);
    retId = listener->Notify(appName, replacesId, icon, summary, body, {}, {{}}, 0);
    QCOMPARE(retId, replacesId);
    QCOMPARE(++proxiedNotifications, d->getSentPackets());

    // notifications with blacklisted subjects are not synced:
    QVERIFY(listener->getApplications().contains(appName));
    listener->getApplications()[appName].blacklistExpression.setPattern(QStringLiteral("black[12]|foo(bar|baz)"));
    retId = listener->Notify(appName, replacesId, icon, QStringLiteral("summary black1"), body, {}, {{}}, 0);
    QCOMPARE(retId, 0U);
    QCOMPARE(proxiedNotifications, d->getSentPackets());
    retId = listener->Notify(appName, replacesId, icon, QStringLiteral("summary foobar"), body, {}, {{}}, 0);
    QCOMPARE(retId, 0U);
    QCOMPARE(proxiedNotifications, d->getSentPackets());
    // other subjects are synced:
    retId = listener->Notify(appName, replacesId, icon, QStringLiteral("summary foo"), body, {}, {{}}, 0);
    QCOMPARE(retId, replacesId);
    QCOMPARE(++proxiedNotifications, d->getSentPackets());
    retId = listener->Notify(appName, replacesId, icon, QStringLiteral("summary black3"), body, {}, {{}}, 0);
    QCOMPARE(retId, replacesId);
    QCOMPARE(++proxiedNotifications, d->getSentPackets());
    // also body is checked by blacklist if requested:
    plugin->config()->set(QStringLiteral("generalIncludeBody"), true);
    retId = listener->Notify(appName, replacesId, icon, summary, QStringLiteral("body black1"), {}, {{}}, 0);
    QCOMPARE(retId, 0U);
    QCOMPARE(proxiedNotifications, d->getSentPackets());
    retId = listener->Notify(appName, replacesId, icon, summary, QStringLiteral("body foobaz"), {}, {{}}, 0);
    QCOMPARE(retId, 0U);
    QCOMPARE(proxiedNotifications, d->getSentPackets());
    // body does not matter if inclusion was not requested:
    plugin->config()->set(QStringLiteral("generalIncludeBody"), false);
    retId = listener->Notify(appName, replacesId, icon, summary, QStringLiteral("body black1"), {}, {{}}, 0);
    QCOMPARE(retId, replacesId);
    QCOMPARE(++proxiedNotifications, d->getSentPackets());
    // without body, also ticker value is different:
    QCOMPARE(d->getLastPacket()->get<QString>("ticker"), summary);
    retId = listener->Notify(appName, replacesId, icon, summary, QStringLiteral("body foobaz"), {}, {{}}, 0);
    QCOMPARE(retId, replacesId);
    QCOMPARE(++proxiedNotifications, d->getSentPackets());

    // back to normal:
    listener->getApplications()[appName].blacklistExpression.setPattern(QLatin1String(""));
    plugin->config()->set(QStringLiteral("generalIncludeBody"), true);
    retId = listener->Notify(appName, replacesId, icon, summary, body, {}, {{}}, 0);
    QCOMPARE(retId, replacesId);
    QCOMPARE(++proxiedNotifications, d->getSentPackets());
    retId = listener->Notify(appName2, replacesId, icon2, summary2, body2, {}, {{}}, 0);
    QCOMPARE(retId, replacesId);
    QCOMPARE(++proxiedNotifications, d->getSentPackets());

    // icon synchronization:
    QStringList iconPaths;
    // appIcon
    int count = 0;
    const QStringList icons = KIconLoader::global()->queryIcons(-KIconLoader::SizeEnormous, KIconLoader::Application);
    for (const auto& iconName : icons) {
        if (!iconName.endsWith(QLatin1String(".png")))
            continue;
        if (count++ > 3) // max 3 iterations
            break;
        iconPaths.append(iconName); // memorize some paths for later

        // existing icons are sync-ed if requested
        plugin->config()->set(QStringLiteral("generalSynchronizeIcons"), true);
        QFileInfo fi(iconName);
        retId = listener->Notify(appName, replacesId, fi.baseName(), summary, body, {}, {{}}, 0);
        QCOMPARE(retId, replacesId);
        QCOMPARE(++proxiedNotifications, d->getSentPackets());
        QVERIFY(d->getLastPacket()->hasPayload());
        QCOMPARE(d->getLastPacket()->payloadSize(), fi.size());
        // works also with abolute paths
        retId = listener->Notify(appName, replacesId, iconName, summary, body, {}, {{}}, 0);
        QCOMPARE(retId, replacesId);
        QCOMPARE(++proxiedNotifications, d->getSentPackets());
        QVERIFY(d->getLastPacket()->hasPayload());
        QCOMPARE(d->getLastPacket()->payloadSize(), fi.size());
        // extensions other than png are not accepted:
        retId = listener->Notify(appName, replacesId, iconName + ".svg", summary, body, {}, {{}}, 0);
        QCOMPARE(retId, replacesId);
        QCOMPARE(++proxiedNotifications, d->getSentPackets());
        QVERIFY(!d->getLastPacket()->hasPayload());

        // if sync not requested no payload:
        plugin->config()->set(QStringLiteral("generalSynchronizeIcons"), false);
        retId = listener->Notify(appName, replacesId, fi.baseName(), summary, body, {}, {{}}, 0);
        QCOMPARE(retId, replacesId);
        QCOMPARE(++proxiedNotifications, d->getSentPackets());
        QVERIFY(!d->getLastPacket()->hasPayload());
        QCOMPARE(d->getLastPacket()->payloadSize(), 0);
    }
    plugin->config()->set(QStringLiteral("generalSynchronizeIcons"), true);

    // image-path in hints
    if (iconPaths.size() > 0) {
        retId = listener->Notify(appName, replacesId, iconPaths.size() > 1 ? iconPaths[1] : icon, summary, body, {}, {{"image-path", iconPaths[0]}}, 0);
        QCOMPARE(retId, replacesId);
        QCOMPARE(++proxiedNotifications, d->getSentPackets());
        QVERIFY(d->getLastPacket()->hasPayload());
        QFileInfo hintsFi(iconPaths[0]);
        // image-path has priority over appIcon parameter:
        QCOMPARE(d->getLastPacket()->payloadSize(), hintsFi.size());
    }

    // image_path in hints
    if (iconPaths.size() > 0) {
        retId = listener->Notify(appName, replacesId, iconPaths.size() > 1 ? iconPaths[1] : icon, summary, body, {}, {{"image_path", iconPaths[0]}}, 0);
        QCOMPARE(retId, replacesId);
        QCOMPARE(++proxiedNotifications, d->getSentPackets());
        QVERIFY(d->getLastPacket()->hasPayload());
        QFileInfo hintsFi(iconPaths[0]);
        // image_path has priority over appIcon parameter:
        QCOMPARE(d->getLastPacket()->payloadSize(), hintsFi.size());
    }

    // image-data in hints
    // set up:
    QBuffer* buffer;
    QImage image;
    int width = 2, height = 2, rowStride = 4*width, bitsPerSample = 8,
            channels = 4;
    bool hasAlpha = 1;
    char rawData[] = { 0x01, 0x02, 0x03, 0x04,  // raw rgba data
                       0x11, 0x12, 0x13, 0x14,
                       0x21, 0x22, 0x23, 0x24,
                       0x31, 0x32, 0x33, 0x34 };
    QVariantMap imageData = {{"width", width}, {"height", height}, {"rowStride", rowStride},
                            {"bitsPerSample", bitsPerSample}, {"channels", channels},
                            {"hasAlpha", hasAlpha}, {"imageData", QByteArray(rawData, sizeof(rawData))}};
    QVariantMap hints;
#define COMPARE_PIXEL(x, y) \
    QCOMPARE(qRed(image.pixel(x,y)), (int)rawData[x*4 + y*rowStride + 0]); \
    QCOMPARE(qGreen(image.pixel(x,y)), (int)rawData[x*4 + y*rowStride + 1]); \
    QCOMPARE(qBlue(image.pixel(x,y)), (int)rawData[x*4 + y*rowStride + 2]); \
    QCOMPARE(qAlpha(image.pixel(x,y)), (int)rawData[x*4 + y*rowStride + 3]);

    hints.insert(QStringLiteral("image-data"), imageData);
    if (iconPaths.size() > 0)
        hints.insert(QStringLiteral("image-path"), iconPaths[0]);
    retId = listener->Notify(appName, replacesId, icon, summary, body, {}, hints, 0);
    QCOMPARE(retId, replacesId);
    QCOMPARE(++proxiedNotifications, d->getSentPackets());
    QVERIFY(d->getLastPacket()->hasPayload());
    buffer = dynamic_cast<QBuffer*>(d->getLastPacket()->payload().data());
    QCOMPARE(d->getLastPacket()->payloadSize(), buffer->size());
    // image-data is attached as png data
    QVERIFY(image.loadFromData(reinterpret_cast<const uchar*>(buffer->data().constData()), buffer->size(), "PNG"));
    // image-data has priority over image-path:
    QCOMPARE(image.byteCount(), rowStride*height);
    // rgba -> argb conversion was done correctly:
    COMPARE_PIXEL(0,0);
    COMPARE_PIXEL(1,0);
    COMPARE_PIXEL(0,1);
    COMPARE_PIXEL(1,1);

    // same for image_data in hints
    hints.clear();
    hints.insert(QStringLiteral("image-data"), imageData);
    if (iconPaths.size() > 0)
        hints.insert(QStringLiteral("image_path"), iconPaths[0]);
    retId = listener->Notify(appName, replacesId, icon, summary, body, {}, hints, 0);
    QCOMPARE(retId, replacesId);
    QCOMPARE(++proxiedNotifications, d->getSentPackets());
    QVERIFY(d->getLastPacket()->hasPayload());
    buffer = dynamic_cast<QBuffer*>(d->getLastPacket()->payload().data());
    QCOMPARE(d->getLastPacket()->payloadSize(), buffer->size());
    // image-data is attached as png data
    QVERIFY(image.loadFromData(reinterpret_cast<const uchar*>(buffer->data().constData()), buffer->size(), "PNG"));
    // image_data has priority over image_path/image-path:
    QCOMPARE(image.byteCount(), rowStride*height);
    // rgba -> argb conversion was done correctly:
    COMPARE_PIXEL(0,0);
    COMPARE_PIXEL(1,0);
    COMPARE_PIXEL(0,1);
    COMPARE_PIXEL(1,1);

    // same for icon_data, which has lowest priority
    hints.clear();
    hints.insert(QStringLiteral("icon_data"), imageData);
    retId = listener->Notify(appName, replacesId, QLatin1String(""), summary, body, {}, hints, 0);
    QCOMPARE(retId, replacesId);
    QCOMPARE(++proxiedNotifications, d->getSentPackets());
    QVERIFY(d->getLastPacket());
    QVERIFY(d->getLastPacket()->hasPayload());
    buffer = dynamic_cast<QBuffer*>(d->getLastPacket()->payload().data());
    // image-data is attached as png data
    QVERIFY(image.loadFromData(reinterpret_cast<const uchar*>(buffer->data().constData()), buffer->size(), "PNG"));
    QCOMPARE(image.byteCount(), rowStride*height);
    // rgba -> argb conversion was done correctly:
    COMPARE_PIXEL(0,0);
    COMPARE_PIXEL(1,0);
    COMPARE_PIXEL(0,1);
    COMPARE_PIXEL(1,1);

#undef COMPARE_PIXEL
}


QTEST_GUILESS_MAIN(TestNotificationListener);

#include "testnotificationlistener.moc"
