// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "pixmapcachemodel_test.h"
#include "../qmlprofilertr.h"
#include <tracing/timelineformattime.h>
#include <QtTest>

namespace QmlProfiler {
namespace Internal {

PixmapCacheModelTest::PixmapCacheModelTest(QObject *parent) : QObject(parent),
    model(&manager, &aggregator)
{
}

void PixmapCacheModelTest::initTestCase()
{
    manager.initialize();
    manager.decreaseTraceStart(1);
    manager.increaseTraceEnd(300);

    for (int i = 0; i < MaximumPixmapEventType; ++i) {
        eventTypeIndices[i] = manager.numEventTypes();
        manager.appendEventType(QmlEventType(PixmapCacheEvent, UndefinedRangeType, i,
                                             QmlEventLocation("dings.png", 0, 0)));
    }

    // random data, should still result in consistent model.
    for (int i = 0; i < 100; ++i) {
        QmlEvent event;
        event.setTypeIndex(eventTypeIndices[(i * 13) % MaximumPixmapEventType]);
        event.setTimestamp(i);
        event.setNumbers({i + 1, i - 1, i * 2});
        manager.appendEvent(std::move(event));
    }

    for (int i = 0; i < MaximumPixmapEventType; ++i) {
        eventTypeIndices[i + MaximumPixmapEventType] = manager.numEventTypes();
        manager.appendEventType(QmlEventType(PixmapCacheEvent, UndefinedRangeType, i,
                                             QmlEventLocation("blah.png", 0, 0)));
    }


    // Explicitly test some "interesting" things
    int nextItem = model.count();
    QmlEvent event;
    event.setTypeIndex(eventTypeIndices[MaximumPixmapEventType + PixmapLoadingStarted]);
    event.setTimestamp(101);
    manager.appendEvent(QmlEvent(event));
    QCOMPARE(model.cacheState(nextItem), PixmapCacheModel::Uncached);
    QCOMPARE(model.loadState(nextItem), PixmapCacheModel::Loading);

    event.setTypeIndex(eventTypeIndices[MaximumPixmapEventType + PixmapCacheCountChanged]);
    event.setNumbers({0, 0, 200}); // cache count increase
    event.setTimestamp(102);
    manager.appendEvent(QmlEvent(event));
    QCOMPARE(model.cacheState(nextItem), PixmapCacheModel::ToBeCached);
    QCOMPARE(model.loadState(nextItem), PixmapCacheModel::Loading);

    event.setTypeIndex(eventTypeIndices[MaximumPixmapEventType + PixmapLoadingError]);
    event.setTimestamp(103);
    manager.appendEvent(QmlEvent(event));
    QCOMPARE(model.cacheState(nextItem), PixmapCacheModel::Corrupt);
    QCOMPARE(model.loadState(nextItem), PixmapCacheModel::Error);

    event.setTypeIndex(eventTypeIndices[MaximumPixmapEventType + PixmapCacheCountChanged]);
    event.setNumbers({0, 0, 199}); // cache count decrease
    event.setTimestamp(104);
    manager.appendEvent(QmlEvent(event));
    QCOMPARE(model.cacheState(nextItem), PixmapCacheModel::Uncacheable);
    QCOMPARE(model.loadState(nextItem), PixmapCacheModel::Error);


    ++nextItem;
    QCOMPARE(model.count(), nextItem);
    event.setTypeIndex(eventTypeIndices[MaximumPixmapEventType + PixmapLoadingStarted]);
    event.setTimestamp(105);
    manager.appendEvent(QmlEvent(event));
    QCOMPARE(model.cacheState(nextItem), PixmapCacheModel::Uncached);
    QCOMPARE(model.loadState(nextItem), PixmapCacheModel::Loading);

    event.setTypeIndex(eventTypeIndices[MaximumPixmapEventType + PixmapLoadingError]);
    event.setTimestamp(106);
    manager.appendEvent(QmlEvent(event));
    QCOMPARE(model.cacheState(nextItem), PixmapCacheModel::Uncacheable);
    QCOMPARE(model.loadState(nextItem), PixmapCacheModel::Error);

    event.setTypeIndex(eventTypeIndices[MaximumPixmapEventType + PixmapCacheCountChanged]);
    // We still cache the previous item, even though it's uncacheable.
    // This way we get a corrupt cache entry ...
    event.setNumbers({0, 0, 200});
    event.setTimestamp(107);
    manager.appendEvent(QmlEvent(event));
    QCOMPARE(model.cacheState(nextItem - 1), PixmapCacheModel::Corrupt);
    QCOMPARE(model.loadState(nextItem - 1), PixmapCacheModel::Error);
    QCOMPARE(model.cacheState(nextItem), PixmapCacheModel::Uncacheable);
    QCOMPARE(model.loadState(nextItem), PixmapCacheModel::Error);

    // ... which is immediately removed by another cache count change
    event.setTypeIndex(eventTypeIndices[MaximumPixmapEventType + PixmapCacheCountChanged]);
    event.setNumbers({0, 0, 199}); // cache count decrease, removes the corrupt entry
    event.setTimestamp(108);
    manager.appendEvent(QmlEvent(event));
    QCOMPARE(model.cacheState(nextItem - 1), PixmapCacheModel::Uncacheable);
    QCOMPARE(model.loadState(nextItem - 1), PixmapCacheModel::Error);
    QCOMPARE(model.cacheState(nextItem), PixmapCacheModel::Uncacheable);
    QCOMPARE(model.loadState(nextItem), PixmapCacheModel::Error);


    ++nextItem;
    QCOMPARE(model.count(), nextItem);
    event.setTypeIndex(eventTypeIndices[MaximumPixmapEventType + PixmapLoadingStarted]);
    event.setTimestamp(109);
    manager.appendEvent(QmlEvent(event));
    QCOMPARE(model.cacheState(nextItem), PixmapCacheModel::Uncached);
    QCOMPARE(model.loadState(nextItem), PixmapCacheModel::Loading);

    event.setTypeIndex(eventTypeIndices[MaximumPixmapEventType + PixmapCacheCountChanged]);
    event.setNumbers({0, 0, 200}); // cache count increase
    event.setTimestamp(110);
    manager.appendEvent(QmlEvent(event));
    QCOMPARE(model.cacheState(nextItem), PixmapCacheModel::ToBeCached);
    QCOMPARE(model.loadState(nextItem), PixmapCacheModel::Loading);

    event.setTypeIndex(eventTypeIndices[MaximumPixmapEventType + PixmapSizeKnown]);
    event.setNumbers({50, 50});
    event.setTimestamp(111);
    manager.appendEvent(QmlEvent(event));
    QCOMPARE(model.cacheState(nextItem), PixmapCacheModel::Cached);
    QCOMPARE(model.loadState(nextItem), PixmapCacheModel::Loading);

    event.setTypeIndex(eventTypeIndices[MaximumPixmapEventType + PixmapCacheCountChanged]);
    event.setNumbers({0, 0, 199}); // cache count decrease
    event.setTimestamp(112);
    manager.appendEvent(QmlEvent(event));
    QCOMPARE(model.cacheState(nextItem), PixmapCacheModel::Uncached);
    QCOMPARE(model.loadState(nextItem), PixmapCacheModel::Loading);

    // one pixmap item, and two cache count changes with known size
    QCOMPARE(model.count(), nextItem + 3);
    event.setTypeIndex(eventTypeIndices[MaximumPixmapEventType + PixmapSizeKnown]);
    event.setNumbers({20, 30});
    event.setTimestamp(113);
    manager.appendEvent(QmlEvent(event)); // Results in Uncached, with valid size
    QCOMPARE(model.count(), nextItem + 3); // no item added here; we just store the size

    event.setTypeIndex(eventTypeIndices[MaximumPixmapEventType + PixmapLoadingError]);
    event.setTimestamp(114);
    // terminates the still loading item, adding another cache count change
    manager.appendEvent(std::move(event));
    QCOMPARE(model.count(), nextItem + 4);
    QCOMPARE(model.cacheState(nextItem), PixmapCacheModel::Uncacheable);
    QCOMPARE(model.loadState(nextItem), PixmapCacheModel::Error);


    // some more random data
    for (int i = MaximumPixmapEventType; i < 2 * MaximumPixmapEventType; ++i) {
        for (int j = 0; j < i; ++j) {
            QmlEvent event;
            event.setTypeIndex(eventTypeIndices[i]);
            event.setTimestamp(i + j + 200);
            event.setNumbers({i + 1, i - 1, i - j});
            manager.appendEvent(std::move(event));
        }
    }


    manager.finalize();
}

void PixmapCacheModelTest::testConsistency()
{
    // one unique URL
    QCOMPARE(model.expandedRowCount(), 4);
    QVERIFY(model.collapsedRowCount() >= model.expandedRowCount());

    float prevHeight = 0;
    qint64 currentTime = -1;
    qint64 currentEnd = -1;
    QHash<int, qint64> collapsedEnds;
    for (int i = 0; i < model.count(); ++i) {
        int *typesEnd = eventTypeIndices + 2 * MaximumPixmapEventType;
        QVERIFY(std::find(eventTypeIndices, typesEnd, model.typeId(i)) != typesEnd);

        QVERIFY(model.startTime(i) >= currentTime);
        currentTime = model.startTime(i);
        QVERIFY(currentTime >= manager.traceStart());
        currentEnd = model.endTime(i);
        QVERIFY(currentEnd <= manager.traceEnd());

        const QVariantMap details = model.details(i);

        int collapsedRow = model.collapsedRow(i);
        int expandedRow = model.expandedRow(i);

        switch (collapsedRow) {
        case 0:
            QFAIL("There should be no events in row 0");
            break;
        case 1:
            QVERIFY(model.relativeHeight(i) != prevHeight);
            prevHeight = model.relativeHeight(i);
            QCOMPARE(expandedRow, 1);
            break;
        default:
            QVERIFY(expandedRow > 1);
            QCOMPARE(model.relativeHeight(i), 1.0f);
            QVERIFY(collapsedRow < model.collapsedRowCount());
            if (collapsedEnds.contains(collapsedRow))
                QVERIFY(collapsedEnds[collapsedRow] <= currentTime);
            collapsedEnds[collapsedRow] = model.endTime(i);
        }


        switch (expandedRow) {
        case 0:
            QFAIL("There should be no events in row 0");
            break;
        case 1:
            QCOMPARE(collapsedRow, 1);
            QVERIFY(details[QLatin1String("displayName")].toString() == Tr::tr("Image Cached"));
            QVERIFY(details.contains(Tr::tr("Cache Size")));
            break;
        default:
            QVERIFY(collapsedRow > 1);
            QCOMPARE(model.relativeHeight(i), 1.0f);
            QVERIFY(expandedRow < model.expandedRowCount());
            QVERIFY(details[QLatin1String("displayName")].toString() == Tr::tr("Image Loaded"));
            QCOMPARE(details[Tr::tr("Duration")].toString(),
                    Timeline::formatTime(model.duration(i)));
            // In expanded view pixmaps of the same URL but different sizes are allowed to overlap.
            // It looks bad, but that should be a rare thing.
            break;
        }

        QString filename = details[Tr::tr("File")].toString();
        QVERIFY(filename == QString("dings.png") || filename == QString("blah.png"));
        QVERIFY(details.contains(Tr::tr("Width")));
        QVERIFY(details.contains(Tr::tr("Height")));
    }
}

void PixmapCacheModelTest::testRowMaxValue()
{
    QCOMPARE(model.rowMaxValue(2), 0);
    QVERIFY(model.rowMaxValue(1) > 0);
}

void PixmapCacheModelTest::testColor()
{
    QRgb row1Color = 0;
    QRgb dingsColor = 0;
    QRgb blahColor = 0;
    for (int i = 0; i < model.count(); ++i) {
        if (model.collapsedRow(i) == 1) {
            if (row1Color == 0)
                row1Color = model.color(i);
            else
                QCOMPARE(model.color(i), row1Color);
        } else {
            QRgb &pixmapColor = (model.fileName(i) == QString("blah.png")) ?
                        blahColor : dingsColor;
            if (pixmapColor == 0)
                pixmapColor = model.color(i);
            else
                QCOMPARE(model.color(i), pixmapColor);
        }
    }
}

void PixmapCacheModelTest::testLabels()
{
    const QVariantList labels = model.labels();
    QCOMPARE(labels.length(), 3);

    const QVariantMap countRow = labels[0].toMap();

    QCOMPARE(countRow[QString("description")].toString(), Tr::tr("Cache Size"));
    QCOMPARE(countRow[QString("id")].toInt(), 0);

    const QVariantMap dingsRow = labels[1].toMap();
    QCOMPARE(dingsRow[QString("description")].toString(), QString("dings.png"));
    QCOMPARE(dingsRow[QString("id")].toInt(), 1); // urlIndex + 1

    const QVariantMap blahRow = labels[2].toMap();
    QCOMPARE(blahRow[QString("description")].toString(), QString("blah.png"));
    QCOMPARE(blahRow[QString("id")].toInt(), 2); // urlIndex + 1
}

void PixmapCacheModelTest::cleanupTestCase()
{
    manager.clearAll();
    QCOMPARE(model.count(), 0);
}

} // namespace Internal
} // namespace QmlProfiler
