/*
 * Copyright (C) 2016  Daniel Vrátil <dvratil@kde.org>
 *
 * 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) any later version.
 *
 * 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, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 */

#include "pimeventsplugintest.h"
#include "fakepimdatasource.h"
#include "testdataparser.h"
#include "../pimeventsplugin.h"
#include "testutils.h"

#include <QTest>
#include <QSignalSpy>

Q_DECLARE_METATYPE(DateEventDataHash)
Q_DECLARE_METATYPE(CalendarEvents::EventData)

void PimEventsPluginTest::initTestCase()
{
    qputenv("TZ", "Europe/Berlin");
    qRegisterMetaType<DateEventDataHash>("QMultiHash<QDate, CalendarEvents::EventData>");
    qRegisterMetaType<CalendarEvents::EventData>("CalendarEvents::EventData");
}

bool PimEventsPluginTest::compareEventDataHashes(const DateEventDataHash &actual, const DateEventDataHash &expected)
{
    COMPARE(actual.size(), expected.size());
    Q_FOREACH (const QDate &resultKey, actual.uniqueKeys()) {
        VERIFY(expected.contains(resultKey));
        auto resultValues = actual.values(resultKey);
        auto expectedValues = expected.values(resultKey);
        COMPARE(resultValues.size(), expectedValues.size());
        std::sort(resultValues.begin(), resultValues.end(), std::less<CalendarEvents::EventData>());
        std::sort(expectedValues.begin(), expectedValues.end(), std::less<CalendarEvents::EventData>());
        COMPARE(resultValues, expectedValues);
    }
    return true;
}

DateEventDataHash PimEventsPluginTest::populateCalendar(FakePimDataSource *source, bool uniqueEventData)
{
    const QStringList allData = TestDataParser::allTestData();
    DateEventDataHash expectedData;
    for (const QString &data : allData) {
        TestDataParser parser(data, true);
        if (parser.rangeEnd() < QDate(2016, 5, 1) || parser.rangeStart() > QDate(2016, 5, 31)) {
            continue;
        }
        const KCalCore::Event::Ptr event = parser.incidence().dynamicCast<KCalCore::Event>();
        if (event) {
            source->setAkonadiIdForIncidence(event, parser.akonadiId());
            source->calendar()->addEvent(event);
            Q_FOREACH (const CalendarEvents::EventData &dt, parser.eventData()) {
                if (uniqueEventData) {
                    expectedData.insert(dt.startDateTime().date(), dt);
                } else {
                    QDate d = dt.startDateTime().date();
                    while (d <= dt.endDateTime().date()) {
                        expectedData.insert(d, dt);
                        d = d.addDays(1);
                    }
                }
            }
        }
    }

    return expectedData;
}

QVector<CalendarEvents::EventData> PimEventsPluginTest::findEventData(const KCalCore::Event::Ptr &event, const DateEventDataHash &allData)
{
    QVector<CalendarEvents::EventData> data;
    for (auto it = allData.cbegin(), end = allData.cend(); it != end; ++it) {
        // This is a very naive check
        if (it->title() == event->summary()
            && it->description() == event->description()
            && it->isAllDay() == event->allDay()) {
            data.push_back((*it));
        }
    }

    return data;
}

void PimEventsPluginTest::testLoadEventsForDataRange()
{
    FakePimDataSource source;
    const DateEventDataHash expectedData = populateCalendar(&source, false);

    PimEventsPlugin plugin(&source);
    QSignalSpy dataReadySpy(&plugin, &PimEventsPlugin::dataReady);
    QVERIFY(dataReadySpy.isValid());

    plugin.loadEventsForDateRange(QDate(2016, 5, 1), QDate(2016, 5, 31));
    QCOMPARE(dataReadySpy.size(), 1);
    const auto results = dataReadySpy.takeFirst().first().value<DateEventDataHash>();
    QVERIFY(compareEventDataHashes(results, expectedData));

    plugin.loadEventsForDateRange(QDate(2016, 1, 1), QDate(2016, 1, 30));
    QCOMPARE(dataReadySpy.size(), 0);
}

void PimEventsPluginTest::testEventAdded()
{
    const QStringList allData = TestDataParser::allTestData();

    FakePimDataSource source;

    PimEventsPlugin plugin(&source);
    QSignalSpy dataReadySpy(&plugin, &PimEventsPlugin::dataReady);
    QVERIFY(dataReadySpy.isValid());

    plugin.loadEventsForDateRange(QDate(2016, 5, 1), QDate(2016, 5, 31));
    QCOMPARE(dataReadySpy.size(), 0);

    Q_FOREACH (const QString &data, allData) {
        TestDataParser parser(data, true);
        if (parser.rangeEnd() < QDate(2016, 5, 1) || parser.rangeStart() > QDate(2016, 5, 31)) {
            continue;
        }
        const KCalCore::Event::Ptr event = parser.incidence().dynamicCast<KCalCore::Event>();
        if (event) {
            source.setAkonadiIdForIncidence(event, parser.akonadiId());
            source.calendar()->addEvent(event);
            DateEventDataHash expectedData;
            Q_FOREACH (const CalendarEvents::EventData &dt, parser.eventData()) {
                QDate d = dt.startDateTime().date();
                while (d <= dt.endDateTime().date()) {
                    expectedData.insert(d, dt);
                    d = d.addDays(1);
                }
            }

            QCOMPARE(dataReadySpy.size(), 1);
            const auto results = dataReadySpy.takeFirst().first().value<DateEventDataHash>();
            QVERIFY(compareEventDataHashes(results, expectedData));
        }
    }
}

void PimEventsPluginTest::testEventModified()
{
    FakePimDataSource source;

    PimEventsPlugin plugin(&source);
    QSignalSpy eventModifiedSpy(&plugin, &PimEventsPlugin::eventModified);
    QVERIFY(eventModifiedSpy.isValid());

    // Populate model
    const auto allData = populateCalendar(&source, true);

    // We don't care about the result of this, we just need to have mStart and
    // mEnd set
    plugin.loadEventsForDateRange(QDate(2016, 5, 1), QDate(2016, 5, 31));

    // Non-recurring event
    {
        QVERIFY(eventModifiedSpy.isEmpty());
        KCalCore::Event::Ptr event = source.calendar()->event(QStringLiteral("4d7fdd2c-2d3a-4ecf-8964-00eb92225209"));
        QVERIFY(event);
        const auto expectedData = findEventData(event, allData);
        QCOMPARE(expectedData.size(), 1);
        event->setSummary(QStringLiteral("TEST"));

        QVERIFY(source.calendar()->addEvent(event));

        QCOMPARE(eventModifiedSpy.size(), 1);
        const auto result = eventModifiedSpy.takeFirst().first().value<CalendarEvents::EventData>();
        // TODO: Test for other property changes too?
        QCOMPARE(result.title(), event->summary());
        QCOMPARE(result.uid(), expectedData[0].uid());
    }

    // Recurring event
    {
        QVERIFY(eventModifiedSpy.isEmpty());
        KCalCore::Event::Ptr event = source.calendar()->event(QStringLiteral("69971015-fe9c-4800-a20e-d46bafa24e41"));
        QVERIFY(event);
        auto expectedData = findEventData(event, allData);
        event->setSummary(QStringLiteral("TEST2"));
        QVERIFY(source.calendar()->addEvent(event));

        QCOMPARE(eventModifiedSpy.size(), expectedData.size());
        while (!eventModifiedSpy.isEmpty()) {
            const auto args = eventModifiedSpy.takeFirst();
            const auto &resultData = args[0].value<CalendarEvents::EventData>();
            const auto expected = std::find_if(expectedData.begin(), expectedData.end(),
                                               [resultData](const CalendarEvents::EventData &e) {
                return e.uid() == resultData.uid();
            });
            QVERIFY(expected != expectedData.end());
            expectedData.erase(expected);
            QCOMPARE(resultData.title(), QString::fromLatin1("TEST2"));
        }
        QVERIFY(expectedData.isEmpty());
    }
}

void PimEventsPluginTest::testEventRemoved()
{
    FakePimDataSource source;

    PimEventsPlugin plugin(&source);
    QSignalSpy eventRemovedSpy(&plugin, &PimEventsPlugin::eventRemoved);
    QVERIFY(eventRemovedSpy.isValid());

    const auto allData = populateCalendar(&source, true);

    // We don't care about the result of this, we just need to have mStart and
    // mEnd set
    plugin.loadEventsForDateRange(QDate(2016, 5, 1), QDate(2016, 5, 31));

    // Non-recurring event
    {
        QVERIFY(eventRemovedSpy.isEmpty());
        const KCalCore::Event::Ptr event = source.calendar()->event(QStringLiteral("4d7fdd2c-2d3a-4ecf-8964-00eb92225209"));
        QVERIFY(event);
        const auto expectedData = findEventData(event, allData);
        QCOMPARE(expectedData.size(), 1);

        QVERIFY(source.calendar()->deleteEvent(event));

        QCOMPARE(eventRemovedSpy.size(), 1);
        const auto result = eventRemovedSpy.takeFirst().first().toString();
        QCOMPARE(result, expectedData[0].uid());
    }

    // Recurring event
    {
        QVERIFY(eventRemovedSpy.isEmpty());
        KCalCore::Event::Ptr event = source.calendar()->event(QStringLiteral("69971015-fe9c-4800-a20e-d46bafa24e41"));
        QVERIFY(event);
        auto expectedData = findEventData(event, allData);

        QVERIFY(source.calendar()->deleteEvent(event));

        QCOMPARE(eventRemovedSpy.size(), expectedData.size());
        while (!eventRemovedSpy.isEmpty()) {
            const QString resultUid = eventRemovedSpy.takeFirst().first().toString();
            const auto expected = std::find_if(expectedData.begin(), expectedData.end(),
                                               [resultUid](const CalendarEvents::EventData &e) {
                return e.uid() == resultUid;
            });
            QVERIFY(expected != expectedData.end());
            expectedData.erase(expected);
        }
        QVERIFY(expectedData.isEmpty());
    }
}

QTEST_MAIN(PimEventsPluginTest)
