/*
 * Copyright (C) 2020-2021 Dmitriy Purgin <dpurgin@gmail.com>
 * Copyright (C) 2019-2025 Dmitriy Purgin <dmitriy.purgin@sequality.at>
 * Copyright (C) 2019-2025 sequality software engineering e.U. <office@sequality.at>
 *
 * This file is part of QtOrm library.
 *
 * QtOrm is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * QtOrm 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with QtOrm.  If not, see <https://www.gnu.org/licenses/>.
 */

#include <QtTest>

#include <QOrmEntityInstanceCache>
#include <QOrmError>
#include <QOrmMetadataCache>
#include <QOrmSession>
#include <QOrmSqliteConfiguration>
#include <QOrmSqliteProvider>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QSqlRecord>
#include <QSqlField>

#include "domain/person.h"
#include "domain/province.h"
#include "domain/town.h"
#include "domain/withnotnull.h"
#include "domain/withforeignkey.h"
#include "domain/withunique.h"

#include "private/qormglobal_p.h"

class SqliteSessionTest : public QObject
{
    Q_OBJECT

public:
    SqliteSessionTest();
    ~SqliteSessionTest();

private slots:
    void init();
    void cleanup();

    void testCascadedCreate();
    void testCreateWithTransientProperties();
    void testReadWithTransientProperties();

    void testSelectWithOneToMany();
    void testSelectWithOneToManyWhereIsNull();
    void testSelectWithManyToOne();
    void testSelectReturnsCachedInstances();
    void testSelectWithSingleStringFilter();
    void testSelectWithOrder();
    void testSelectFromNestedSelect();
    void testSelectWithListFilter();
    void testSelectWithLimitOffset();
    void testSelectWithOverwriteCachedInstances();

    void testCount();
    void testCountWithFilter();

    void testMergeFailsWithInconsistentReferences();
    void testMergeOfExistingUncachedEntitiesWithExplicitIdsUpdates();
    void testMergeNewEntitiesNoAutogeneratedIds();
    void testMergeNewEntitiesAfterSchemaUpdate();

    void testRemoveInstance();
    void testRemoveWithFilter();

    void testTransactionRollback();

    void testSchemaCreatedForReferencedEntities();
    void testSchemaAppendCreatesTablesAndAddsColumns();
    void testSchemaUpdateCreatesTablesAndAddsColumns();
    void testSchemaUpdateRemovesColumns();
    void testSchemaUpdateUpdatesNotNull();
    void testSchemaUpdateUpdatesForeignKey();
    void testSchemaUpdateUpdatesUnique();
    void testSchemaUpdateUpdatesUniqueMulti();

    void testForeignKeysEnabled();
};

SqliteSessionTest::SqliteSessionTest()
{
}

SqliteSessionTest::~SqliteSessionTest()
{
}

void SqliteSessionTest::init()
{
    QFile db{"testdb.db"};

    if (db.exists())
        QVERIFY(db.remove());

    qRegisterOrmEntity<Town, Province, Person, WithNotNull, WithForeignKey, WithUnique>();
}

void SqliteSessionTest::cleanup()
{
}

void SqliteSessionTest::testCascadedCreate()
{
    QOrmSqliteConfiguration sqliteConfiguration{};
    sqliteConfiguration.setVerbose(true);
    sqliteConfiguration.setSchemaMode(QOrmSqliteConfiguration::SchemaMode::Recreate);
    sqliteConfiguration.setDatabaseName("testdb.db");
    QOrmSqliteProvider* sqliteProvider = new QOrmSqliteProvider{sqliteConfiguration};
    QOrmSessionConfiguration sessionConfiguration{sqliteProvider, true};
    QOrmSession session{sessionConfiguration};

    Province* upperAustria = new Province(QString::fromUtf8("Oberösterreich"));
    Province* lowerAustria = new Province(QString::fromUtf8("Niederösterreich"));

    Town* hagenberg = new Town(QString::fromUtf8("Hagenberg"), upperAustria);
    Town* pregarten = new Town(QString::fromUtf8("Pregarten"), upperAustria);
    Town* melk = new Town(QString::fromUtf8("Melk"), lowerAustria);

    upperAustria->setTowns({hagenberg, pregarten});
    lowerAustria->setTowns({melk});

    QVERIFY(session.merge(hagenberg, pregarten, melk, upperAustria, lowerAustria));

    QCOMPARE(upperAustria->id(), 1);
    QCOMPARE(lowerAustria->id(), 2);

    QCOMPARE(hagenberg->id(), 1);
    QCOMPARE(pregarten->id(), 2);
    QCOMPARE(melk->id(), 3);

    QOrmSqliteProvider* provider =
        static_cast<QOrmSqliteProvider*>(session.configuration().provider());
    QSqlQuery query{provider->database()};

    QVERIFY(query.exec(QString::fromUtf8("SELECT * FROM Province WHERE name = 'Oberösterreich'")));
    QCOMPARE(query.numRowsAffected(), 1);

    QVERIFY(
        query.exec(QString::fromUtf8("SELECT * FROM Province WHERE name = 'Niederösterreich'")));
    QCOMPARE(query.numRowsAffected(), 1);
}

class TownWithTransient : public Town
{
    Q_OBJECT

    Q_PROPERTY(bool hasLargePopulation READ hasLargePopulation NOTIFY hasLargePopulationChanged)
    Q_ORM_PROPERTY(hasLargePopulation TRANSIENT)

public:
    Q_INVOKABLE TownWithTransient(QObject* parent = nullptr)
        : Town{parent}
    {
    }

    bool hasLargePopulation() const { return false; }

signals:
    void hasLargePopulationChanged();
};

void SqliteSessionTest::testCreateWithTransientProperties()
{
    qRegisterOrmEntity<TownWithTransient>();

    QOrmSqliteConfiguration sqliteConfiguration{};
    sqliteConfiguration.setVerbose(true);
    sqliteConfiguration.setSchemaMode(QOrmSqliteConfiguration::SchemaMode::Recreate);
    sqliteConfiguration.setDatabaseName(":memory:");
    QOrmSqliteProvider* sqliteProvider = new QOrmSqliteProvider{sqliteConfiguration};
    QOrmSessionConfiguration sessionConfiguration{sqliteProvider, true};
    QOrmSession session{sessionConfiguration};

    TownWithTransient* town = new TownWithTransient;
    town->setName("Hagenberg");
    QVERIFY(session.merge(town));

    QSqlQuery query{sqliteProvider->database()};

    QVERIFY(
        query.exec(QString::fromUtf8("SELECT * FROM TownWithTransient WHERE name = 'Hagenberg'")));
    QCOMPARE(query.numRowsAffected(), 1);
}

void SqliteSessionTest::testReadWithTransientProperties()
{
    qRegisterOrmEntity<TownWithTransient>();

    QOrmSqliteConfiguration sqliteConfiguration{};
    sqliteConfiguration.setVerbose(true);
    sqliteConfiguration.setSchemaMode(QOrmSqliteConfiguration::SchemaMode::Bypass);
    sqliteConfiguration.setDatabaseName(":memory:");
    QOrmSqliteProvider* sqliteProvider = new QOrmSqliteProvider{sqliteConfiguration};
    sqliteProvider->connectToBackend();

    QSqlQuery query{sqliteProvider->database()};
    QVERIFY(query.exec(
        "CREATE TABLE TownWithTransient(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)"));
    QVERIFY(query.exec("INSERT INTO TownWithTransient(name) VALUES('Hagenberg')"));

    QOrmSessionConfiguration sessionConfiguration{sqliteProvider, true};
    QOrmSession session{sessionConfiguration};

    auto result = session.from<TownWithTransient>().select();
    QCOMPARE(result.error(), QOrm::ErrorType::None);
    auto data = result.toVector();
    QCOMPARE(data.size(), 1);
    QCOMPARE(data[0]->id(), 1);
    QCOMPARE(data[0]->name(), "Hagenberg");
}

void SqliteSessionTest::testSelectWithOneToMany()
{
    // prepare database
    {
        QOrmSession session;
        Province* upperAustria = new Province(QString::fromUtf8("Oberösterreich"));
        Province* lowerAustria = new Province(QString::fromUtf8("Niederösterreich"));

        Town* hagenberg = new Town(QString::fromUtf8("Hagenberg"), upperAustria);
        Town* pregarten = new Town(QString::fromUtf8("Pregarten"), upperAustria);
        Town* melk = new Town(QString::fromUtf8("Melk"), lowerAustria);

        upperAustria->setTowns({hagenberg, pregarten});
        lowerAustria->setTowns({melk});

        QVERIFY(session.merge(hagenberg, pregarten, melk, upperAustria, lowerAustria));
    }

    // Load data from the database using a new ORM session
    QOrmSqliteConfiguration sqliteConfiguration;
    sqliteConfiguration.setVerbose(true);
    sqliteConfiguration.setSchemaMode(QOrmSqliteConfiguration::SchemaMode::Bypass);
    sqliteConfiguration.setDatabaseName("testdb.db");
    QOrmSqliteProvider* sqliteProvider = new QOrmSqliteProvider{sqliteConfiguration};
    QOrmSessionConfiguration sessionConfiguration{sqliteProvider, true};
    QOrmSession session{sessionConfiguration};

    QOrmQueryResult result = session.from<Province>().select();

    QCOMPARE(result.error().type(), QOrm::ErrorType::None);

    auto data = result.toVector();
    QCOMPARE(data.size(), 2);

    QCOMPARE(qobject_cast<Province*>(data[0])->id(), 1);
    QCOMPARE(qobject_cast<Province*>(data[0])->name(), QString::fromUtf8("Oberösterreich"));
    QCOMPARE(qobject_cast<Province*>(data[0])->towns().size(), 2);

    QCOMPARE(qobject_cast<Province*>(data[0])->towns()[0]->id(), 1);
    QCOMPARE(qobject_cast<Province*>(data[0])->towns()[0]->name(), QString::fromUtf8("Hagenberg"));

    QCOMPARE(qobject_cast<Province*>(data[0])->towns()[1]->id(), 2);
    QCOMPARE(qobject_cast<Province*>(data[0])->towns()[1]->name(), QString::fromUtf8("Pregarten"));

    QCOMPARE(qobject_cast<Province*>(data[1])->id(), 2);
    QCOMPARE(qobject_cast<Province*>(data[1])->name(), QString::fromUtf8("Niederösterreich"));

    QCOMPARE(qobject_cast<Province*>(data[1])->towns()[0]->id(), 3);
    QCOMPARE(qobject_cast<Province*>(data[1])->towns()[0]->name(), QString::fromUtf8("Melk"));
}

void SqliteSessionTest::testSelectWithOneToManyWhereIsNull()
{
    // prepare database
    {
        QOrmSession session;
        Province* upperAustria = new Province(QString::fromUtf8("Oberösterreich"));

        Town* hagenberg = new Town(QString::fromUtf8("Hagenberg"), upperAustria);
        Town* pregarten = new Town(QString::fromUtf8("Pregarten"), upperAustria);
        Town* melk = new Town(QString::fromUtf8("Melk"), nullptr);

        upperAustria->setTowns({hagenberg, pregarten});

        QVERIFY(session.merge(hagenberg, pregarten, melk, upperAustria));
    }

    // Load data from the database using a new ORM session
    QOrmSqliteConfiguration sqliteConfiguration;
    sqliteConfiguration.setVerbose(true);
    sqliteConfiguration.setSchemaMode(QOrmSqliteConfiguration::SchemaMode::Bypass);
    sqliteConfiguration.setDatabaseName("testdb.db");
    QOrmSqliteProvider* sqliteProvider = new QOrmSqliteProvider{sqliteConfiguration};
    QOrmSessionConfiguration sessionConfiguration{sqliteProvider, true};
    QOrmSession session{sessionConfiguration};

    QOrmQueryResult result =
        session.from<Town>().filter(Q_ORM_CLASS_PROPERTY(province) == nullptr).select();

    QCOMPARE(result.error().type(), QOrm::ErrorType::None);

    auto data = result.toVector();
    QCOMPARE(data.size(), 1);

    QCOMPARE(qobject_cast<Town*>(data[0])->id(), 3);
    QCOMPARE(qobject_cast<Town*>(data[0])->name(), QString::fromUtf8("Melk"));
    QCOMPARE(qobject_cast<Town*>(data[0])->province(), nullptr);
}

void SqliteSessionTest::testSelectWithManyToOne()
{
    // prepare database
    {
        QOrmSession session;

        Town* hagenberg = new Town{QString::fromUtf8("Hagenberg"), nullptr};
        Town* pregarten = new Town{QString::fromUtf8("Pregarten"), nullptr};

        Person* franzHuber =
            new Person{QString::fromUtf8("Franz"), QString::fromUtf8("Huber"), hagenberg};
        Person* lisaMaier =
            new Person{QString::fromUtf8("Lisa"), QString::fromUtf8("Maier"), hagenberg};

        Province* upperAustria = new Province(QString::fromUtf8("Oberösterreich"));

        QVERIFY(session.merge(hagenberg, pregarten, franzHuber, lisaMaier, upperAustria));
    }

    // Load data from the database using a new ORM session
    QOrmSqliteConfiguration sqliteConfiguration;
    sqliteConfiguration.setVerbose(true);
    sqliteConfiguration.setSchemaMode(QOrmSqliteConfiguration::SchemaMode::Bypass);
    sqliteConfiguration.setDatabaseName("testdb.db");
    QOrmSqliteProvider* sqliteProvider = new QOrmSqliteProvider{sqliteConfiguration};
    QOrmSessionConfiguration sessionConfiguration{sqliteProvider, true};
    QOrmSession session{sessionConfiguration};

    auto result = session.from<Person>().select();
    QVERIFY((std::is_same_v<decltype(result)::Projection, Person>));

    QCOMPARE(result.error().type(), QOrm::ErrorType::None);

    auto data = result.toVector();
    QCOMPARE(data.size(), 2);

    Person* franzHuber{data[0]};

    QCOMPARE(franzHuber->id(), 1);
    QCOMPARE(franzHuber->firstName(), QString::fromUtf8("Franz"));
    QCOMPARE(franzHuber->lastName(), QString::fromUtf8("Huber"));
    QVERIFY(franzHuber->town() != nullptr);
    QCOMPARE(franzHuber->town()->name(), QString::fromUtf8("Hagenberg"));

    Person* lisaMaier{data[1]};
    QCOMPARE(lisaMaier->id(), 2);
    QCOMPARE(lisaMaier->firstName(), QString::fromUtf8("Lisa"));
    QCOMPARE(lisaMaier->lastName(), QString::fromUtf8("Maier"));
    QVERIFY(lisaMaier->town() != nullptr);
    QCOMPARE(lisaMaier->town()->name(), QString::fromUtf8("Hagenberg"));
}

void SqliteSessionTest::testSelectReturnsCachedInstances()
{
    QOrmSession session;

    Province* upperAustria = new Province(QString::fromUtf8("Oberösterreich"));
    Province* lowerAustria = new Province(QString::fromUtf8("Niederösterreich"));

    QVERIFY(session.merge(upperAustria, lowerAustria));

    auto result = session.from<Province>().select().toVector();
    QVERIFY((std::is_same_v<QVector<Province*>, decltype(result)>));
    QCOMPARE(result.size(), 2);
    QVERIFY(result.contains(upperAustria));
    QVERIFY(result.contains(lowerAustria));
}

void SqliteSessionTest::testSelectWithSingleStringFilter()
{
    QOrmSession session;
    session.merge(new Province(QString::fromUtf8("Oberösterreich")),
                  new Province(QString::fromUtf8("Niederösterreich")),
                  new Province(QString::fromUtf8("Salzburg")));

    auto result = session.from<Province>()
                      .filter(Q_ORM_CLASS_PROPERTY(name) == QString::fromUtf8("Oberösterreich"))
                      .select();

    QCOMPARE(result.error().type(), QOrm::ErrorType::None);
    QCOMPARE(result.toVector().size(), 1);

    auto upperAustria = result.toVector().front();
    QCOMPARE(upperAustria->name(), QString::fromUtf8("Oberösterreich"));
}

void SqliteSessionTest::testSelectWithOrder()
{
    QOrmSession session;
    session.merge(new Province(QString::fromUtf8("Tirol")),
                  new Province(QString::fromUtf8("Oberösterreich")),
                  new Province(QString::fromUtf8("Niederösterreich")),
                  new Province(QString::fromUtf8("Salzburg")));

    {
        auto result =
            session.from<Province>().order(Q_ORM_CLASS_PROPERTY(name)).select().toVector();

        QCOMPARE(result.size(), 4);
        QCOMPARE(result[0]->name(), QString::fromUtf8("Niederösterreich"));
        QCOMPARE(result[1]->name(), QString::fromUtf8("Oberösterreich"));
        QCOMPARE(result[2]->name(), QString::fromUtf8("Salzburg"));
        QCOMPARE(result[3]->name(), QString::fromUtf8("Tirol"));
    }

    {
        auto result = session.from<Province>()
                          .order(Q_ORM_CLASS_PROPERTY(name), Qt::DescendingOrder)
                          .select()
                          .toVector();

        QCOMPARE(result.size(), 4);
        QCOMPARE(result[0]->name(), QString::fromUtf8("Tirol"));
        QCOMPARE(result[1]->name(), QString::fromUtf8("Salzburg"));
        QCOMPARE(result[2]->name(), QString::fromUtf8("Oberösterreich"));
        QCOMPARE(result[3]->name(), QString::fromUtf8("Niederösterreich"));
    }
}

void SqliteSessionTest::testSelectFromNestedSelect()
{
    QOrmSession session;

    auto upperAustria = new Province{QString::fromUtf8("Oberösterreich")};
    auto lowerAustria = new Province{QString::fromUtf8("Niederösterreich")};

    auto freistadt = new Town{QString::fromUtf8("Freistadt"), upperAustria};
    auto hagenberg = new Town{QString::fromUtf8("Hagenberg im Mühlkreis"), upperAustria};
    auto melk = new Town{QString::fromUtf8("Melk"), lowerAustria};

    upperAustria->setTowns({freistadt, hagenberg});
    lowerAustria->setTowns({melk});

    session.merge(upperAustria, lowerAustria, freistadt, hagenberg, melk);

    auto nested = session.from<Town>()
                      .filter(Q_ORM_CLASS_PROPERTY(province) == upperAustria)
                      .build(QOrm::Operation::Read);

    auto result =
        session.from(nested).order(Q_ORM_CLASS_PROPERTY(name), Qt::DescendingOrder).select();

    QCOMPARE(result.toVector().size(), 2);
}

void SqliteSessionTest::testSelectWithListFilter()
{
    QOrmSession session;
    session.merge(new Province(QString::fromUtf8("Tirol")),
                  new Province(QString::fromUtf8("Oberösterreich")),
                  new Province(QString::fromUtf8("Niederösterreich")),
                  new Province(QString::fromUtf8("Salzburg")));

    {
        auto result = session.from<Province>()
                          .filter(Q_ORM_CLASS_PROPERTY(id) == QVector<int>{1, 3, 4})
                          .order(Q_ORM_CLASS_PROPERTY(id))
                          .select()
                          .toVector();
        QCOMPARE(result.size(), 3);
        QCOMPARE(result.at(0)->id(), 1);
        QCOMPARE(result.at(1)->id(), 3);
        QCOMPARE(result.at(2)->id(), 4);
    }

    {
        auto result = session.from<Province>()
                          .filter(Q_ORM_CLASS_PROPERTY(id) != QVector<int>{1, 3})
                          .order(Q_ORM_CLASS_PROPERTY(id))
                          .select()
                          .toVector();
        QCOMPARE(result.size(), 2);
        QCOMPARE(result.at(0)->id(), 2);
        QCOMPARE(result.at(1)->id(), 4);
    }
}

void SqliteSessionTest::testSelectWithLimitOffset()
{
    QOrmSession session;
    session.merge(new Province(QString::fromUtf8("Tirol")),
                  new Province(QString::fromUtf8("Oberösterreich")),
                  new Province(QString::fromUtf8("Niederösterreich")),
                  new Province(QString::fromUtf8("Salzburg")),
                  new Province(QString::fromUtf8("Burgenland")),
                  new Province(QString::fromUtf8("Kärnten")));

    {
        auto result =
            session.from<Province>().order(Q_ORM_CLASS_PROPERTY(id)).limit(3).select().toVector();

        QCOMPARE(result.size(), 3);
        QCOMPARE(result.at(0)->id(), 1);
        QCOMPARE(result.at(1)->id(), 2);
        QCOMPARE(result.at(2)->id(), 3);
    }

    {
        auto result =
            session.from<Province>().order(Q_ORM_CLASS_PROPERTY(id)).offset(2).select().toVector();

        QCOMPARE(result.size(), 4);
        QCOMPARE(result.at(0)->id(), 3);
        QCOMPARE(result.at(1)->id(), 4);
        QCOMPARE(result.at(2)->id(), 5);
        QCOMPARE(result.at(3)->id(), 6);
    }

    {
        auto result = session.from<Province>()
                          .order(Q_ORM_CLASS_PROPERTY(id))
                          .limit(3)
                          .offset(2)
                          .select()
                          .toVector();

        QCOMPARE(result.size(), 3);
        QCOMPARE(result.at(0)->id(), 3);
        QCOMPARE(result.at(1)->id(), 4);
        QCOMPARE(result.at(2)->id(), 5);
    }
}

void SqliteSessionTest::testSelectWithOverwriteCachedInstances()
{
    QOrmSession session;

    Province* upperAustria = new Province(QString::fromUtf8("Oberösterreich"));

    QVERIFY(session.merge(upperAustria));

    upperAustria->setName("Oberoesterreich");

    QVERIFY(session.entityInstanceCache()->isModified(upperAustria));

    std::ignore =
        session.from<Province>().select(QOrm::QueryFlags::OverwriteCachedInstances).toVector();
    QCOMPARE(upperAustria->name(), QString::fromUtf8("Oberösterreich"));
    QVERIFY(!session.entityInstanceCache()->isModified(upperAustria));
}

void SqliteSessionTest::testCount()
{
    QOrmSession session;
    session.merge(new Province(QString::fromUtf8("Oberösterreich")),
                  new Province(QString::fromUtf8("Niederösterreich")),
                  new Province(QString::fromUtf8("Salzburg")));

    auto result = session.from<Province>().count();

    QCOMPARE(result.error().type(), QOrm::ErrorType::None);
    QCOMPARE(result.value(), 3);
}

void SqliteSessionTest::testCountWithFilter()
{
    QOrmSession session;
    session.merge(new Province(QString::fromUtf8("Oberösterreich")),
                  new Province(QString::fromUtf8("Niederösterreich")),
                  new Province(QString::fromUtf8("Salzburg")));

    auto result =
        session.from<Province>().filter(Q_ORM_CLASS_PROPERTY(name).contains("österreich")).count();

    QCOMPARE(result.error().type(), QOrm::ErrorType::None);
    QCOMPARE(result.value(), 2);
}

void SqliteSessionTest::testMergeFailsWithInconsistentReferences()
{
    QOrmSession session;
    QOrmMetadataCache* metadataCache = session.metadataCache();

    Province* upperAustria = new Province(QString::fromUtf8("Oberösterreich"));
    Province* lowerAustria = new Province(QString::fromUtf8("Niederösterreich"));

    Town* hagenberg = new Town(QString::fromUtf8("Hagenberg"), upperAustria);
    Town* pregarten = new Town(QString::fromUtf8("Pregarten"), upperAustria);
    Town* melk = new Town(QString::fromUtf8("Melk"), lowerAustria);

    upperAustria->setTowns({pregarten});

    QVERIFY(QOrmPrivate::crossReferenceError(metadataCache->get<Town>(), hagenberg).has_value());
    QVERIFY(!QOrmPrivate::crossReferenceError(metadataCache->get<Town>(), pregarten).has_value());
    QVERIFY(QOrmPrivate::crossReferenceError(metadataCache->get<Town>(), melk).has_value());

    // Province instances to have a cross-reference error but it cannot be detected because the
    // referencing issues are not in the list. The cross-reference is detected on the other side
    // of the relation.
    QVERIFY(!QOrmPrivate::crossReferenceError(metadataCache->get<Province>(), upperAustria)
                 .has_value());
    QVERIFY(!QOrmPrivate::crossReferenceError(metadataCache->get<Province>(), lowerAustria)
                 .has_value());
}

void SqliteSessionTest::testMergeOfExistingUncachedEntitiesWithExplicitIdsUpdates()
{
    int idUpperAustria = -1;
    int idLowerAustria = -1;

    {
        QOrmSession session;

        Province* upperAustria = new Province(QString::fromUtf8("Oberösterreich"));
        Province* lowerAustria = new Province(QString::fromUtf8("Niederösterreich"));

        session.merge(upperAustria, lowerAustria);

        idUpperAustria = upperAustria->id();
        idLowerAustria = lowerAustria->id();
    }

    {
        QOrmSession session{QOrmSessionConfiguration::fromFile(":/qtorm_bypass_schema.json")};

        Province* upperAustria = new Province(idUpperAustria, QString::fromUtf8("Oberösterreich"));
        Province* lowerAustria =
            new Province(idLowerAustria, QString::fromUtf8("Niederösterreich"));

        session.merge(upperAustria, lowerAustria);

        QEXPECT_FAIL("", "Not implemented yet", TestFailMode::Abort);
        QCOMPARE(upperAustria->id(), idUpperAustria);
        QCOMPARE(lowerAustria->id(), idLowerAustria);
        QCOMPARE(session.from<Province>().select().toVector().size(), 2);
    }
}

class Community : public QObject
{
    Q_OBJECT

    Q_PROPERTY(int id READ id WRITE setId NOTIFY idChanged)
    Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
    Q_ORM_PROPERTY(id IDENTITY AUTOGENERATED false)

public:
    Q_INVOKABLE explicit Community(QObject* parent = nullptr)
        : QObject{parent}
    {
    }

    int id() const { return m_id; }
    void setId(int id)
    {
        if (m_id != id)
        {
            m_id = id;
            emit idChanged();
        }
    }

    QString name() const { return m_name; }
    void setName(const QString& name)
    {
        if (m_name != name)
        {
            m_name = name;
            emit nameChanged();
        }
    }

signals:
    void idChanged();
    void nameChanged();

private:
    int m_id{0};
    QString m_name;
};

void SqliteSessionTest::testMergeNewEntitiesNoAutogeneratedIds()
{
    QOrmSession session;

    Community* hagenberg = new Community();
    hagenberg->setId(4232);
    hagenberg->setName("Hagenberg");

    QVERIFY(session.merge(hagenberg));

    QCOMPARE(hagenberg->id(), 4232);
}

void SqliteSessionTest::testMergeNewEntitiesAfterSchemaUpdate()
{
    QOrmSession session{QOrmSessionConfiguration::fromFile(":/qtorm_update_schema.json")};

    Community* hagenberg = new Community();
    hagenberg->setId(4232);
    hagenberg->setName("Hagenberg");

    QVERIFY(session.merge(hagenberg));

    QCOMPARE(hagenberg->id(), 4232);
}

void SqliteSessionTest::testTransactionRollback()
{
    QOrmSession session;

    Province* upperAustria = new Province(QString::fromUtf8("Oberösterreich"));
    Province* lowerAustria = new Province(QString::fromUtf8("Niederösterreich"));

    QVERIFY(session.merge(upperAustria, lowerAustria));

    {
        auto transactionToken = session.declareTransaction(QOrm::TransactionPropagation::Require,
                                                           QOrm::TransactionAction::Rollback);

        upperAustria->setName(QString::fromUtf8("Upper Austria"));
        QVERIFY(session.merge(upperAustria));
    }

    QCOMPARE(upperAustria->name(), QString::fromUtf8("Oberösterreich"));
}

void SqliteSessionTest::testSchemaCreatedForReferencedEntities()
{
    {
        QOrmSession session;
        QVERIFY(session.merge(new Province("Oberösterreich")));
    }

    {
        QOrmSession session{QOrmSessionConfiguration::fromFile(":/qtorm_bypass_schema.json")};

        auto result = session.from<Province>().select();
        QCOMPARE(result.error().type(), QOrm::ErrorType::None);
    }
}

void SqliteSessionTest::testSchemaAppendCreatesTablesAndAddsColumns()
{
    {
        QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
        db.setDatabaseName("testdb.db");
        QVERIFY(db.open());

        static const QStringList statements{
            "CREATE TABLE Town(id INTEGER PRIMARY KEY AUTOINCREMENT, population INTEGER)",
            "INSERT INTO Town(id, population) VALUES(1, 1000)",
            "INSERT INTO Town(id, population) VALUES(2, 20000)",
            "CREATE TABLE Person(id INTEGER PRIMARY KEY AUTOINCREMENT)"};

        for (const QString& statement : statements)
        {
            qDebug() << "Executing" << statement;
            QSqlQuery query{db};
            QVERIFY(query.exec(statement));
            QCOMPARE(query.lastError().type(), QSqlError::NoError);
        }

        db.close();
        QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection);
    }

    {
        QOrmSession session{QOrmSessionConfiguration::fromFile(":/qtorm_append_schema.json")};

        auto result = session.from<Town>().select();
        QCOMPARE(result.error().type(), QOrm::ErrorType::None);
        auto townData = result.toVector();
        QCOMPARE(townData.size(), 2);
        QCOMPARE(townData[0]->id(), 1);
        QCOMPARE(townData[1]->id(), 2);

        result = session.from<Province>().select();
        QCOMPARE(result.error().type(), QOrm::ErrorType::None);

        result = session.from<Person>().select();
        QCOMPARE(result.error().type(), QOrm::ErrorType::None);
    }

    {
        QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
        db.setDatabaseName("testdb.db");
        QVERIFY(db.open());

        QVERIFY(db.tables().contains("Town"));
        QSqlRecord record = db.record("Town");
        QCOMPARE(record.count(), 4);
        QVERIFY(record.contains("id"));
        QVERIFY(record.contains("name"));
        QVERIFY(record.contains("population"));
        QVERIFY(record.contains("province_id"));

        QVERIFY(db.tables().contains("Province"));
        record = db.record("Province");
        QCOMPARE(record.count(), 2);
        QVERIFY(record.contains("id"));
        QVERIFY(record.contains("name"));

        QVERIFY(db.tables().contains("Person"));
        record = db.record("Person");
        QCOMPARE(record.count(), 5);
        QVERIFY(record.contains("id"));
        QVERIFY(record.contains("firstName"));
        QVERIFY(record.contains("lastName"));
        QVERIFY(record.contains("town_id"));
        QVERIFY(record.contains("personParent_id"));

        db.close();
        QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection);
    }
}

void SqliteSessionTest::testSchemaUpdateCreatesTablesAndAddsColumns()
{
    {
        QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
        db.setDatabaseName("testdb.db");
        QVERIFY(db.open());

        static const QStringList statements{
            "CREATE TABLE Town(id INTEGER PRIMARY KEY AUTOINCREMENT)",
            "INSERT INTO Town(id) VALUES(1)",
            "INSERT INTO Town(id) VALUES(2)",
            "CREATE TABLE Person(id INTEGER PRIMARY KEY AUTOINCREMENT)"};

        for (const QString& statement : statements)
        {
            qDebug() << "Executing" << statement;
            QSqlQuery query{db};
            QVERIFY(query.exec(statement));
            QCOMPARE(query.lastError().type(), QSqlError::NoError);
        }

        db.close();
        QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection);
    }

    {
        QOrmSession session{QOrmSessionConfiguration::fromFile(":/qtorm_update_schema.json")};

        auto result = session.from<Town>().select();
        QCOMPARE(result.error().type(), QOrm::ErrorType::None);
        auto townData = result.toVector();
        QCOMPARE(townData.size(), 2);
        QCOMPARE(townData[0]->id(), 1);
        QCOMPARE(townData[1]->id(), 2);

        result = session.from<Province>().select();
        QCOMPARE(result.error().type(), QOrm::ErrorType::None);

        result = session.from<Person>().select();
        QCOMPARE(result.error().type(), QOrm::ErrorType::None);
    }

    {
        QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
        db.setDatabaseName("testdb.db");
        QVERIFY(db.open());

        QVERIFY(db.tables().contains("Town"));
        QSqlRecord record = db.record("Town");
        QCOMPARE(record.count(), 3);
        QVERIFY(record.contains("id"));
        QVERIFY(record.contains("name"));
        QVERIFY(record.contains("province_id"));

        QVERIFY(db.tables().contains("Province"));
        record = db.record("Province");
        QCOMPARE(record.count(), 2);
        QVERIFY(record.contains("id"));
        QVERIFY(record.contains("name"));

        QVERIFY(db.tables().contains("Person"));
        record = db.record("Person");
        QCOMPARE(record.count(), 5);
        QVERIFY(record.contains("id"));
        QVERIFY(record.contains("firstName"));
        QVERIFY(record.contains("lastName"));
        QVERIFY(record.contains("town_id"));
        QVERIFY(record.contains("personParent_id"));

        db.close();
        QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection);
    }
}

void SqliteSessionTest::testSchemaUpdateRemovesColumns()
{
    {
        QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
        db.setDatabaseName("testdb.db");
        QVERIFY(db.open());

        QStringList statements{"CREATE TABLE Town(id INTEGER PRIMARY KEY AUTOINCREMENT"
                               ", postalCode INTEGER"
                               ", name TEXT"
                               ", province_id INTEGER)",
                               "INSERT INTO Town(postalCode, name, province_id) "
                               "    VALUES(4232, 'Hagenberg', 1)",
                               "INSERT INTO Town(postalCode, name, province_id) "
                               "    VALUES(3860, 'Heidenreichstein', 2)",
                               "CREATE TABLE Province(id INTEGER PRIMARY KEY AUTOINCREMENT"
                               ", name TEXT)",
                               "INSERT INTO Province(name) VALUES('Oberoesterreich')",
                               "INSERT INTO Province(name) VALUES('Niederoesterreich')"};

        for (const QString& statement : statements)
        {
            QSqlQuery query{db};
            QVERIFY(query.exec(statement));
            QCOMPARE(query.lastError().type(), QSqlError::NoError);
        }

        db.close();
        QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection);
    }

    {
        QOrmSession session{QOrmSessionConfiguration::fromFile(":/qtorm_update_schema.json")};

        auto result = session.from<Town>().select();
        QCOMPARE(result.error().type(), QOrm::ErrorType::None);

        auto townData = result.toVector();
        QCOMPARE(townData.size(), 2);
        QCOMPARE(townData[0]->id(), 1);
        QCOMPARE(townData[0]->name(), "Hagenberg");
        QVERIFY(townData[0]->province() != nullptr);
        QCOMPARE(townData[0]->province()->id(), 1);
        QCOMPARE(townData[0]->province()->name(), "Oberoesterreich");

        QCOMPARE(townData[1]->id(), 2);
        QCOMPARE(townData[1]->name(), "Heidenreichstein");
        QVERIFY(townData[1]->province() != nullptr);
        QCOMPARE(townData[1]->province()->id(), 2);
        QCOMPARE(townData[1]->province()->name(), "Niederoesterreich");

        result = session.from<Province>().select();
        QCOMPARE(result.error().type(), QOrm::ErrorType::None);

        result = session.from<Person>().select();
        QCOMPARE(result.error().type(), QOrm::ErrorType::None);
    }

    {
        QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
        db.setDatabaseName("testdb.db");
        QVERIFY(db.open());

        QVERIFY(db.tables().contains("Town"));
        QSqlRecord record = db.record("Town");
        QCOMPARE(record.count(), 3);
        QVERIFY(record.contains("id"));
        QVERIFY(record.contains("name"));
        QVERIFY(record.contains("province_id"));

        QVERIFY(db.tables().contains("Province"));
        record = db.record("Province");
        QCOMPARE(record.count(), 2);
        QVERIFY(record.contains("id"));
        QVERIFY(record.contains("name"));

        QVERIFY(db.tables().contains("Person"));
        record = db.record("Person");
        QCOMPARE(record.count(), 5);
        QVERIFY(record.contains("id"));
        QVERIFY(record.contains("firstName"));
        QVERIFY(record.contains("lastName"));
        QVERIFY(record.contains("town_id"));
        QVERIFY(record.contains("personParent_id"));

        db.close();
        QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection);
    }
}

void SqliteSessionTest::testSchemaUpdateUpdatesNotNull()
{
    {
        QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
        db.setDatabaseName("testdb.db");
        QVERIFY(db.open());

        static const QStringList statements{
            "CREATE TABLE WithNotNull(id INTEGER PRIMARY KEY AUTOINCREMENT, data INTEGER)",
            "INSERT INTO WithNotNull(id, data) VALUES(1, 2)",
            "INSERT INTO WithNotNull(id, data) VALUES(2, 3)"};

        for (const QString& statement : statements)
        {
            qDebug() << "Executing" << statement;
            QSqlQuery query{db};
            QVERIFY(query.exec(statement));
            QCOMPARE(query.lastError().type(), QSqlError::NoError);
        }

        db.close();
        QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection);
    }

    {
        QOrmSession session{QOrmSessionConfiguration::fromFile(":/qtorm_update_schema.json")};

        auto result = session.from<WithNotNull>().select();
        QCOMPARE(result.error().type(), QOrm::ErrorType::None);
        auto notNullData = result.toVector();
        QCOMPARE(notNullData.size(), 2);
        QCOMPARE(notNullData[0]->id(), 1);
        QCOMPARE(notNullData[1]->id(), 2);
    }

    {
        QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
        db.setDatabaseName("testdb.db");
        QVERIFY(db.open());

        QVERIFY(db.tables().contains("WithNotNull"));
        QSqlRecord record = db.record("WithNotNull");
        QCOMPARE(record.count(), 2);
        QVERIFY(record.contains("id"));
        QVERIFY(record.contains("data"));
        QCOMPARE(record.field("data").requiredStatus(), QSqlField::Required);

        db.close();
        QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection);
    }
}

void SqliteSessionTest::testSchemaUpdateUpdatesForeignKey()
{
    {
        QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
        db.setDatabaseName("testdb.db");
        QVERIFY(db.open());

        static const QStringList statements{
            "CREATE TABLE WithNotNull(id INTEGER PRIMARY KEY AUTOINCREMENT, data INTEGER NOT NULL)",
            "INSERT INTO WithNotNull(id, data) VALUES(1, 2)",
            "CREATE TABLE WithForeignKey(id INTEGER PRIMARY KEY AUTOINCREMENT, data_id INTEGER)",
            "INSERT INTO WithForeignKey(id, data_id) VALUES(1, 1)",
            "INSERT INTO WithForeignKey(id, data_id) VALUES(2, 1)"};

        for (const QString& statement : statements)
        {
            qDebug() << "Executing" << statement;
            QSqlQuery query{db};
            QVERIFY(query.exec(statement));
            QCOMPARE(query.lastError().type(), QSqlError::NoError);
        }

        db.close();
        QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection);
    }

    {
        QOrmSession session{QOrmSessionConfiguration::fromFile(":/qtorm_update_schema.json")};

        auto result = session.from<WithForeignKey>().select();
        QCOMPARE(result.error().type(), QOrm::ErrorType::None);
        auto fkData = result.toVector();
        QCOMPARE(fkData.size(), 2);
        QCOMPARE(fkData[0]->id(), 1);
        QCOMPARE(fkData[1]->id(), 2);
    }

    {
        QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
        db.setDatabaseName("testdb.db");
        QVERIFY(db.open());

        QSqlQuery query{db};
        QVERIFY(query.exec(R"(SELECT 1 FROM pragma_foreign_key_list('WithForeignKey') WHERE "from" = 'data_id';)"));
        QCOMPARE(query.lastError().type(), QSqlError::NoError);
        QVERIFY(query.first());

        db.close();
        QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection);
    }
}

void SqliteSessionTest::testSchemaUpdateUpdatesUnique()
{
    {
        QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
        db.setDatabaseName("testdb.db");
        QVERIFY(db.open());

        static const QStringList statements{
            "CREATE TABLE WithUnique(id INTEGER PRIMARY KEY AUTOINCREMENT, single TEXT, multione TEXT, multitwo TEXT, UNIQUE(multione, multitwo))",
            "INSERT INTO WithUnique(id, single, multione, multitwo) VALUES(1, 'test', 'test', 'test')",
            "INSERT INTO WithUnique(id, single, multione, multitwo) VALUES(2, 'test2', 'test', 'test2')"};

        for (const QString& statement : statements)
        {
            qDebug() << "Executing" << statement;
            QSqlQuery query{db};
            QVERIFY(query.exec(statement));
            QCOMPARE(query.lastError().type(), QSqlError::NoError);
        }

        db.close();
        QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection);
    }

    {
        QOrmSession session{QOrmSessionConfiguration::fromFile(":/qtorm_update_schema.json")};

        auto result = session.from<WithUnique>().select();
        QCOMPARE(result.error().type(), QOrm::ErrorType::None);
        auto fkData = result.toVector();
        QCOMPARE(fkData.size(), 2);
        QCOMPARE(fkData[0]->id(), 1);
        QCOMPARE(fkData[1]->id(), 2);
    }

    {
        QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
        db.setDatabaseName("testdb.db");
        QVERIFY(db.open());

        QSqlQuery query{db};
        QVERIFY(!query.exec("INSERT INTO WithUnique(id, single, multione, multitwo) VALUES(3, 'test', 'test', 'test3')"));
        QCOMPARE(query.lastError().type(), QSqlError::ConnectionError);

        db.close();
        QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection);
    }
}

void SqliteSessionTest::testSchemaUpdateUpdatesUniqueMulti()
{
    {
        QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
        db.setDatabaseName("testdb.db");
        QVERIFY(db.open());

        static const QStringList statements{
            "CREATE TABLE WithUnique(id INTEGER PRIMARY KEY AUTOINCREMENT, single TEXT UNIQUE, multione TEXT UNIQUE, multitwo TEXT)",
            "INSERT INTO WithUnique(id, single, multione, multitwo) VALUES(1, 'test', 'test', 'test')",
            "INSERT INTO WithUnique(id, single, multione, multitwo) VALUES(2, 'test2', 'test2', 'test')"};

        for (const QString& statement : statements)
        {
            qDebug() << "Executing" << statement;
            QSqlQuery query{db};
            QVERIFY(query.exec(statement));
            QCOMPARE(query.lastError().type(), QSqlError::NoError);
        }

        db.close();
        QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection);
    }

    {
        QOrmSession session{QOrmSessionConfiguration::fromFile(":/qtorm_update_schema.json")};

        auto result = session.from<WithUnique>().select();
        QCOMPARE(result.error().type(), QOrm::ErrorType::None);
        auto fkData = result.toVector();
        QCOMPARE(fkData.size(), 2);
        QCOMPARE(fkData[0]->id(), 1);
        QCOMPARE(fkData[1]->id(), 2);
    }

    {
        QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
        db.setDatabaseName("testdb.db");
        QVERIFY(db.open());

        QSqlQuery query{db};
        QVERIFY(query.exec("INSERT INTO WithUnique(id, single, multione, multitwo) VALUES(3, 'test3', 'test', 'test3')"));
        QCOMPARE(query.lastError().type(), QSqlError::NoError);

        db.close();
        QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection);
    }
}

void SqliteSessionTest::testRemoveInstance()
{
    QOrmSession session;

    Province* upperAustria = new Province{QString::fromUtf8("Oberösterreich")};
    QVERIFY(session.merge(upperAustria));
    QVERIFY(session.remove(upperAustria));
    QVERIFY(session.from<Province>().select().toVector().empty());
}

void SqliteSessionTest::testRemoveWithFilter()
{
    QOrmSession session;

    // Do not run this test if the provider does not support remove with filter.
    QOrmSqliteProvider::SqliteCapabilities caps{session.configuration().provider()->capabilities()};

    if (!caps.testFlag(QOrmSqliteProvider::SupportsReturningClause))
    {
        QSKIP("SQLite version does not support RETURNING");
    }

    Province* upperAustria = new Province();
    upperAustria->setName("Oberösterreich");

    Province* lowerAustria = new Province();
    lowerAustria->setName("Niederösterreich");

    Town* hagenberg = new Town();
    hagenberg->setName("Hagenberg");
    hagenberg->setProvince(upperAustria);

    Town* linz = new Town();
    linz->setName("Linz");
    linz->setProvince(upperAustria);

    Town* melk = new Town();
    melk->setName("Melk");
    melk->setProvince(lowerAustria);

    upperAustria->setTowns({hagenberg, linz});
    lowerAustria->setTowns({melk});

    session.merge(upperAustria, lowerAustria, hagenberg, linz, melk);

    {
        auto query = session.from<Town>().select();
        QCOMPARE(query.error().type(), QOrm::ErrorType::None);

        auto rowset = query.toVector();
        QCOMPARE(rowset.size(), 3);
        QVERIFY(rowset.contains(hagenberg));
        QVERIFY(rowset.contains(linz));
        QVERIFY(rowset.contains(melk));
    }

    {
        auto query =
            session.from<Town>().filter(Q_ORM_CLASS_PROPERTY(province) == upperAustria).remove();
        QCOMPARE(query.error(), QOrm::ErrorType::None);
        QCOMPARE(query.numRowsAffected(), 2);

        QVERIFY(!session.entityInstanceCache()->contains(hagenberg));
        QVERIFY(!session.entityInstanceCache()->contains(linz));

        auto removedInstances = query.toVector();

        QCOMPARE(removedInstances.size(), 2);
        QVERIFY(removedInstances.contains(hagenberg));
        QVERIFY(removedInstances.contains(linz));
        qDeleteAll(query.toVector());
    }
}

void SqliteSessionTest::testForeignKeysEnabled()
{
    QOrmSqliteConfiguration sqliteConfiguration{};
    sqliteConfiguration.setForeignKeysEnabled(true);
    sqliteConfiguration.setDatabaseName(":memory:");
    QOrmSqliteProvider* sqliteProvider = new QOrmSqliteProvider{sqliteConfiguration};
    QOrmSessionConfiguration sessionConfiguration{sqliteProvider, true};
    QOrmSession session{sessionConfiguration};

    QOrmSqliteProvider* provider =
        static_cast<QOrmSqliteProvider*>(session.configuration().provider());
    provider->connectToBackend();
    QSqlQuery query{provider->database()};

    QVERIFY(query.exec("PRAGMA foreign_keys") && query.next());
    QCOMPARE(query.value("foreign_keys").toBool(), true);
}

QTEST_GUILESS_MAIN(SqliteSessionTest)

#include "tst_ormsession.moc"
