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

#include "tst_qrangemodel.h"

#include <QtCore/qjsondocument.h>
#include <QtCore/qjsonarray.h>
#include <QtCore/qstringlistmodel.h>
#include <QtTest/qsignalspy.h>

#include <QtGui/qcolor.h>
#include <QtGui/qpolygon.h>
#include <QtGui/qpen.h>

#if QT_CONFIG(itemmodeltester)
#include <QtTest/qabstractitemmodeltester.h>
#endif

#if defined(__cpp_lib_ranges)
#include <ranges>
#endif

QList<QPersistentModelIndex> tst_QRangeModel::allIndexes(QAbstractItemModel *model,
                                                         const QModelIndex &parent)
{
    QList<QPersistentModelIndex> pmiList;
    for (int row = 0; row < model->rowCount(parent); ++row) {
        for (int column = 0; column < model->columnCount(parent); ++column) {
            const QModelIndex mi = model->index(row, column, parent);
            pmiList += mi;
            if (model->hasChildren(mi))
                pmiList += allIndexes(model, mi);
        }
    }
    return pmiList;
}

void tst_QRangeModel::verifyPmiList(const QList<QPersistentModelIndex> &pmiList)
{
    for (const auto &pmi : pmiList) {
        auto debug = qScopeGuard([&pmi]{
            qCritical() << "Failing index" << pmi << pmi.isValid();
        });
        QVERIFY(pmi.isValid());
        QCOMPARE_NE(pmi.flags(), Qt::NoItemFlags);
        QCOMPARE(pmi.parent().isValid(), pmi.parent().data().isValid());
        debug.dismiss();
    }
}

void tst_QRangeModel::basics()
{
#if QT_CONFIG(itemmodeltester)
    QFETCH(Factory, factory);
    auto model = factory();

    QAbstractItemModelTester modelTest(model.get(), this);
#else
    QSKIP("QAbstractItemModelTester not available");
#endif
}

using ModelFromData = std::function<std::unique_ptr<QAbstractItemModel>(std::vector<int> &)>;

void tst_QRangeModel::modifies_data()
{
    QTest::addColumn<ModelFromData>("modelFromData");
    QTest::addColumn<bool>("modifiesOriginal");

    QTest::newRow("copy") << ModelFromData([](std::vector<int> &numbers){
        return std::unique_ptr<QAbstractItemModel>(new QRangeModel(numbers));
    }) << false;

    QTest::newRow("reference_wrapper") << ModelFromData([](std::vector<int> &numbers){
        return std::unique_ptr<QAbstractItemModel>(new QRangeModel(std::ref(numbers)));
    }) << true;

    QTest::newRow("pointer") << ModelFromData([](std::vector<int> &numbers){
        return std::unique_ptr<QAbstractItemModel>(new QRangeModel(&numbers));
    }) << true;
}

void tst_QRangeModel::modifies()
{
    QFETCH(ModelFromData, modelFromData);
    QFETCH(bool, modifiesOriginal);

    int dataSize = 1;
    std::vector<int> numbers { 1 };
    auto model = modelFromData(numbers);

    {
        QCOMPARE(model->rowCount(), numbers.size());
        const QModelIndex index = model->index(model->rowCount() - 1, 0);
        QCOMPARE(index.data(), numbers[index.row()]);
    }

    {
        QVERIFY(model->insertRows(0, 1));
        QCOMPARE(model->rowCount(), ++dataSize);
        QCOMPARE(int(numbers.size()) == model->rowCount(), modifiesOriginal);
    }

    {
        const QModelIndex index = model->index(0, 0);
        QVERIFY(model->setData(index, 2));
        QCOMPARE(index.data() == numbers[index.row()], modifiesOriginal);
    }
}

void tst_QRangeModel::minimalIterator()
{
    struct Minimal
    {
        struct iterator
        {
            using value_type = QString;
            using size_type = int;
            using difference_type = int;
            using reference = value_type;
            using pointer = value_type;
            using iterator_category = std::forward_iterator_tag;

            constexpr iterator &operator++()
            { ++m_index; return *this; }
            constexpr iterator operator++(int)
            { auto copy = *this; ++m_index; return copy; }

            reference operator*() const
            { return QString::number(m_index); }
            constexpr bool operator==(const iterator &other) const noexcept
            { return m_index == other.m_index; }
            constexpr bool operator!=(const iterator &other) const noexcept
            { return m_index != other.m_index; }

            size_type m_index;
        };

#if defined (__cpp_concepts)
        static_assert(std::forward_iterator<iterator>);
#endif
        iterator begin() const { return iterator{0}; }
        iterator end() const { return iterator{m_size}; }

        int m_size;
    } minimal{100};

    QRangeModel model(minimal);
    QCOMPARE(model.rowCount(), minimal.m_size);
    for (int row = model.rowCount() - 1; row >= 0; --row) {
        const QModelIndex index = model.index(row, 0);
        QCOMPARE(index.data(), QString::number(row));
        QVERIFY(!index.flags().testFlag(Qt::ItemIsEditable));
    }
}

void tst_QRangeModel::ranges()
{
#if defined(__cpp_lib_ranges)
    const int lowest = 1;
    const int highest = 10;
    QRangeModel model(std::views::iota(lowest, highest));
    QCOMPARE(model.rowCount(), highest - lowest);
    QCOMPARE(model.columnCount(), 1);
#else
    QSKIP("C++ ranges library not available");
#endif
}

void tst_QRangeModel::json()
{
    QJsonDocument json = QJsonDocument::fromJson(R"([ "one", "two" ])");
    QVERIFY(json.isArray());
    QRangeModel model(json.array());
    QCOMPARE(model.rowCount(), 2);
    const QModelIndex index = model.index(1, 0);
    QVERIFY(index.isValid());
    QCOMPARE(index.data().toString(), "two");
}

void tst_QRangeModel::ownership()
{
#if 0 // static assert expected
    {
        std::vector<std::vector<int> *> data;
        QRangeModel modelOnCopy(data);
    }

    {
        std::shared_ptr<std::vector<std::vector<int>*>> data;
        QRangeModel modelOnMovedData(std::move(data));
    }

    {
        QSharedPointer<std::vector<std::vector<int>*>> data;
        QRangeModel modelOnMovedData(std::move(data));
    }
#endif

    { // a list of pointers to objects
        Object *object = new Object;
        QPointer guard = object;
        std::vector<Object *> objects {
            object
        };
        { // model takes ownership of its own copy of the vector (!)
            QRangeModel modelOnMove(std::move(objects));
        }
        QVERIFY(!guard);
        objects = { new Object };
        guard = objects[0];
        { // model does not take ownership
            QRangeModel modelOnPointer(&objects);
        }
        QVERIFY(guard);
        { // model does not take ownership
            QRangeModel modelOnRef(std::ref(objects));
        }
        QVERIFY(guard);

        { // model does take ownership
            QRangeModel movedIntoModel(std::move(objects));
            QCOMPARE(movedIntoModel.columnCount(), 2);
        }
        QVERIFY(!guard);
    }

    { // a list of shared_ptr
        Object *object = new Object;
        QPointer guard = object;
        std::vector<std::shared_ptr<Object>> objects {
            std::shared_ptr<Object>(object)
        };
        { // model does not take ownership
            QCOMPARE(objects[0].use_count(), 1);
            QRangeModel modelOnCopy(objects);
            QCOMPARE(modelOnCopy.rowCount(), 1);
            QCOMPARE(objects[0].use_count(), 2);
        }
        QCOMPARE(objects[0].use_count(), 1);
        { // model does not take ownership
            QRangeModel modelOnPointer(&objects);
            QCOMPARE(objects[0].use_count(), 1);
        }
        QCOMPARE(objects[0].use_count(), 1);
        QVERIFY(guard);
        { // model does not take ownership
            QRangeModel modelOnRef(std::ref(objects));
            QCOMPARE(objects[0].use_count(), 1);
        }
        QCOMPARE(objects[0].use_count(), 1);
        QVERIFY(guard);
        { // model owns the last shared copy
            QRangeModel movedIntoModel(std::move(objects));
        }
        QVERIFY(!guard);
    }

    { // a table of pointers
        Object *object = new Object;
        QPointer guard = object;
        std::vector<std::vector<Object *>> table {
            {object}
        };
        { // model does not take ownership
            QRangeModel modelOnCopy(table);
        }
        QVERIFY(guard);
        { // model does not take ownership
            QRangeModel modelOnPointer(&table);
        }
        QVERIFY(guard);
        { // model does not take ownership
            QRangeModel modelOnRef(std::ref(table));
        }
        QVERIFY(guard);
        { // model does take ownership of rows, but not of objects within each row
            QRangeModel movedIntoModel(std::move(table));
        }
        QVERIFY(guard);
        delete object;
    }

    { // a table of shared pointers to rows
        std::vector<std::shared_ptr<Object>> objects = { std::make_shared<Object>() };

        {
            QRangeModel model(objects);
            QCOMPARE(objects.front().use_count(), 2);
        }

        QCOMPARE(objects.front().use_count(), 1);
    }

    { // a table of shared pointers to rows
        using SharedObjectsList = std::vector<std::shared_ptr<Object>>;
        std::vector<std::shared_ptr<SharedObjectsList>> table = {
            std::make_shared<SharedObjectsList>(SharedObjectsList{ std::make_shared<Object>() })
        };

        {
            QRangeModel model(table);
            QCOMPARE(table.front().use_count(), 2);
            QCOMPARE(table.front()->front().use_count(), 1);
        }

        QCOMPARE(table.front().use_count(), 1);
        QCOMPARE(table.front()->front().use_count(), 1);
    }
}

void tst_QRangeModel::overrideRoleNames()
{
    // verify that an overridden roleNames() gets called consistently
    class RoleModel : public QRangeModel
    {
    public:
        RoleModel() : QRangeModel(QList<std::tuple<Object *>>{
            new Object,
            new Object,
            new Object,
        }) {
        }

        QHash<int, QByteArray> roleNames() const override
        {
            return {
                {Qt::UserRole, "string"},
                {Qt::UserRole + 1, "number"}
            };
        }
    };

    RoleModel model;
    const QList<int> expectedKeys = {Qt::UserRole, Qt::UserRole + 1};
    QCOMPARE(model.roleNames().size(), expectedKeys.size());

    const QModelIndex index = model.index(0, 0);
    QVERIFY(model.setData(index, "string value", Qt::UserRole));
    QVERIFY(model.setData(index, 42, Qt::UserRole + 1));
    QVERIFY(!model.setData(index, "display"));

    const auto itemData = model.itemData(index);
    QCOMPARE(itemData.keys(), expectedKeys);
    QCOMPARE(itemData.value(Qt::UserRole), "string value");
    QCOMPARE(itemData.value(Qt::UserRole + 1), 42);

    QVERIFY(model.setItemData(model.index(1, 0), itemData));
}

void tst_QRangeModel::setRoleNames()
{
    QRangeModel model(QStringList{});

    const QHash<int, QByteArray> expectedRoleNames = {
        {Qt::DisplayRole, "display"},
        {Qt::EditRole, "edit"},
        {Qt::RangeModelDataRole, "modelData"},
    };

    QSignalSpy spy(&model, &QRangeModel::roleNamesChanged);
    QCOMPARE(model.roleNames(), expectedRoleNames);
    QVERIFY(spy.isEmpty());

    const QHash<int, QByteArray> roleNames = {
        {Qt::UserRole, "one"},
        {Qt::UserRole + 1, "two"},
    };
    model.setRoleNames(roleNames);
    QCOMPARE(spy.count(), 1);
    QCOMPARE(model.roleNames(), roleNames);

    model.setRoleNames({});
    QCOMPARE(spy.count(), 2);
    QCOMPARE(model.roleNames(), expectedRoleNames);
}

void tst_QRangeModel::defaultRoleNames()
{
    // default QAIM role names for anything that we didn't specialize roleNames for
    const QHash<int, QByteArray> qaimRoleNames = QStringListModel().roleNames();

    [qaimRoleNames]{
        const QHash<int, QByteArray> expectedRoleNames = {
            {Qt::RangeModelDataRole, "modelData"},
            {Qt::UserRole, "string"},
            {Qt::UserRole + 1, "number"},
        };

        QCOMPARE(QRangeModel(QList<Object *>{}).roleNames(),
                 qaimRoleNames);
        QCOMPARE(QRangeModel(QList<std::tuple<Object *>>{}).roleNames(),
                 expectedRoleNames);
        QCOMPARE(QRangeModel(QList<std::tuple<Object *, Object *>>{}).roleNames(),
                 expectedRoleNames);
    }();

    [qaimRoleNames]{
        const QHash<int, QByteArray> expectedRoleNames = {
            {Qt::RangeModelDataRole, "modelData"},
            {Qt::DisplayRole, "display"},
            {Qt::DecorationRole, "decoration"},
            {Qt::ToolTipRole, "toolTip"},
        };
        QCOMPARE(QRangeModel(QList<Item>{}).roleNames(),
                 qaimRoleNames);
        QCOMPARE(QRangeModel(QList<std::tuple<Item>>{}).roleNames(),
                 expectedRoleNames);
        QCOMPARE(QRangeModel(QList<std::tuple<Item, Item, Item>>{}).roleNames(),
                 expectedRoleNames);
        QCOMPARE(QRangeModel(QList<QList<Item>>{}).roleNames(),
                 expectedRoleNames);
    }();

    [qaimRoleNames]{
        using Tree = QList<MultiRoleGadget>;
        struct EmptyTreeProtocol
        {
            const MultiRoleGadget *parentRow(const MultiRoleGadget &) const { return nullptr; }
            const Tree &childRows(const MultiRoleGadget &) const { return empty; }
            Tree empty;
        };
        const QHash<int, QByteArray> expectedRoleNames = {
            {Qt::RangeModelDataRole, "modelData"},
            {Qt::DisplayRole, "display"},
            {Qt::DecorationRole, "decoration"},
            {Qt::UserRole, "number"},
            {Qt::UserRole + 1, "user"},
        };
        QCOMPARE(QRangeModel(QList<MultiRoleGadget>{}).roleNames(),
                 expectedRoleNames);
        QCOMPARE(QRangeModel(QList<QList<MultiRoleGadget>>{}).roleNames(),
                 expectedRoleNames);
        QCOMPARE(QRangeModel(std::vector<std::array<MultiRoleGadget, 5>>{}).roleNames(),
                 expectedRoleNames);
        QCOMPARE(QRangeModel(Tree{}, EmptyTreeProtocol{}).roleNames(),
                 expectedRoleNames);
    }();

    [qaimRoleNames]{
        const QHash<int, QByteArray> singleValueRoleNames = {
            {Qt::DisplayRole, "display"},
            {Qt::EditRole, "edit"},
            {Qt::RangeModelDataRole, "modelData"},
        };

        QCOMPARE(QRangeModel(QList<Row>{}).roleNames(), qaimRoleNames);
        QCOMPARE(QRangeModel(QList<std::tuple<Item, MultiRoleGadget>>{}).roleNames(),
                 qaimRoleNames);
        QCOMPARE(QRangeModel(QList<int>{}).roleNames(), singleValueRoleNames);
        QCOMPARE(QRangeModel(QList<QList<QString>>{}).roleNames(), singleValueRoleNames);
    }();
}

class MultiRoleObject : public Object
{
public:
    template <typename Signal>
    bool isConnected(Signal &&signal) const
    {
        return isSignalConnected(QMetaMethod::fromSignal(signal));
    }
};

template <>
struct QRangeModel::RowOptions<MultiRoleObject>
{
    static constexpr auto rowCategory = QRangeModel::RowCategory::MultiRoleItem;
};

void tst_QRangeModel::autoConnectPolicy_data()
{
    QTest::addColumn<QRangeModel::AutoConnectPolicy>("policy");

    QTest::addRow("Full") << QRangeModel::AutoConnectPolicy::Full;
    QTest::addRow("OnRead") << QRangeModel::AutoConnectPolicy::OnRead;
}

void tst_QRangeModel::autoConnectPolicy()
{
    QFETCH(const QRangeModel::AutoConnectPolicy, policy);

    [policy]{
        QList<MultiRoleObject *> objectList = {
            new MultiRoleObject,
            new MultiRoleObject,
            new MultiRoleObject,
        };
        QRangeModel model(&objectList);
        model.setAutoConnectPolicy(policy);
        QSignalSpy dataChangedSpy(&model, &QAbstractItemModel::dataChanged);

        int emissions = 0;
        objectList[0]->setString("String 0");
        if (policy == QRangeModel::AutoConnectPolicy::OnRead) {
            QCOMPARE(dataChangedSpy.size(), emissions);
        } else {
            QCOMPARE(dataChangedSpy.size(), ++emissions);
            QCOMPARE(dataChangedSpy.at(0).at(0), model.index(0, 0));
            QCOMPARE(dataChangedSpy.at(0).at(1), model.index(0, 0));
            QCOMPARE(dataChangedSpy.at(0).at(2), QVariant::fromValue(QList<int>{Qt::UserRole}));
        }

        if (policy == QRangeModel::AutoConnectPolicy::OnRead) {
            QVERIFY(!objectList.at(1)->isConnected(&Object::stringChanged));
            QVERIFY(!objectList.at(1)->isConnected(&Object::numberChanged));
            model.data(model.index(1, 0), Qt::UserRole + 1);
            QVERIFY(!objectList.at(1)->isConnected(&Object::stringChanged));
            QVERIFY(objectList.at(1)->isConnected(&Object::numberChanged));
            model.itemData(model.index(1, 0));
            QVERIFY(objectList.at(1)->isConnected(&Object::stringChanged));
        }

        objectList[1]->setNumber(42);
        QCOMPARE(dataChangedSpy.size(), ++emissions);
        QCOMPARE(dataChangedSpy.at(emissions - 1).at(0), model.index(1, 0));
        QCOMPARE(dataChangedSpy.at(emissions - 1).at(1), model.index(1, 0));
        QCOMPARE(dataChangedSpy.at(emissions - 1).at(2), QVariant::fromValue(QList<int>{Qt::UserRole + 1}));

        QVERIFY(model.insertRow(0));
        QCOMPARE(objectList.at(1)->isConnected(&Object::numberChanged),
                 policy == QRangeModel::AutoConnectPolicy::Full);
        QCOMPARE(objectList.at(1)->isConnected(&Object::stringChanged),
                 policy == QRangeModel::AutoConnectPolicy::Full);
    }();

    [policy]{
        QList<QList<MultiRoleObject *>> objectTable = {
            {new MultiRoleObject, new MultiRoleObject},
            {new MultiRoleObject, new MultiRoleObject},
        };
        QRangeModel model(&objectTable);
        connect(&model, &QRangeModel::rowsInserted,
                &model, [&objectTable](const QModelIndex &, int first, int last){
            while (first <= last) {
                objectTable[first][0] = new MultiRoleObject;
                objectTable[first][1] = new MultiRoleObject;
                ++first;
            }
        });
        connect(&model, &QRangeModel::columnsInserted,
                &model, [&objectTable](const QModelIndex &, int first, int last){
            for (auto &row : objectTable) {
                for (int column = first; column <= last; ++column)
                    row[column] = new MultiRoleObject;
            }
        });
        model.setAutoConnectPolicy(policy);
        QSignalSpy dataChangedSpy(&model, &QAbstractItemModel::dataChanged);

        objectTable[0][1]->setString("String 0/1");
        QCOMPARE(dataChangedSpy.size(), policy == QRangeModel::AutoConnectPolicy::Full ? 1 : 0);

        model.insertRows(1, 2);
        for (const auto &row : std::as_const(objectTable)) {
            for (const auto &object : row) {
                QCOMPARE(object->isConnected(&Object::numberChanged),
                         policy == QRangeModel::AutoConnectPolicy::Full);
                QCOMPARE(object->isConnected(&Object::stringChanged),
                         policy == QRangeModel::AutoConnectPolicy::Full);
            }
        }

        model.insertColumn(0);
        for (const auto &row : std::as_const(objectTable)) {
            for (const auto &object : row) {
                QCOMPARE(object->isConnected(&Object::numberChanged),
                         policy == QRangeModel::AutoConnectPolicy::Full);
                QCOMPARE(object->isConnected(&Object::stringChanged),
                         policy == QRangeModel::AutoConnectPolicy::Full);
            }
        }
    }();

    [policy]{
        QList<std::tuple<MultiRoleObject *, MultiRoleObject *>> objectTable = {
            {new MultiRoleObject, new MultiRoleObject},
            {new MultiRoleObject, new MultiRoleObject},
        };
        QRangeModel model(&objectTable);
        model.setAutoConnectPolicy(policy);
        QSignalSpy dataChangedSpy(&model, &QAbstractItemModel::dataChanged);

        if (policy == QRangeModel::AutoConnectPolicy::OnRead) {
            for (int row = 0; row < model.rowCount(); ++row) {
                for (int column = 0; column < model.columnCount(); ++column)
                    model.itemData(model.index(row, column));
            }
        }

        auto *topRight = get<1>(objectTable[0]);
        auto *bottomLeft = get<0>(objectTable[1]);
        topRight->setNumber(52);
        QCOMPARE(dataChangedSpy.size(), 1);

        QVERIFY(bottomLeft->isConnected(&Object::numberChanged));
        QVERIFY(bottomLeft->isConnected(&Object::stringChanged));
        QVERIFY(model.removeRows(1, 1));
        bottomLeft->setNumber(52); // this will lazily break the connection
        QVERIFY(!bottomLeft->isConnected(&Object::numberChanged));
        QVERIFY(bottomLeft->isConnected(&Object::stringChanged));
        QCOMPARE(dataChangedSpy.size(), 1);
        bottomLeft->setNumber(53); // this should not crash
        bottomLeft->setString("No update");
        QVERIFY(!bottomLeft->isConnected(&Object::stringChanged));

        const QModelIndex index = model.index(0, 0);
        dataChangedSpy.clear();
        QVERIFY(model.setData(index, "string", Qt::UserRole));
        QCOMPARE(dataChangedSpy.count(), 1);
        QCOMPARE(dataChangedSpy.back().at(2),
                 QVariant::fromValue(QList<int>{Qt::UserRole}));
        // this will right now emit dataChanged three times:
        QVERIFY(model.setItemData(index, QMap<int, QVariant>{
            {Qt::UserRole, QVariant("string")},
            {Qt::UserRole + 1, QVariant(42)},
        }));
        QCOMPARE(dataChangedSpy.count(), 2);
        QCOMPARE(dataChangedSpy.back().at(2),
                 QVariant::fromValue(QList<int>{Qt::UserRole, Qt::UserRole + 1}));
        QVERIFY(model.setData(index, 42, Qt::UserRole + 1));
        QCOMPARE(dataChangedSpy.count(), 3);
        QCOMPARE(dataChangedSpy.back().at(2),
                 QVariant::fromValue(QList<int>{Qt::UserRole + 1}));
    }();

    [policy]{
        QList<Object *> objectList = {
            new Object, new Object, new Object
        };
        Object *top = objectList.front();
        Object *bottom = objectList.back();

        QRangeModel model(std::move(objectList));
        model.setAutoConnectPolicy(policy);
        QSignalSpy dataChangedSpy(&model, &QAbstractItemModel::dataChanged);

        if (policy == QRangeModel::AutoConnectPolicy::OnRead) {
            // read top-left and bottom-right
            model.data(model.index(0, 0));
            model.data(model.index(model.rowCount() - 1, model.columnCount() - 1));
        }
        top->setString("abc");
        QCOMPARE(dataChangedSpy.count(), 1);
        QCOMPARE(dataChangedSpy.back().at(0), model.index(0, 0));
        QCOMPARE(dataChangedSpy.back().at(1), model.index(0, 0));
        QCOMPARE(dataChangedSpy.back().at(2), QVariant::fromValue(QList<int>{Qt::DisplayRole}));
        bottom->setNumber(42);
        QCOMPARE(dataChangedSpy.count(), 2);
        QCOMPARE(dataChangedSpy.back().at(0), model.index(model.rowCount() - 1,
                                                          model.columnCount() - 1));
        QCOMPARE(dataChangedSpy.back().at(2), QVariant::fromValue(QList<int>{Qt::DisplayRole}));
        dataChangedSpy.clear();

        top->setNumber(1234);
        bottom->setString("def");
        QCOMPARE(dataChangedSpy.count(), policy == QRangeModel::AutoConnectPolicy::Full ? 2 : 0);
    }();

    [policy]{
        using Tree = QList<MultiRoleObject *>;
        struct ObjectTreeProtocol
        {
            const MultiRoleObject *parentRow(const MultiRoleObject &row) const
            {
                return static_cast<MultiRoleObject *>(row.parent());
            }
            void setParentRow(MultiRoleObject &row, MultiRoleObject *parent)
            {
                row.setParent(parent);
            }
            const Tree &childRows(const MultiRoleObject &row) const {
                // don't do that at home...
                return *reinterpret_cast<const Tree *>(&row.children());
            }
            Tree &childRows(const MultiRoleObject &) {
                empty = {};
                return empty;
            }
            Tree empty;
        };
        Tree tree {
            new MultiRoleObject,
            new MultiRoleObject,
            new MultiRoleObject,
        };
        tree[0]->setObjectName("root 0");
        tree[1]->setObjectName("root 1");
        tree[2]->setObjectName("root 2");
        auto *child01 = new MultiRoleObject;
        child01->setObjectName("child 0/1");
        child01->setParent(tree[0]);
        (new MultiRoleObject)->setParent(tree[1]);
        (new MultiRoleObject)->setParent(tree[2]);

        QRangeModel model(std::ref(tree), ObjectTreeProtocol{});
        QSignalSpy dataChangedSpy(&model, &QAbstractItemModel::dataChanged);
        model.setAutoConnectPolicy(policy);

        const QModelIndex root0 = model.index(0, 0);
        const QModelIndex child01Index = model.index(0, 0, root0);
        if (policy == QRangeModel::AutoConnectPolicy::OnRead)
            QVERIFY(model.data(child01Index, Qt::UserRole + 1).isValid());

        child01->setNumber(42);
        QCOMPARE(dataChangedSpy.size(), 1);

        QCOMPARE(dataChangedSpy.at(0).at(0).value<QModelIndex>(), child01Index);
        QCOMPARE(dataChangedSpy.at(0).at(1).value<QModelIndex>(), child01Index);
        QCOMPARE(dataChangedSpy.at(0).at(2), QVariant::fromValue(QList{Qt::UserRole + 1}));
    }();

    // build tests
    { // make sure we don't kill the compiler with recursive templates
        QList<std::array<Object *, 1000000>> wideList;
        QRangeModel model(wideList);
    }

    { // work with custom tuple types
        QList<ObjectRow> objectRows;
        QRangeModel model(objectRows);
    }

    { // correctly resolve optional children
        struct Protocol {
            ObjectRow *parentRow(const ObjectRow &) const { return nullptr; }
            const auto &childRows(const ObjectRow &) const { return emptyRow; }

            std::optional<std::vector<ObjectRow>> emptyRow = std::nullopt;
        };
        std::vector<ObjectRow> objectTree;
        QRangeModel model(objectTree, Protocol{});
        model.setAutoConnectPolicy(QRangeModel::AutoConnectPolicy::Full);
    }
}

void tst_QRangeModel::dimensions()
{
    QFETCH(Factory, factory);
    auto model = factory();
    QFETCH(const int, expectedRowCount);
    QFETCH(const int, expectedColumnCount);

    QCOMPARE(model->rowCount(), expectedRowCount);
    QCOMPARE(model->columnCount(), expectedColumnCount);
}

void tst_QRangeModel::sibling()
{
    QFETCH(Factory, factory);
    auto model = factory();

    QModelIndex withChildren;
    const auto test = [model = model.get(), &withChildren](const QModelIndex &parent){
        const QModelIndex first = model->index(0, 0, parent);
        // deliberately requesting siblings outside of the range
        for (int r = 0; r < model->rowCount() + 1; ++r) {
            for (int c = 0; c < model->columnCount() + 1; ++c) {
                const QModelIndex next = model->sibling(r, c, first);
                const QModelIndex qaimNext = model->QAbstractItemModel::sibling(r, c, first);
                if (!withChildren.isValid() && model->hasChildren(next))
                    withChildren = next;
                QCOMPARE(next, qaimNext);
            }
        }
    };

    test({});
    if (withChildren.isValid())
        test(withChildren);
}

void tst_QRangeModel::flags()
{
    QFETCH(Factory, factory);
    auto model = factory();
    QFETCH(const ChangeActions, changeActions);

    const bool hasMimeTypes = !model->mimeTypes().isEmpty();
    const QModelIndex first = model->index(0, 0);
    QVERIFY(first.isValid());
    const QModelIndex last = model->index(model->rowCount() - 1, model->columnCount() - 1);
    QVERIFY(last.isValid());

    QCOMPARE(first.flags().testFlag(Qt::ItemIsEditable),
             changeActions.testFlags(ChangeAction::SetData));
    QCOMPARE(last.flags().testFlag(Qt::ItemIsEditable),
             changeActions.testFlags(ChangeAction::SetData));
    if (last.column() != 0)
        QVERIFY(last.flags().testFlag(Qt::ItemNeverHasChildren));
    QCOMPARE(first.flags().testFlag(Qt::ItemIsDragEnabled), hasMimeTypes);
    QCOMPARE(first.flags().testFlag(Qt::ItemIsDropEnabled),
             changeActions.testAnyFlags(ChangeAction::SetData | ChangeAction::InsertRows));
}

void tst_QRangeModel::headerData()
{
    QFETCH(Factory, factory);
    QFETCH(QVariant, headerValue);
    auto model = factory();

    QCOMPARE(model->headerData(0, Qt::Horizontal), headerValue);
}

void tst_QRangeModel::data()
{
    QFETCH(Factory, factory);
    auto model = factory();

    QVERIFY(!model->data({}).isValid());

    const QModelIndex first = model->index(0, 0);
    QVERIFY(first.isValid());
    const QModelIndex last = model->index(model->rowCount() - 1, model->columnCount() - 1);
    QVERIFY(last.isValid());

    QVERIFY(first.data().isValid());
    QVERIFY(last.data().isValid());
}

void tst_QRangeModel::multiData()
{
    QFETCH(Factory, factory);
    auto model = factory();

    const QModelIndex index = model->index(0, 0);
    QVERIFY(index.isValid());
    QModelRoleData displayData(Qt::DisplayRole);
    model->multiData(index, displayData);

    QCOMPARE(displayData.data(), model->data(index, Qt::DisplayRole));
}

void tst_QRangeModel::setData()
{
    QFETCH(Factory, factory);
    auto model = factory();
    QFETCH(const ChangeActions, changeActions);

    QVERIFY(!model->setData({}, {}));

    const QModelIndex first = model->index(0, 0);
    QVERIFY(first.isValid());

    QVariant newValue = 12345;
    const QVariant oldValue = first.data();
    QVERIFY(oldValue.isValid());

    if (!newValue.canConvert(oldValue.metaType()))
        newValue = QVariant(oldValue.metaType());
    QCOMPARE(first.data(), oldValue);
    QCOMPARE(model->setData(first, newValue), changeActions.testFlag(ChangeAction::SetData));
    QCOMPARE(first.data() == oldValue, !changeActions.testFlag(ChangeAction::SetData));

    // don't crash for invalid role values, but ignore return value - it will
    // work with items that are backed by a map.
    model->setData(first, oldValue, Qt::UserRole + 255);
}

static constexpr bool fakedRole(int role)
{
    return role == Qt::EditRole
        || role == Qt::RangeModelDataRole
        || role == Qt::RangeModelDataRole + 1;
}

void tst_QRangeModel::itemData()
{
    QFETCH(Factory, factory);
    auto model = factory();

    QVERIFY(model->itemData({}).isEmpty());

    const QModelIndex index = model->index(0, 0);
    const QMap<int, QVariant> itemData = model->itemData(index);
    for (int role = 0; role < Qt::UserRole; ++role) {
        // we fake those in data()
        if (fakedRole(role))
            continue;
        QCOMPARE(itemData.value(role), index.data(role));
    }
}

void tst_QRangeModel::setItemData()
{
    QFETCH(Factory, factory);
    auto model = factory();
    QFETCH(const ChangeActions, changeActions);

    QVERIFY(!model->setItemData({}, {}));

    const QModelIndex index = model->index(0, 0);
    QMap<int, QVariant> itemData = model->itemData(index);
    // we only care about multi-role models
    const auto roles = itemData.keys();
    if (roles == QList<int>{Qt::DisplayRole, Qt::EditRole}
     || roles == QList<int>{Qt::DisplayRole, Qt::EditRole, Qt::RangeModelDataRole}) {
        QSKIP("Can't test setItemData on models with single values!");
     }

    itemData = {};
    for (int role : roles) {
        if (fakedRole(role)) // faked
            continue;
        QVariant data = role != Qt::DecorationRole ? QVariant(QStringLiteral("%1").arg(role))
                                                   : QVariant(QColor(Qt::magenta));
        itemData.insert(role, data);
    }

    QCOMPARE_NE(model->itemData(index), itemData);
    QCOMPARE(model->setItemData(index, itemData),
             changeActions.testFlag(ChangeAction::SetItemData));
    if (!changeActions.testFlag(ChangeAction::SetItemData))
        return; // nothing more to test for those models

    {
        auto newItemData = model->itemData(index);
        newItemData.take(Qt::EditRole); // faked
        auto diagnostics = qScopeGuard([&]{
            qDebug() << "Mismatch";
            qDebug() << "     Actual:" << newItemData;
            qDebug() << "   Expected:" << itemData;
        });
        QCOMPARE(newItemData == itemData, changeActions.testFlag(ChangeAction::SetItemData));
        diagnostics.dismiss();
    }

    for (int role = 0; role < Qt::UserRole; ++role) {
        if (fakedRole(role))
            continue;

        QVariant data = index.data(role);
        auto diagnostics = qScopeGuard([&]{
            qDebug() << "Mismatch for" << Qt::ItemDataRole(role);
            qDebug() << "     Actual:" << data;
            qDebug() << "   Expected:" << itemData.value(role);
        });
        QCOMPARE(data == itemData.value(role), changeActions.testFlag(ChangeAction::SetData));
        diagnostics.dismiss();
    }
}

void tst_QRangeModel::clearItemData()
{
    QFETCH(Factory, factory);
    auto model = factory();
    QFETCH(const ChangeActions, changeActions);

    QVERIFY(!model->clearItemData({}));

    const QModelIndex index0 = model->index(1, 0);
    const QModelIndex index1 = model->index(1, 1);
    const QVariant oldDataAt0 = index0.data();
    const QVariant oldDataAt1 = index1.data();
    QCOMPARE(model->clearItemData(index0), changeActions.testFlags(ChangeAction::SetData));
    QCOMPARE(index0.data() == oldDataAt0, !changeActions.testFlags(ChangeAction::SetData));
    QCOMPARE(index1.data(), oldDataAt1);
}

void tst_QRangeModel::modelData()
{
    QFETCH(Factory, factory);
    auto model = factory();
    QFETCH(const ChangeActions, changeActions);

    const auto roleNames = model->roleNames();
    // models must support RangeModelDataRole if it's part of roleNames;
    // otherwise, we still might support it for certain columns.
    const bool promisesRangeModelData = roleNames.contains(Qt::RangeModelDataRole);
    const QModelIndex index = model->index(0, 0);
    const QVariant data = model->data(index, Qt::RangeModelDataRole);
    QVERIFY(data.isValid() || !promisesRangeModelData);

    bool setDataResult = false;
    // we can not swap out QObjects, even if setData() is permitted and
    // RangeModelDataRole is reported
    if (changeActions.testFlag(ChangeAction::SetData) && data.isValid()) {
        QEXPECT_FAIL("listOfMetaObjectTupleCopy", "Can't replace QObject items", Continue);
        QEXPECT_FAIL("arrayOfUniqueMultiObjectTuplesRef", "Can't replace QObject items", Continue);
        setDataResult = model->setData(index, data, Qt::RangeModelDataRole);
        QVERIFY(setDataResult || !promisesRangeModelData);
        if (setDataResult) {
            // if we could setData (with an unchanged value), then try with a
            // different row, and verify that the DisplayRole changes.
            if (model->rowCount() > 1) {
                const QModelIndex index2 = model->index(1, 0);
                const QVariant data2 = model->data(index2, Qt::RangeModelDataRole);
                QVERIFY(model->setData(index, data2, Qt::RangeModelDataRole));
                QCOMPARE(model->data(index, Qt::DisplayRole), model->data(index2, Qt::DisplayRole));
            } else {
                QSKIP("Cannot test changing of modelData with a model with only one row");
            }
        }
    }
}

void tst_QRangeModel::rangeModelDataInTable()
{
    std::vector<Object *> table = {
        new Object,
        new Object,
        new Object
    };
    QRangeModel model(std::ref(table));
    QCOMPARE(model.rowCount(), 3);
    QCOMPARE(model.columnCount(), 2);

    const QModelIndex topLeft = model.index(0, 0);
    const QModelIndex bottomRight = model.index(2, 1);
    QCOMPARE(model.data(topLeft), table.at(topLeft.row())->string());
    QCOMPARE(model.data(bottomRight), table.at(bottomRight.row())->number());

    QVERIFY(model.setData(topLeft, "fortyTwo", Qt::RangeModelDataRole));
    QVERIFY(model.setData(bottomRight, 42, Qt::RangeModelDataRole));
}

void tst_QRangeModel::insertRows()
{
    QFETCH(Factory, factory);
    auto model = factory();
    QFETCH(const int, expectedRowCount);
    QFETCH(const int, expectedColumnCount);
    QFETCH(const ChangeActions, changeActions);
    const bool canSetData = changeActions.testFlag(ChangeAction::SetData);

    // associative containers are empty, so we need to explicitly set data for
    // newly created rows
    static const QList<QByteArrayView> associativeContainers = {
        "listOfNamedRoles",
        "tableOfEnumRoles",
        "tableOfIntRoles",
        "stdTableOfIntRoles",
        "stdTableOfIntRolesWithSharedRows",
    };
    const auto it = std::find_if(associativeContainers.begin(), associativeContainers.end(),
                [current = QByteArrayView(QTest::currentDataTag())](const QByteArrayView &tag) {
        if (tag == current)
            return true;
        for (auto suffix : { "Pointer", "Copy", "Ref", "UPtr", "SPtr" }) {
            if (tag + suffix == current)
                return true;
        }
        return false;
    });

    if (it != associativeContainers.end()) {
        connect(model.get(), &QAbstractItemModel::rowsInserted,
                this, [model = model.get()](const QModelIndex &parent, int start, int end) {
            for (int row = start; row <= end; ++row) {
                model->setData(model->index(row, 0, parent), row);
                model->setData(model->index(row, model->columnCount(parent) - 1, parent), row);
            }
        });
    }

    const QList<QPersistentModelIndex> pmiList = allIndexes(model.get());

    QCOMPARE(model->rowCount(), expectedRowCount);
    QCOMPARE(model->insertRow(0), changeActions.testFlag(ChangeAction::InsertRows));
    QCOMPARE(model->rowCount() == expectedRowCount + 1,
             changeActions.testFlag(ChangeAction::InsertRows));

    // get and put data into the new row
    const QModelIndex firstItem = model->index(0, 0);
    const QModelIndex lastItem = model->index(0, expectedColumnCount - 1);
    QVERIFY(firstItem.isValid());
    QVERIFY(lastItem.isValid());
    const QVariant firstValue = firstItem.data();
    const QVariant lastValue = lastItem.data();

    QEXPECT_FAIL("tableOfPointersPointer", "No item created", Continue);
    QEXPECT_FAIL("listOfMetaObjectTupleCopy", "No object created", Continue);
    QEXPECT_FAIL("listOfMetaObjectTupleMoveOfCopy", "No object created", Continue);

    QVERIFY(firstValue.isValid() && lastValue.isValid());
    QCOMPARE(model->setData(firstItem, lastValue), canSetData && lastValue.isValid());
    QCOMPARE(model->setData(lastItem, firstValue), canSetData && firstValue.isValid());

    // append more rows
    QCOMPARE(model->insertRows(model->rowCount(), 5),
             changeActions.testFlag(ChangeAction::InsertRows));
    QCOMPARE(model->rowCount() == expectedRowCount + 6,
             changeActions.testFlag(ChangeAction::InsertRows));

    verifyPmiList(pmiList);
}

void tst_QRangeModel::removeRows()
{
    QFETCH(Factory, factory);
    auto model = factory();
    QFETCH(const int, expectedRowCount);
    QFETCH(const ChangeActions, changeActions);

    QCOMPARE(model->rowCount(), expectedRowCount);
    QCOMPARE(model->removeRow(0), changeActions.testFlag(ChangeAction::RemoveRows));
    QCOMPARE(model->rowCount() == expectedRowCount - 1,
             changeActions.testFlag(ChangeAction::RemoveRows));
    QCOMPARE(model->removeRows(model->rowCount() - 2, 2),
             changeActions.testFlag(ChangeAction::RemoveRows));
    QCOMPARE(model->rowCount() == expectedRowCount - 3,
             changeActions.testFlag(ChangeAction::RemoveRows));

    const int newRowCount = model->rowCount();
    // make sure we don't crash when removing more than exist
    const bool couldRemove = model->removeRows(model->rowCount() - 5, model->rowCount() * 2);
    QCOMPARE_LE(model->rowCount(), newRowCount);
    QCOMPARE(couldRemove, model->rowCount() != newRowCount);
}

void tst_QRangeModel::moveRows()
{
    QFETCH(Factory, factory);
    auto model = factory();
    QFETCH(const int, expectedRowCount);
    QFETCH(const ChangeActions, changeActions);

    QCOMPARE(model->rowCount(), expectedRowCount);
    if (expectedRowCount < 3)
        QSKIP("Model is to small for testing moveRows");

    const QVariant first = model->index(0, 0).data();
    const QVariant second = model->index(1, 0).data();
    const QVariant last = model->index(expectedRowCount - 1, 0).data();

    // various noops, should always fail
    QVERIFY(!model->moveRows({}, 0, 1, {}, 0));
    QVERIFY(!model->moveRows({}, 0, 1, {}, 1));
    QVERIFY(!model->moveRows({}, 0, 0, {}, expectedRowCount));

    // try to move first to last
    QCOMPARE(model->moveRows({}, 0, 1, {}, expectedRowCount),
             changeActions != ChangeAction::ReadOnly);
    if (changeActions == ChangeAction::ReadOnly)
        return;

    QCOMPARE(model->index(0, 0).data(), second); // second is now on first
    QCOMPARE(model->index(expectedRowCount - 2, 0).data(), last); // last is now second last
    QCOMPARE(model->index(expectedRowCount - 1, 0).data(), first);

    // move all but one row to the end - this restores the order
    QVERIFY(model->moveRows({}, 0, expectedRowCount - 1,
                            {}, expectedRowCount));
    QCOMPARE(model->index(0, 0).data(), first);
    QCOMPARE(model->index(1, 0).data(), second);
    QCOMPARE(model->index(expectedRowCount - 1, 0).data(), last);

    // move the last row step by step up to the top
    for (int row = model->rowCount() - 1; row > 0; --row)
        QVERIFY(model->moveRow({}, row, {}, row - 1));
    QCOMPARE(model->index(0, 0).data(), last);
    // move all except the first row up - this restores the order again
    QVERIFY(model->moveRows({}, 1, expectedRowCount - 1, {}, 0));
    QCOMPARE(model->index(0, 0).data(), first);
    QCOMPARE(model->index(1, 0).data(), second);
    QCOMPARE(model->index(expectedRowCount - 1, 0).data(), last);
}

void tst_QRangeModel::insertColumns()
{
    QFETCH(Factory, factory);
    auto model = factory();
    QFETCH(const int, expectedColumnCount);
    QFETCH(const ChangeActions, changeActions);

    QCOMPARE(model->columnCount(), expectedColumnCount);
    QCOMPARE(model->insertColumn(0), changeActions.testFlag(ChangeAction::InsertColumns));
    QCOMPARE(model->columnCount() == expectedColumnCount + 1,
             changeActions.testFlag(ChangeAction::InsertColumns));

    // append
    QCOMPARE(model->insertColumns(model->columnCount(), 5),
             changeActions.testFlag(ChangeAction::InsertColumns));
    QCOMPARE(model->columnCount() == expectedColumnCount + 6,
             changeActions.testFlag(ChangeAction::InsertColumns));
}

void tst_QRangeModel::removeColumns()
{
    QFETCH(Factory, factory);
    auto model = factory();
    QFETCH(const int, expectedColumnCount);
    QFETCH(const ChangeActions, changeActions);

    QCOMPARE(model->columnCount(), expectedColumnCount);
    QCOMPARE(model->removeColumn(0),
             changeActions.testFlag(ChangeAction::RemoveColumns));
}

void tst_QRangeModel::moveColumns()
{
    QFETCH(Factory, factory);
    auto model = factory();
    QFETCH(const int, expectedColumnCount);
    QFETCH(const ChangeActions, changeActions);

    QCOMPARE(model->columnCount(), expectedColumnCount);
    if (expectedColumnCount < 2)
        QSKIP("Cannot test moveColumns with a single-column model");

    const QVariant first = model->index(0, 0).data();
    const QVariant second = model->index(0, 1).data();
    const QVariant last = model->index(0, expectedColumnCount - 1).data();

    // various noops, should always fail
    QVERIFY(!model->moveColumns({}, 0, 1, {}, 0));
    QVERIFY(!model->moveColumns({}, 0, 1, {}, 1));

    QCOMPARE(model->moveColumns({}, 0, 1, {}, expectedColumnCount),
             bool(changeActions & ChangeAction::ChangeColumns));
    if (!(changeActions & ChangeAction::ChangeColumns))
        return;

    QCOMPARE(model->index(0, 0).data(), second);
    QCOMPARE(model->index(0, expectedColumnCount - 2).data(), last);
    QCOMPARE(model->index(0, expectedColumnCount - 1).data(), first);

    // the rest only makes sense for models with at least 3 columns
    if (expectedColumnCount >= 3) {
        // move all but one column to the end - this restores the order
        QVERIFY(model->moveColumns({}, 0, expectedColumnCount - 1,
                                {}, expectedColumnCount));
        QCOMPARE(model->index(0, 0).data(), first);
        QCOMPARE(model->index(0, 1).data(), second);
        QCOMPARE(model->index(0, expectedColumnCount - 1).data(), last);

        // move the last row step by step up to the top
        for (int column = model->columnCount() - 1; column > 0; --column)
            QVERIFY(model->moveColumn({}, column, {}, column - 1));
        QCOMPARE(model->index(0, 0).data(), last);
        // move all except the first row up - this restores the order again
        QVERIFY(model->moveColumns({}, 1, expectedColumnCount - 1, {}, 0));
        QCOMPARE(model->index(0, 0).data(), first);
        QCOMPARE(model->index(0, 1).data(), second);
        QCOMPARE(model->index(0, expectedColumnCount - 1).data(), last);
    }
}

void tst_QRangeModel::inconsistentColumnCount()
{
#ifndef QT_NO_DEBUG
    QTest::ignoreMessage(QtCriticalMsg, "QRangeModel: "
        "Column-range at row 1 is not large enough!");
#endif

    std::vector<std::vector<int>> fuzzyTable = {
        {0},
        {},
        {2},
    };
    QRangeModel model(fuzzyTable);
    QCOMPARE(model.columnCount(), 1);
    for (int row = 0; row < model.rowCount(); ++row) {
        auto debug = qScopeGuard([&]{
            qCritical() << "Test failed for row" << row << fuzzyTable.at(row).size();
        });
        const bool shouldWork = int(fuzzyTable.at(row).size()) >= model.columnCount();
        const auto index = model.index(row, model.columnCount() - 1);
        QCOMPARE(index.isValid(), shouldWork);
        // none of these should crash
        QCOMPARE(index.data().isValid(), shouldWork);
        QCOMPARE(model.setData(index, row + 5), shouldWork);
        QCOMPARE(model.clearItemData(index), shouldWork);
        debug.dismiss();
    }
}

void tst_QRangeModel::largeArrays()
{
    {
        std::array<int, 10000> largeArray = {};
        QRangeModel model(largeArray);
        const QModelIndex index = model.index(int(largeArray.size() - 1), 0);
        QCOMPARE(index.data(), 0);
    }

    {
        int largeArray[10000] = {};
        QRangeModel model(&largeArray);
        const QModelIndex index = model.index(int(std::size(largeArray)) - 1, 0);
        QCOMPARE(index.data(), 0);
    }

    {
        std::array<std::array<int, 10000>, 1> largeColumn = {};
        QRangeModel model(largeColumn);
        const QModelIndex index = model.index(int(largeColumn.size() - 1),
                                              int(largeColumn[0].size() - 1));
        QCOMPARE(index.data(), 0);
    }

    {
        using row = std::array<int, 10000>;
        std::array<std::shared_ptr<row>, 1> largeColumn = { std::make_shared<row>() };
        QRangeModel model(largeColumn);
        const QModelIndex index = model.index(int(largeColumn.size() - 1),
                                              int(largeColumn[0]->size() - 1));
        QCOMPARE(index.data(), 0);
    }
}

void tst_QRangeModel::mapsAsRange()
{
    {
        QRangeModel model(QMap<int, QString>{
            {1, u"eins"_s}
        });
        QCOMPARE(model.columnCount(), 1);
    }

    {
        QRangeModel model(QHash<int, QString>{
            {1, u"eins"_s}
        });
        QCOMPARE(model.columnCount(), 1);
    }

    {
        QRangeModel model(std::map<int, QString>{
            {1, u"eins"_s}
        });
        QCOMPARE(model.columnCount(), 2);
    }

    {
        QRangeModel model(std::unordered_map<int, QString>{
            {1, u"eins"_s}
        });
        QCOMPARE(model.columnCount(), 2);
    }
}

void tst_QRangeModel::spanAsRange()
{
    QList<int> list = {1, 2, 3};
    QSpan span(list);
    QRangeModel model(span);
}

void tst_QRangeModel::filterAsRange()
{
#if defined(__cpp_lib_ranges)
    auto view = std::views::iota(0, 100)
              | std::views::filter([](int i){ return 0 == i % 2; })
              | std::views::transform([](int i){ return i * i; });

    QRangeModel model(view);
    QCOMPARE(model.rowCount(), 50);
#else
    QSKIP("Test of std::ranges requires C++ 20");
#endif
}

template <>
struct QRangeModel::ItemAccess<QPolygon>
{
    static QVariant readRole(const QPolygon &polygon, int role)
    {
        if (role == Qt::DisplayRole) {
            QString string;
            bool first = true;
            for (const auto &point : polygon) {
                if (!first)
                    string += ";";
                else
                    first = false;
                string += u"%1/%2"_s.arg(point.x()).arg(point.y());
            }
            return string;
        }
        return QVariant();
    }

    static bool writeRole(QPolygon &target, const QVariant &value, int role)
    {
        target.clear();
        if (role == Qt::DisplayRole || role == Qt::EditRole) {
            bool ok = false;
            for (auto entry : QStringTokenizer(value.toString(), u';')) {
                const auto tokens = entry.tokenize(u'/');
                auto coord = tokens.cbegin();
                QPoint point;
                point.rx() = coord->toInt(&ok);
                if (!ok)
                    break;
                ++coord;
                point.ry() = coord->toInt(&ok);
                if (!ok)
                    break;
                target += point;
            }
            return ok;
        }
        return false;
    }
};

void tst_QRangeModel::multiRoleContainer()
{
    static_assert(QRangeModelDetails::item_access<QPolygon>::hasReadRole);

    QList<QPolygon> listOfPolygons = {
        QPolygon{{0, 0}, {1, 0}, {1, 1}, {0, 1}},
        QPolygon{{0, 0}, {2, 0}, {2, 2}, {0, 2}},
        QPolygon{{0, 0}, {3, 0}, {3, 3}, {0, 3}},
    };

    QRangeModel model(std::ref(listOfPolygons));

    QCOMPARE(model.rowCount(), 3);
    QCOMPARE(model.columnCount(), 1);

    const QModelIndex first = model.index(0, 0);
    QCOMPARE(first.data(), "0/0;1/0;1/1;0/1");
    QVERIFY(model.setData(first, "1/0;1/1;0/1;0/0"));
    QCOMPARE(first.data(), "1/0;1/1;0/1;0/0");
}

template <>
struct QRangeModel::RowOptions<QPoint>
{
    static constexpr auto rowCategory = QRangeModel::RowCategory::MultiRoleItem;
};

void tst_QRangeModel::multiRoleTuple()
{
    QList<QPoint> listOfPoints = {
        {0, 0},
        {1, 0},
        {1, 1},
        {0, 1},
    };

    QRangeModel model(listOfPoints);
    QCOMPARE(model.rowCount(), listOfPoints.size());
    QCOMPARE(model.columnCount(), 1);

    const QModelIndex item = model.index(1, 0);
    QCOMPARE(item.data().value<QPoint>(), listOfPoints.at(1));
    QVERIFY(model.setData(item, listOfPoints.back()));
    QCOMPARE(item.data().value<QPoint>(), listOfPoints.back());
}

namespace ADLTest
{
struct Value
{
    int x;

    template <typename V = Value>
    friend auto refTo(const V &)
    {
        static_assert(QtPrivate::type_dependent_false<V>(),
                      "refTo should never be found through ADL.");
    }
    template <typename V = Value>
    friend auto pointerTo(const V &)
    {
        static_assert(QtPrivate::type_dependent_false<V>(),
                      "pointerTo should never be found through ADL.");
    }
};

struct Range
{
    static inline bool beginCalled = false;
    static inline bool sizeCalled = false;

    friend Value *begin(Range &r)
    {
        Range::beginCalled = true;
        return r.values;
    }

    friend Value *end(Range &r)
    {
        // never called by QRM, only used in tree models
        return r.values + std::size(r.values);
    }

    friend size_t size(const Range &r)
    {
        Range::sizeCalled = true;
        return std::size(r.values);
    }

    Value values[3] = {{0}, {1}, {2}};
};
} // namespace ADLTest

void tst_QRangeModel::adlTest()
{
    QRangeModel adlModel(std::vector<ADLTest::Value>{});

    ADLTest::Range r;

    // compile tests
    {
        QRangeModel model(&r);
    }
    {
        QRangeModel model(std::make_unique<ADLTest::Range>());
    }
    {
        QRangeModel model(std::ref(r));
    }

    QRangeModel model(std::move(r));
    QCOMPARE(model.rowCount(), 3);
    const QModelIndex top = model.index(0, 0);
    const QModelIndex bottom = model.index(model.rowCount() - 1, 0);

    QVERIFY(top.isValid());
    QVERIFY(bottom.isValid());

    QVariant topData = model.data(top);
    QVariant bottomData = model.data(bottom);
    QCOMPARE(topData.value<ADLTest::Value>().x, top.row());
    QCOMPARE(bottomData.value<ADLTest::Value>().x, bottom.row());

    QVERIFY(ADLTest::Range::beginCalled);
    QVERIFY(ADLTest::Range::sizeCalled);
}

class ItemAccessItem : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString display MEMBER m_display)
    Q_PROPERTY(QColor decoration MEMBER m_decoration)
public:
    ItemAccessItem(const QString &display, QColor decoration)
        : m_display(display), m_decoration(decoration)
    {}

    QString m_display;
    QColor m_decoration;
};

template <>
struct QRangeModel::ItemAccess<ItemAccessItem>
{
    static QVariant readRole(const ItemAccessItem &item, int role)
    {
        switch (role) {
        case Qt::DisplayRole:
            return item.m_display.toUpper();
        case Qt::DecorationRole:
            return QColor(item.m_decoration.red(), item.m_decoration.green(),
                          item.m_decoration.blue(), 128);
        default:
            break;
        }
        return {};
    }

    static bool writeRole(ItemAccessItem &item, const QVariant &data, int role)
    {
        switch (role) {
        case Qt::DisplayRole:
        case Qt::EditRole:
            item.m_display = data.toString();
            return true;
        default:
            break;
        }
        return false;
    }
};

void tst_QRangeModel::itemAccess_data()
{
    QTest::addColumn<std::shared_ptr<QRangeModel>>("model");

    QTest::addRow("raw")
        << std::make_shared<QRangeModel>(QList<ItemAccessItem *>{
            new ItemAccessItem{"one", Qt::red}
        });

    QTest::addRow("std::shared_ptr")
        << std::make_shared<QRangeModel>(QList<std::shared_ptr<ItemAccessItem>>{
            std::make_shared<ItemAccessItem>("one", Qt::red),
        });

    {
        auto data = std::vector<std::unique_ptr<ItemAccessItem>>{};
        data.emplace_back(std::make_unique<ItemAccessItem>("one", Qt::red));
        auto model = std::make_shared<QRangeModel>(std::move(data));
        QTest::addRow("std::unique_ptr")
            << model;
    }

    QTest::addRow("QSharedPtr")
        << std::make_shared<QRangeModel>(QList<QSharedPointer<ItemAccessItem>>{
            QSharedPointer<ItemAccessItem>(new ItemAccessItem{"one", Qt::red})
        });
}

void tst_QRangeModel::itemAccess()
{
    QFETCH(std::shared_ptr<QRangeModel>, model);

    QCOMPARE(model->columnCount(), 1);
    const QModelIndex index = model->index(0, 0);
    QCOMPARE(model->data(index), "ONE");
    QCOMPARE(model->data(index, Qt::DecorationRole).value<QColor>().alpha(), 128);
    QVERIFY(model->setData(index, "Two"));
    QCOMPARE(model->data(index), "TWO");
    QVERIFY(!model->setData(index, QVariant::fromValue(Qt::blue), Qt::DecorationRole));
}

void tst_QRangeModel::sortBasic()
{
    { // fast path: no PMIs
        QList<int> list = {1, 10, 3, 8, 5, 6, 7, 4, 9, 2, 11};
        auto sorted = list;
        std::sort(sorted.begin(), sorted.end(), [](int a, int b){ return a > b; });
        QRangeModel(&list).sort(0, Qt::DescendingOrder);
        QCOMPARE(list, sorted);
    }

    { // slow path: with PMIs
        QList<int> list = {1, 10, 3, 8, 5, 6, 7, 4, 9, 2, 11};
        auto sorted = list;
        std::sort(sorted.begin(), sorted.end());

        QRangeModel model(&list);
        QPersistentModelIndex pmi0(model.index(0, 0));
        QPersistentModelIndex pmi4(model.index(4, 0));
        const auto oldData0 = pmi0.data();
        const auto oldData4 = pmi4.data();

        model.sort(0, Qt::AscendingOrder);
        QCOMPARE(list, sorted);

        QCOMPARE(pmi0.data(), oldData0);
        QCOMPARE(pmi4.data(), oldData4);
    }

    { // two columns
        using Item = std::pair<QString, int>;
        QList<Item> list = {
            {"b", 2}, {"d", 4}, {"e", 5}, {"c", 3}, {"a", 1}
        };
        QRangeModel model(&list);
        QPersistentModelIndex pmi0(model.index(0, 0));
        QPersistentModelIndex pmi4(model.index(4, 1));
        const auto oldData0 = pmi0.data();
        const auto oldData4 = pmi4.data();

        model.sort(0, Qt::AscendingOrder);
        QCOMPARE(list, (QList<Item>{
            {"a", 1}, {"b", 2}, {"c", 3}, {"d", 4}, {"e", 5}
        }));
        QCOMPARE(pmi0.data(), oldData0);
        QCOMPARE(pmi4.data(), oldData4);

        model.sort(1, Qt::DescendingOrder);
        QCOMPARE(list, (QList<Item>{
            {"e", 5}, {"d", 4}, {"c", 3}, {"b", 2}, {"a", 1}
        }));
        QCOMPARE(pmi0.data(), oldData0);
        QCOMPARE(pmi4.data(), oldData4);
    }

    { // unordered
#ifndef QT_NO_DEBUG
        QTest::ignoreMessage(QtCriticalMsg, "QRangeModel: "
                             "Cannot compare items of type QPen in column 0!");
#endif
        QRangeModel model(QList<QPen>{{Qt::red}, {Qt::green}, {Qt::blue}});
        model.sort(0);
    }
}

void tst_QRangeModel::sortRole()
{
    QRangeModel model(QList<QMap<int, QVariant>>{
        {
            {Qt::DisplayRole, 4},
            {Qt::DecorationRole, 1},
        },
        {
            {Qt::DisplayRole, 3},
            {Qt::DecorationRole, 2},
        },
        {
            {Qt::DisplayRole, 2},
            {Qt::DecorationRole, 3},
        },
        {
            {Qt::DisplayRole, 1},
            {Qt::DecorationRole, 4},
        },
    });

    QModelIndex index = model.index(0, 0);
    model.setSortRole(Qt::DisplayRole);
    model.sort(0);
    QCOMPARE(model.data(index, Qt::DisplayRole), 1);
    QCOMPARE(model.data(index, Qt::DecorationRole), 4);

    index = model.index(0, 0);
    model.setSortRole(Qt::DecorationRole);
    model.sort(0);
    QCOMPARE(model.data(index, Qt::DisplayRole), 4);
    QCOMPARE(model.data(index, Qt::DecorationRole), 1);
}

void tst_QRangeModel::sortCollator_data()
{
    QTest::addColumn<QVariantList>("data");
    QTest::addColumn<QCollator>("collator");
    QTest::addColumn<QVariantList>("sorted");

    QVariantList caseData{"C", "b", "A", "d"};

    QCollator nullCollator = QCollator(QLocale::C);
    QCollator caseSensitive = nullCollator;
    caseSensitive.setCaseSensitivity(Qt::CaseSensitive);
    QCollator caseInsensitive;
    caseInsensitive.setCaseSensitivity(Qt::CaseInsensitive);
    QCollator localeAware = QCollator(QLocale::German);

    QTest::addRow("CaseSensitive")
        << caseData << caseSensitive << QVariantList{"A", "C", "b", "d"};
    if (caseInsensitive.compare("b", "C") < 0) {
        QTest::addRow("CaseInsensitive")
            << caseData << caseInsensitive << QVariantList{"A", "b", "C", "d"};
    } else {
        qInfo("Platform doesn't implement case insensitive collation.");
    }


    QVariantList i18nData{"a", "z", "ö", "ä"};

    QTest::addRow("LocaleUnaware")
        << i18nData << nullCollator << QVariantList{"a", "z", "ä", "ö"};
    if (localeAware.compare("ä", "z") < 0) {
        QTest::addRow("LocaleAware")
            << i18nData << localeAware << QVariantList{"a", "ä", "ö", "z"};
    } else {
        qInfo("Platform doesn't implement collation with a German locale.");
    }
}

void tst_QRangeModel::sortCollator()
{
    QFETCH(QVariantList, data);
    QFETCH(const QCollator, collator);
    QFETCH(const QVariantList, sorted);

    { // QVariant comparison
        QRangeModel model(std::ref(data));
        model.setSortCollator(collator);
        model.sort(0);
        QCOMPARE(data, sorted);
    }

    { // test shortcut for compile-time typed values
        QStringList stringData;
        for (const auto &var : std::as_const(data))
            stringData << var.toString();
        QStringList sortedStrings;
        for (const auto &var : std::as_const(sorted))
            sortedStrings << var.toString();
        QRangeModel model(std::ref(stringData));
        model.setSortCollator(collator);
        model.sort(0);
        QCOMPARE(stringData, sortedStrings);
    }
}

void tst_QRangeModel::sort()
{
    QFETCH(Factory, factory);
    QFETCH(ChangeActions, changeActions);
    auto model = factory();

    QTest::failOnWarning();

    model->sort(0, Qt::AscendingOrder);
    {
        QPersistentModelIndex pmi = model->index(0, 0);
        model->sort(0, Qt::DescendingOrder);
        if (changeActions.testFlag(ChangeAction::Sort))
            QCOMPARE_NE(pmi.row(), 0);
        else
            QCOMPARE(pmi.row(), 0);
    }

    const int column = model->columnCount() - 1;
    model->sort(column, Qt::DescendingOrder);
    {
        QPersistentModelIndex pmi = model->index(0, column);
        model->sort(column, Qt::AscendingOrder);
        if (changeActions.testFlag(ChangeAction::Sort))
            QCOMPARE_NE(pmi.row(), 0);
        else
            QCOMPARE(pmi.row(), 0);
    }
}

class Person : public QObject {
    Q_OBJECT
    Q_PROPERTY(QString name MEMBER m_name)
    Q_PROPERTY(int age MEMBER m_age)
public:
    Person(const QString &name, int age)
        : m_name(name), m_age(age)
    {}
private:
    QString m_name;
    int m_age;
};

template <>
struct QRangeModel::RowOptions<Person>
{
    static constexpr auto rowCategory = QRangeModel::RowCategory::MultiRoleItem;
};

void tst_QRangeModel::matchBasic()
{
    {
        QList<int> list = {13, 23, 3, 34, 5};
        QRangeModel model(list);
        const QModelIndex start = model.index(0, 0);
        const auto result = model.match(start, Qt::DisplayRole, 3, -1, Qt::MatchExactly);
        QCOMPARE(result.size(), 1);
        QCOMPARE(model.data(result.at(0)), 3);
    }

    {
        QList<QString> list = {"1", "2", "35", "435", "5321"};
        QRangeModel model(list);
        const QModelIndex start = model.index(0, 0);
        const auto result = model.match(start, Qt::DisplayRole, "3", -1, Qt::MatchContains);
        QCOMPARE(result.size(), 3);
        QCOMPARE(model.data(result.at(0)), "35");
        QCOMPARE(model.data(result.at(1)), "435");
        QCOMPARE(model.data(result.at(2)), "5321");
    }

    {
        QList<QString> list = {"1", "2", "133", "43", "15"};
        QRangeModel model(list);
        const QModelIndex start = model.index(0, 0);
        const auto result = model.match(start, Qt::DisplayRole, "1", -1, Qt::MatchStartsWith);
        QCOMPARE(result.size(), 3);
        QCOMPARE(model.data(result.at(0)), "1");
        QCOMPARE(model.data(result.at(1)), "133");
        QCOMPARE(model.data(result.at(2)), "15");
    }

    {
        QList<QString> list = {"1", "2", "33", "43", "5"};
        QRangeModel model(list);
        const QModelIndex start = model.index(0, 0);
        const auto result = model.match(start, Qt::DisplayRole, "3", -1, Qt::MatchEndsWith);
        QCOMPARE(result.size(), 2);
        QCOMPARE(model.data(result.at(0)), "33");
        QCOMPARE(model.data(result.at(1)), "43");
    }

    {
        QList<QString> list = {"ALICE", "Alice", "AliCe", "ALIce"};
        QRangeModel model(list);
        const QModelIndex start = model.index(0, 0);
        const auto result = model.match(start, Qt::DisplayRole, "Alice", -1, Qt::MatchCaseSensitive);
        QCOMPARE(result.size(), 1);
        QCOMPARE(model.data(result.at(0)), "Alice");
    }

    {
        QList<int> list = {1, 2, 3, 2, 4, 2};
        QRangeModel model(list);
        const QModelIndex start = model.index(0, 0);
        const auto result = model.match(start, Qt::DisplayRole, 2, 1, Qt::MatchExactly);
        QCOMPARE(result.size(), 1);
        QCOMPARE(model.data(result.at(0)), 2);
    }

    {
        std::vector<std::unique_ptr<Person>> list;
        list.push_back(std::make_unique<Person>("Alice", 30));
        list.push_back(std::make_unique<Person>("Marie", 25));
        list.push_back(std::make_unique<Person>("Charlie", 30));

        QRangeModel model(std::move(list));
        const QModelIndex start = model.index(0, 0);

        const int nameRole = model.roleNames().key("name");
        const auto matchName = model.match(start, nameRole, "Marie", -1, Qt::MatchExactly);
        QCOMPARE(matchName.size(), 1);
        QCOMPARE(model.data(matchName.at(0), nameRole).toString(), "Marie");

        const int ageRole = model.roleNames().key("age");
        const auto matchAge = model.match(start, ageRole, 30, -1, Qt::MatchExactly);
        QCOMPARE(matchAge.size(), 2);
        QCOMPARE(model.data(matchAge.at(0), ageRole).toInt(), 30);
        QCOMPARE(model.data(matchAge.at(1), ageRole).toInt(), 30);
    }
}

void tst_QRangeModel::match()
{
    QFETCH(Factory, factory);
    QFETCH(QVariant, headerValue);
    auto model = factory();

    const int role = model->roleNames().key(headerValue.toByteArray());
    const QModelIndex start = model->index(0, 0);
    const QVariant value = model->data(start, role);
    { // find one
        const auto result = model->match(start, role, value, 1, Qt::MatchExactly);
        QCOMPARE(result.size(), 1);
        QCOMPARE(result.first(), start);
    }

    { // find all
        const QVariant value = model->data(start, role);
        const auto result = model->match(start, role, value, -1, Qt::MatchExactly);
        QVERIFY(result.size() >= 1);
        QCOMPARE(result.first(), start);
    }

    { // wrap
        const int lastRow = model->rowCount() - 1;
        const QModelIndex lastIndex = model->index(lastRow, 0);
        const auto result = model->match(lastIndex, role, value, 1,
                                         Qt::MatchExactly | Qt::MatchWrap);
        QVERIFY(result.size() >= 1);
    }
}

QTEST_MAIN(tst_QRangeModel)
#include "tst_qrangemodel.moc"
