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

#include "tst_qmlls_qqmlcodemodel.h"

#include <QtQmlToolingSettings/private/qqmltoolingsettings_p.h>
#include <QtQmlLS/private/qprocessscheduler_p.h>
#include <QtQmlLS/private/qqmlcodemodel_p.h>
#include <QtQmlLS/private/qqmlcodemodelmanager_p.h>
#include <QtQmlLS/private/qqmllsutils_p.h>
#include <QtQmlDom/private/qqmldomitem_p.h>
#include <QtQmlDom/private/qqmldomtop_p.h>

#include <QtCore/qstringlist.h>
#include <QtTest/qsignalspy.h>

struct TestCodeModelManager final : public QmlLsp::QQmlCodeModelManager
{
    TestCodeModelManager(QQmlToolingSharedSettings *settings = nullptr)
        : QmlLsp::QQmlCodeModelManager(nullptr, settings)
    {
        disableCMakeCalls();
    }

    QmlLsp::QQmlCodeModel *findCodeModelForFile(const QByteArray &url)
    {
        return QmlLsp::QQmlCodeModelManager::findCodeModelForFile(url);
    }
    QmlLsp::QQmlCodeModel *findCodeModel(const QByteArray &url)
    {
        const auto it = QmlLsp::QQmlCodeModelManager::findWorkspace(url);
        return it != m_workspaces.end() ? it->codeModel.get() : nullptr;
    }
};

tst_qmlls_qqmlcodemodel::tst_qmlls_qqmlcodemodel() : QQmlDataTest(QT_QQMLCODEMODEL_DATADIR) { }

void tst_qmlls_qqmlcodemodel::buildPathsForFileUrl_data()
{
    QTest::addColumn<QString>("pathFromIniFile");
    QTest::addColumn<QString>("pathFromCommandLine");
    QTest::addColumn<QString>("expectedPath");

    const QString path1 = u"/Users/helloWorld/build-myProject"_s;
    const QString path2 = u"/Users/helloWorld/build-custom"_s;
    const QString path3 = u"/Users/helloWorld/build-12345678"_s;

    QTest::addRow("justCommandLine") << QString() << path1 << path1;
    QTest::addRow("all3") << path1 << path3 << path3;

    QTest::addRow("commandLineOverridesEnvironmentVariable") << QString() << path3 << path3;
    QTest::addRow("commandLineOverridesIniFile") << path2 << path3 << path3;

    QTest::addRow("iniFile") << path1 << QString() << path1;

    // bug where qmlls allocates memory in an endless loop because of a folder called "_deps"
    QTest::addRow("endlessLoop") << QString() << testFile(u"buildfolderwithdeps"_s)
                                 << testFile(u"buildfolderwithdeps"_s);
}

void tst_qmlls_qqmlcodemodel::buildPathsForFileUrl()
{
    QFETCH(QString, pathFromIniFile);
    QFETCH(QString, pathFromCommandLine);
    QFETCH(QString, expectedPath);

    QQmlToolingSharedSettings settings(u"qmlls"_s);

    QTemporaryDir temporaryDirectory;
    QVERIFY(temporaryDirectory.isValid());
    {
        QFile settingsFile(temporaryDirectory.filePath(".qmlls.ini"));
        QVERIFY(settingsFile.open(QFile::WriteOnly | QFile::Text));
        settingsFile.write("[General]\n");
        if (!pathFromIniFile.isEmpty())
            settingsFile.write("buildDir=\"%1\""_L1.arg(pathFromIniFile).toUtf8());
    }

    TestCodeModelManager model(&settings);
    model.addRootUrls({ QUrl::fromLocalFile(temporaryDirectory.path()).toEncoded() });

    if (!pathFromCommandLine.isEmpty())
        model.setBuildPathsForRootUrl(QByteArray(), QStringList{ pathFromCommandLine });

    const QByteArray nonExistentUrl =
            QUrl::fromLocalFile(temporaryDirectory.filePath(u"file.qml"_s)).toEncoded();

    QStringList result = model.buildPathsForFileUrl(nonExistentUrl);
    QCOMPARE_EQ(result, QStringList{ expectedPath });

    result = model.findCodeModelForFile(nonExistentUrl)->buildPathsForOpenedFiles();
    QCOMPARE_EQ(result, QStringList{ expectedPath });
}

void tst_qmlls_qqmlcodemodel::findFilePathsFromFileNames_data()
{
    QTest::addColumn<QStringList>("fileNames");
    QTest::addColumn<QStringList>("expectedPaths");
    QTest::addColumn<QSet<QString>>("missingFiles");
    QTest::addColumn<QSet<QString>>("alreadyWatchedFiles");

    const QString folder = testFile("sourceFolder");
    const QString subfolder = testFile("sourceFolder/subSourceFolder/subsubSourceFolder");
    const QSet<QString> noMissingFiles;
    const QSet<QString> noAlreadyWatchedFiles;

    QTest::addRow("notExistingFile")
            << QStringList{ u"notExistingFile.h"_s } << QStringList{}
            << QSet<QString>{ u"notExistingFile.h"_s } << noAlreadyWatchedFiles;

    QTest::addRow("myqmlelement") << QStringList{ u"myqmlelement.h"_s }
                                  << QStringList{ folder + u"/myqmlelement.h"_s,
                                                  subfolder + u"/myqmlelement.h"_s }
                                  << noMissingFiles << noAlreadyWatchedFiles;

    QTest::addRow("myqmlelementAlreadyWatched")
            << QStringList{ u"myqmlelement.h"_s } << QStringList{ folder + u"/myqmlelement.h"_s }
            << noMissingFiles << QSet<QString>{ subfolder + u"/myqmlelement.h"_s };

    QTest::addRow("myqmlelement2") << QStringList{ u"myqmlelement2.hpp"_s }
                                   << QStringList{ folder + u"/myqmlelement2.hpp"_s }
                                   << noMissingFiles << noAlreadyWatchedFiles;

    QTest::addRow("anotherqmlelement") << QStringList{ u"anotherqmlelement.cpp"_s }
                                       << QStringList{ subfolder + u"/anotherqmlelement.cpp"_s }
                                       << noMissingFiles << noAlreadyWatchedFiles;
}

void tst_qmlls_qqmlcodemodel::findFilePathsFromFileNames()
{
    QFETCH(QStringList, fileNames);
    QFETCH(QStringList, expectedPaths);
    QFETCH(QSet<QString>, missingFiles);
    QFETCH(QSet<QString>, alreadyWatchedFiles);

    QmlLsp::QQmlCodeModel model(testFileUrl(u"sourceFolder"_s).toEncoded());

    auto result = model.findFilePathsFromFileNames(fileNames, alreadyWatchedFiles);

    // the order only is required for the QCOMPARE
    std::sort(result.begin(), result.end());
    std::sort(expectedPaths.begin(), expectedPaths.end());

    QCOMPARE(result, expectedPaths);
    QCOMPARE(model.ignoreForWatching(), missingFiles);
}

using namespace QQmlJS::Dom;

void tst_qmlls_qqmlcodemodel::resourceFiles()
{
    QmlLsp::QQmlCodeModelManager manager;
    const QByteArray rootUrl = testFileUrl("somePath"_L1).toEncoded();

    QTemporaryDir buildDir;
    QVERIFY(buildDir.isValid());
    QDir(buildDir.path()).mkdir(".qt"_L1);
    QmlLsp::QQmllsBuildInformation buildIni;
    buildIni.addModuleSetting(QmlLsp::ModuleSetting{
            testFile("somePath"), {}, { testFile("FolderWithResources/Good.qrc") } });
    buildIni.writeQmllsBuildIniContent(buildDir.filePath(".qt/.qmlls.build.ini"_L1));

    manager.addRootUrls({ rootUrl });

    QCOMPARE_EQ(manager.defaultResourceFiles(), QStringList{});
    manager.setBuildPathsForRootUrl(rootUrl, { buildDir.path() });

    QCOMPARE_EQ(manager.defaultResourceFiles(), QStringList{});
    QCOMPARE_EQ(manager.resourceFilesForFileUrl(rootUrl),
                QStringList{ testFile("FolderWithResources/Good.qrc"_L1) });

    manager.setBuildPathsForRootUrl({}, { testFile("FolderWithResources") });
    QStringList defaultResourceFiles = manager.defaultResourceFiles();
    std::sort(defaultResourceFiles.begin(), defaultResourceFiles.end());
    const QStringList expectedDefaultResourceFiles{
        testFile("FolderWithResources/Bad.qrc"_L1),
        testFile("FolderWithResources/Good.qrc"_L1),
    };
    QCOMPARE_EQ(defaultResourceFiles, expectedDefaultResourceFiles);
    QCOMPARE_EQ(manager.resourceFilesForFileUrl(rootUrl),
                QStringList{ testFile("FolderWithResources/Good.qrc"_L1) });
}

void tst_qmlls_qqmlcodemodel::resourceFilesFallback()
{
    QmlLsp::QQmlCodeModelManager manager;
    const QByteArray rootUrl = testFileUrl("somePath").toEncoded();
    manager.addRootUrls({ rootUrl });

    QCOMPARE_EQ(manager.defaultResourceFiles(), QStringList{});
    manager.setBuildPathsForRootUrl(rootUrl, { testFile("FolderWithResources") });

    QCOMPARE_EQ(manager.defaultResourceFiles(), QStringList{});

    const QStringList expectedDefaultResourceFiles{
        testFile("FolderWithResources/Bad.qrc"),
        testFile("FolderWithResources/Good.qrc"),
    };
    QStringList defaultResourceFiles = manager.resourceFilesForFileUrl(rootUrl);
    std::sort(defaultResourceFiles.begin(), defaultResourceFiles.end());
    QCOMPARE_EQ(defaultResourceFiles, expectedDefaultResourceFiles);
}

void tst_qmlls_qqmlcodemodel::fileNamesToWatch()
{
    DomItem qmlFile;

    auto envPtr = DomEnvironment::create(QStringList(),
                                         DomEnvironment::Option::SingleThreaded
                                                 | DomEnvironment::Option::NoDependencies,
                                         Extended);

    envPtr->loadFile(FileToLoad::fromFileSystem(envPtr, testFile("MyCppModule/Main.qml")),
                     [&qmlFile](Path, const DomItem &, const DomItem &newIt) {
                         qmlFile = newIt.fileObject();
                     });
    envPtr->loadPendingDependencies();

    const auto fileNames = QmlLsp::QQmlCodeModel::fileNamesToWatch(qmlFile);

    // fileNames also contains some builtins it seems, like:
    // QSet("qqmlcomponentattached_p.h", "qqmlcomponent.h", "qobject.h", "qqmllist.h",
    // "helloworld.h", "qqmlengine_p.h")
    QVERIFY(fileNames.contains(u"helloworld.h"_s));

    // test for no duplicates
    QVERIFY(std::is_sorted(fileNames.begin(), fileNames.end()));
    QVERIFY(std::adjacent_find(fileNames.begin(), fileNames.end()) == fileNames.end());

    // should not contain any empty strings
    QVERIFY(!fileNames.contains(QString()));
}

QString tst_qmlls_qqmlcodemodel::readFile(const QString &filename) const
{
    QFile f(testFile(filename));
    if (!f.open(QFile::ReadOnly)) {
        QTest::qFail("Can't read test file", __FILE__, __LINE__);
        return {};
    }
    return f.readAll();
}

void tst_qmlls_qqmlcodemodel::openFiles_data()
{
    QTest::addColumn<bool>("cmakeEnabled");

    QTest::addRow("withCMake") << true;
    QTest::addRow("withoutCMake") << false;
}

void tst_qmlls_qqmlcodemodel::openFiles()
{
    QFETCH(bool, cmakeEnabled);

    QmlLsp::QQmlCodeModel model;
    model.setImportPaths(QLibraryInfo::paths(QLibraryInfo::QmlImportsPath));

    // disabling CMake should not make the test fail!
    if (!cmakeEnabled)
        model.disableCMakeCalls();

    const QByteArray fileAUrl = testFileUrl(u"FileA.qml"_s).toEncoded();
    const QString fileAPath = testFile(u"FileA.qml"_s);

    // open file A
    model.newOpenFile(fileAUrl, 0, readFile(u"FileA.qml"_s));

    QTRY_VERIFY_WITH_TIMEOUT(model.validEnv().field(Fields::qmlFileWithPath).key(fileAPath), 3000);

    {
        const DomItem fileAComponents = model.validEnv()
                                                .field(Fields::qmlFileWithPath)
                                                .key(fileAPath)
                                                .field(Fields::currentItem)
                                                .field(Fields::components);
        // if there is no component then the lazy qml file was not loaded correctly.
        QCOMPARE(fileAComponents.size(), 1);
    }

    QSignalSpy spy(&model, &QmlLsp::QQmlCodeModel::openUpdateThreadFinished);
    model.newOpenFile(fileAUrl, 1, readFile(u"FileA2.qml"_s));
    // wait for QQmlCodeModel to finish loading
    QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 3000);

    {
        const DomItem fileAComponents = model.validEnv()
                                                .field(Fields::qmlFileWithPath)
                                                .key(fileAPath)
                                                .field(Fields::currentItem)
                                                .field(Fields::components);
        // if there is no component then the lazy qml file was not loaded correctly.
        QCOMPARE(fileAComponents.size(), 1);

        // also check if the property is there
        const DomItem properties = fileAComponents.key(QString())
                                           .index(0)
                                           .field(Fields::objects)
                                           .index(0)
                                           .field(Fields::propertyDefs);
        QVERIFY(properties);
        QVERIFY(properties.key(u"helloProperty"_s));
    }
}

void tst_qmlls_qqmlcodemodel::importPathViaSettings()
{
    // prepare the qmlls.ini file
    QFile settingsTemplate(testFile(u"importPathFromSettings/.qmlls.ini.template"_s));
    QVERIFY(settingsTemplate.open(QFile::ReadOnly | QFile::Text));
    const QString data = QString::fromUtf8(settingsTemplate.readAll())
                                 .arg(QStringList{
                                         QDir::cleanPath(testFile(u"."_s)),
                                         testFile(u"SomeFolder"_s),
                                         QLibraryInfo::path(QLibraryInfo::QmlImportsPath),
                                 }
                                              .join(QDir::listSeparator()));

    QFile settingsFile(testFile(u"importPathFromSettings/.qmlls.ini"_s));
    auto guard = qScopeGuard([&settingsFile]() { settingsFile.remove(); });

    QVERIFY(settingsFile.open(QFile::WriteOnly | QFile::Truncate | QFile::Text));
    settingsFile.write(data.toUtf8());
    settingsFile.flush();

    // actually test the qqmlcodemodel
    QQmlToolingSharedSettings settings(u"qmlls"_s);
    settings.addOption(u"importPaths"_s);
    QmlLsp::QQmlCodeModel model(QByteArray(), nullptr, &settings);

    const QString someFile = u"importPathFromSettings/SomeFile.qml"_s;
    const QByteArray fileUrl = testFileUrl(someFile).toEncoded();
    const QString filePath = testFile(someFile);

    model.newOpenFile(fileUrl, 0, readFile(someFile));

    QTRY_VERIFY_WITH_TIMEOUT(model.validEnv().field(Fields::qmlFileWithPath).key(filePath), 3000);

    {
        const DomItem fileAComponents = model.validEnv()
                                                .field(Fields::qmlFileWithPath)
                                                .key(filePath)
                                                .field(Fields::currentItem)
                                                .field(Fields::components);
        // if there is no component then the import path was not used by qqmlcodemodel ?
        QCOMPARE(fileAComponents.size(), 1);
    }
}

static void reloadLotsOfFileMethod()
{
    QmlLsp::QQmlCodeModel model;
    model.setImportPaths(QLibraryInfo::paths(QLibraryInfo::QmlImportsPath));

    QTemporaryDir folder;
    QVERIFY(folder.isValid());

    const QByteArray content = "import QtQuick\n\nItem {}";
    QStringList fileNames;
    for (int i = 0; i < 5; ++i) {
        const QString currentFileName = folder.filePath(QString::number(i).append(u".qml"));
        fileNames.append(currentFileName);

        QFile file(currentFileName);
        QVERIFY(file.open(QFile::WriteOnly));
        file.write(content);
    }

    // open all files
    for (const QString &fileName : fileNames)
        model.newOpenFile(QUrl::fromLocalFile(fileName).toEncoded(), 0, content);

    // wait for them to load
    QTRY_COMPARE_WITH_TIMEOUT(model.validEnv().field(Fields::qmlFileWithPath).keys().size(),
                              fileNames.size(), 3000);

    auto areAllFilesPopulated = [&model]() {
        for (const QString &key : model.validEnv().field(Fields::qmlFileWithPath).keys()) {
            if (model.validEnv()
                        .field(Fields::qmlFileWithPath)
                        .key(key)
                        .field(Fields::currentItem)
                        .field(Fields::components)
                        .size()
                != 1) {
                return false;
            }
        }
        return true;
    };

    // populate all files
    QTRY_VERIFY_WITH_TIMEOUT(areAllFilesPopulated(), 1000);

    // modify all files on disk
    for (const QString &fileName : fileNames) {
        QFile file(fileName);
        QVERIFY(file.open(QFile::WriteOnly | QFile::Append));
        file.write("\n\n");
    }

    QSignalSpy spy(&model, &QmlLsp::QQmlCodeModel::openUpdateThreadFinished);
    // update one file
    model.newOpenFile(QUrl::fromLocalFile(fileNames.front()).toEncoded(), 1, content + "\n\n");

    // wait for QQmlCodeModel to finish loading before leaving the scope
    QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 3000);
}

void tst_qmlls_qqmlcodemodel::reloadLotsOfFiles()
{
    QScopedPointer<QThread> thread(QThread::create([]() { reloadLotsOfFileMethod(); }));

    // should not stack-overflow despite the small stack size to make sure QML files are loaded
    // correctly and not recursively
    thread->setStackSize(1 << 20);
    thread->start();
    thread->wait();
}

void tst_qmlls_qqmlcodemodel::buildPaths_data()
{
    QTest::addColumn<QList<QByteArray>>("roots");
    QTest::addColumn<QByteArray>("root");
    QTest::addColumn<QString>("buildPath");
    QTest::addColumn<QByteArray>("file");

    const QList<QByteArray> roots = {
        testFileUrl("twoWorkspaces/WorkSpaceA"_L1).toEncoded(),
        testFileUrl("twoWorkspaces/WorkSpaceB"_L1).toEncoded(),
    };

    QTest::addRow("WorkspaceA")
            << roots << testFileUrl("twoWorkspaces/WorkSpaceA"_L1).toEncoded()
            << testFile("twoWorkspaces/ImportPathA"_L1)
            << testFileUrl("twoWorkspaces/WorkSpaceA/UseImportPathA.qml"_L1).toEncoded();
    QTest::addRow("WorkspaceB")
            << roots << testFileUrl("twoWorkspaces/WorkSpaceB"_L1).toEncoded()
            << testFile("twoWorkspaces/ImportPathB"_L1)
            << testFileUrl("twoWorkspaces/WorkSpaceB/UseImportPathB.qml"_L1).toEncoded();
}

void tst_qmlls_qqmlcodemodel::buildPaths()
{
    QFETCH(QList<QByteArray>, roots);
    QFETCH(QByteArray, root);
    QFETCH(QString, buildPath);
    QFETCH(QByteArray, file);

    QmlLsp::QQmlCodeModelManager manager;
    manager.addRootUrls(roots);
    QVERIFY(!manager.buildPathsForFileUrl(root).contains(buildPath));
    manager.setBuildPathsForRootUrl(root, { buildPath });
    QVERIFY(manager.buildPathsForFileUrl(root).contains(buildPath));
    QVERIFY(manager.buildPathsForFileUrl(file).contains(buildPath));
}

void tst_qmlls_qqmlcodemodel::defaultWorkspace()
{
    const QByteArray fileAUrl = testFileUrl(u"FileA.qml"_s).toEncoded();
    const QByteArray unrelatedRoot = testFileUrl("twoWorkspaces/WorkSpaceA"_L1).toEncoded();

    TestCodeModelManager manager;
    manager.setImportPaths(QLibraryInfo::paths(QLibraryInfo::QmlImportsPath));
    manager.addRootUrls({ unrelatedRoot });
    manager.newOpenFile(fileAUrl, 0, readFile(u"FileA.qml"_s));
    QTRY_VERIFY_WITH_TIMEOUT(manager.snapshotByUrl(fileAUrl).validDoc, 3000);

    // make sure that fileA was not saved in the unrelated root
    QCOMPARE_NE(manager.findCodeModelForFile(fileAUrl),
                manager.findCodeModelForFile(unrelatedRoot));
    const QByteArray defaultWS;
    QCOMPARE(manager.findCodeModelForFile(fileAUrl), manager.findCodeModelForFile(defaultWS));
}

void tst_qmlls_qqmlcodemodel::closeWorkspace()
{
    const QByteArray defaultRoot;
    const QByteArray root = testFileUrl("twoWorkspaces/WorkSpaceA"_L1).toEncoded();
    const QByteArray file1 =
            testFileUrl("twoWorkspaces/WorkSpaceA/UseImportPathA.qml"_L1).toEncoded();
    const QByteArray file2 =
            testFileUrl("twoWorkspaces/WorkSpaceA/UseImportPathA2.qml"_L1).toEncoded();
    const QString fileContent = readFile("twoWorkspaces/WorkSpaceA/UseImportPathA.qml"_L1);

    TestCodeModelManager manager;
    manager.setImportPaths({ testFile("twoWorkspaces/ImportPathA"_L1),
                             QLibraryInfo::path(QLibraryInfo::QmlImportsPath) });
    manager.addRootUrls({ root });
    manager.newOpenFile(file1, 0, fileContent);
    QTRY_VERIFY_WITH_TIMEOUT(manager.snapshotByUrl(file1).validDoc, 3000);

    QCOMPARE(manager.findCodeModelForFile(file1), manager.findCodeModel(root));

    manager.removeRootUrls({ root });
    QCOMPARE(manager.findCodeModelForFile(file1), manager.findCodeModel(root));
    QCOMPARE_NE(manager.findCodeModelForFile(file1), manager.findCodeModel(defaultRoot));
    QVERIFY(manager.snapshotByUrl(file1).validDoc);

    manager.newOpenFile(file2, 0, fileContent);
    QTRY_VERIFY_WITH_TIMEOUT(manager.snapshotByUrl(file2).validDoc, 3000);
    // new files should not open in closed workspaces
    QCOMPARE(manager.findCodeModelForFile(file2), manager.findCodeModel(defaultRoot));

    manager.closeOpenFile(file1);
    QCOMPARE(manager.findCodeModelForFile(file1), manager.findCodeModel(defaultRoot));
    QVERIFY(!manager.snapshotByUrl(file1).validDoc);
}

void tst_qmlls_qqmlcodemodel::rootUrls()
{
    const QByteArray rootA = testFileUrl("twoWorkspaces/WorkSpaceA"_L1).toEncoded();
    const QByteArray rootB = testFileUrl("twoWorkspaces/WorkSpaceB"_L1).toEncoded();
    const QByteArray defaultWS;

    TestCodeModelManager manager;
    QCOMPARE(manager.rootUrls().size(), 1);
    manager.addRootUrls({ rootA, rootB });
    QCOMPARE(manager.rootUrls().size(), 3);

    QCOMPARE_NE(manager.findCodeModelForFile(rootA), defaultWS);
    QCOMPARE_NE(manager.findCodeModelForFile(rootB), defaultWS);
    QCOMPARE_NE(manager.findCodeModelForFile(rootA), manager.findCodeModelForFile(rootB));
}

void tst_qmlls_qqmlcodemodel::addingWorkspaces()
{
    const QByteArray outerWorkspace = testFileUrl("twoWorkspaces/"_L1).toEncoded();
    const QByteArray innerWorkspace = testFileUrl("twoWorkspaces/WorkSpaceA/"_L1).toEncoded();

    const QByteArray file1 =
            testFileUrl("twoWorkspaces/WorkSpaceA/UseImportPathA.qml"_L1).toEncoded();
    const QByteArray file2 =
            testFileUrl("twoWorkspaces/WorkSpaceA/UseImportPathA2.qml"_L1).toEncoded();

    const QString fileContent = readFile("twoWorkspaces/WorkSpaceA/UseImportPathA.qml"_L1);

    TestCodeModelManager manager;
    manager.setImportPaths({ testFile("twoWorkspaces/ImportPathA"_L1),
                             QLibraryInfo::path(QLibraryInfo::QmlImportsPath) });
    manager.addRootUrls({ outerWorkspace });

    manager.newOpenFile(file1, 0, fileContent);
    QTRY_VERIFY_WITH_TIMEOUT(manager.snapshotByUrl(file1).validDoc, 3000);

    QCOMPARE(manager.findCodeModelForFile(file1), manager.findCodeModelForFile(outerWorkspace));

    manager.addRootUrls({ innerWorkspace });
    // fileA should not use the new root because its open!
    QCOMPARE(manager.findCodeModelForFile(file1), manager.findCodeModelForFile(outerWorkspace));
    // new files like fileB should use the new WS
    QCOMPARE(manager.findCodeModelForFile(file2), manager.findCodeModelForFile(innerWorkspace));

    manager.newOpenFile(file2, 0, fileContent);
    QTRY_VERIFY_WITH_TIMEOUT(manager.snapshotByUrl(file2).validDoc, 3000);
    QCOMPARE(manager.findCodeModelForFile(file2), manager.findCodeModelForFile(innerWorkspace));

    manager.closeOpenFile(file1);
    // fileA was closed, so it can now be reopened in the new outerRoot workspace
    QCOMPARE(manager.findCodeModelForFile(file1), manager.findCodeModelForFile(innerWorkspace));
    manager.newOpenFile(file1, 0, fileContent);
    QTRY_VERIFY_WITH_TIMEOUT(manager.snapshotByUrl(file1).validDoc, 3000);
    QCOMPARE(manager.findCodeModelForFile(file1), manager.findCodeModelForFile(innerWorkspace));

    // closing outerRoot should not affect opened files
    manager.removeRootUrls({ innerWorkspace });
    QCOMPARE(manager.findCodeModelForFile(file1), manager.findCodeModel(innerWorkspace));
    QCOMPARE(manager.findCodeModelForFile(file2), manager.findCodeModel(innerWorkspace));
}

void tst_qmlls_qqmlcodemodel::newWorkspace()
{
    const QByteArray rootA = testFileUrl("twoWorkspaces/WorkSpaceA"_L1).toEncoded();
    const QString buildPathA = testFile("twoWorkspaces/ImportPathA"_L1);
    const QString docPathA = testFile("twoWorkspaces/dummydocpaththatdoesnotexist"_L1);

    TestCodeModelManager manager;

    // set properties before the WS is created (qmlls reads those values via commandline or
    // environment variable, so they should be valid for all code models)
    const QStringList importPaths{ buildPathA, QLibraryInfo::path(QLibraryInfo::QmlImportsPath) };
    manager.setImportPaths(importPaths);
    manager.setDocumentationRootPath(docPathA);

    manager.addRootUrls({ rootA });

    // make sure that the new WS contains the properties set before its existence
    auto *codeModel = manager.findCodeModelForFile(rootA);
    QCOMPARE(codeModel->importPaths(), importPaths);
    QCOMPARE(codeModel->documentationRootPath(), { docPathA });
}

void tst_qmlls_qqmlcodemodel::duplicateWorkspace()
{
    const QByteArray rootA = testFileUrl("twoWorkspaces/WorkSpaceA"_L1).toEncoded();

    TestCodeModelManager manager;
    manager.addRootUrls({ rootA });
    const QList<QByteArray> expectedRoots{ {}, rootA };
    QCOMPARE(manager.rootUrls(), expectedRoots);
    auto *codeModel = manager.findCodeModelForFile(rootA);

    // don't create or recreate ws that already exists
    manager.addRootUrls({ rootA, rootA, rootA });
    QCOMPARE(manager.rootUrls(), expectedRoots);
    QCOMPARE(manager.findCodeModelForFile(rootA), codeModel);
}

void tst_qmlls_qqmlcodemodel::withQmllsBuildIni()
{
    const QByteArray rootAUrl = testFileUrl("twoWorkspaces/WorkSpaceA/"_L1).toEncoded();
    const QByteArray rootBUrl = testFileUrl("twoWorkspaces/WorkSpaceB/"_L1).toEncoded();
    const QString rootA = testFile("twoWorkspaces/WorkSpaceA/"_L1);
    const QString rootB = testFile("twoWorkspaces/WorkSpaceB/"_L1);
    const QString importPathA = testFile("twoWorkspaces/ImportPathA/"_L1);
    const QString importPathB = testFile("twoWorkspaces/ImportPathB/"_L1);
    const QString defaultImportPath = QLibraryInfo::path(QLibraryInfo::QmlImportsPath);
    const QStringList expectedImportPathA{ importPathA, defaultImportPath };
    const QStringList expectedImportPathB{ importPathB, defaultImportPath };
    const QByteArray fileAUrl =
            testFileUrl("twoWorkspaces/WorkSpaceA/UseImportPathA.qml"_L1).toEncoded();
    const QByteArray fileBUrl =
            testFileUrl("twoWorkspaces/WorkSpaceB/UseImportPathB.qml"_L1).toEncoded();
    const QString fileA = testFile("twoWorkspaces/WorkSpaceA/UseImportPathA.qml"_L1);

    QTemporaryDir buildPathA;
    QVERIFY(buildPathA.isValid());

    const QString resourceFileA = buildPathA.filePath("resourceA.qrc");
    const QString resourceFileB = buildPathA.filePath("resourceB.qrc");

    QDir(buildPathA.path()).mkdir(".qt"_L1);
    QmlLsp::QQmllsBuildInformation buildIni;
    buildIni.addModuleSetting(
            QmlLsp::ModuleSetting{ rootA, { importPathA, defaultImportPath }, { resourceFileA } });
    buildIni.addModuleSetting(
            QmlLsp::ModuleSetting{ rootB, { importPathB, defaultImportPath }, { resourceFileB } });
    buildIni.writeQmllsBuildIniContent(buildPathA.filePath(".qt/.qmlls.build.ini"_L1));

    TestCodeModelManager manager;
    manager.addRootUrls({ rootAUrl });

    QCOMPARE_NE(manager.findCodeModelForFile(rootAUrl)->importPaths(), expectedImportPathA);
    QCOMPARE_NE(manager.findCodeModelForFile(rootBUrl)->importPaths(), expectedImportPathB);

    QCOMPARE(manager.resourceFilesForFileUrl(rootAUrl), {});
    QCOMPARE(manager.resourceFilesForFileUrl(rootBUrl), {});

    manager.setBuildPathsForRootUrl(rootAUrl, { buildPathA.path() });
    // import path was updated using .qmlls.build.ini on existing WS
    QCOMPARE(manager.findCodeModelForFile(rootAUrl)->importPaths(), expectedImportPathA);
    QCOMPARE(manager.findCodeModelForFile(rootAUrl)->importPathsForUrl(rootAUrl),
             expectedImportPathA);
    QCOMPARE(manager.resourceFilesForFileUrl(rootAUrl), { resourceFileA });

    manager.addRootUrls({ rootBUrl });
    // import path was set using .qmlls.build.ini on newly created WS
    QCOMPARE(manager.findCodeModelForFile(rootBUrl)->importPaths(), expectedImportPathB);
    QCOMPARE(manager.findCodeModelForFile(rootBUrl)->importPathsForUrl(rootBUrl),
             expectedImportPathB);
    QCOMPARE(manager.resourceFilesForFileUrl(rootBUrl), { resourceFileB });

    manager.newOpenFile(fileAUrl, 0, readFile("twoWorkspaces/WorkSpaceA/UseImportPathA.qml"_L1));
    manager.newOpenFile(fileBUrl, 0, readFile("twoWorkspaces/WorkSpaceB/UseImportPathB.qml"_L1));

    for (const auto &fileUrl : { fileAUrl, fileBUrl })
        QTRY_VERIFY_WITH_TIMEOUT(manager.snapshotByUrl(fileUrl).validDoc, 3000);

    DomItem domItemA = manager.snapshotByUrl(fileAUrl).validDoc;
    DomItem domItemB = manager.snapshotByUrl(fileBUrl).validDoc;

    // sanity check
    QCOMPARE_NE(domItemA, domItemB);

    auto loadedQmldir = [](const DomItem &item) {
        return item[Fields::components]
                .key(QString())
                .index(0)[Fields::objects]
                .index(0)[Fields::prototypes]
                .index(0)[Fields::get][Fields::uri]
                .value()
                .toString();
    };
    QCOMPARE(loadedQmldir(domItemA), "\"%1\""_L1.arg(importPathA + "MyModule/qmldir"_L1));
    QCOMPARE(loadedQmldir(domItemB), "\"%1\""_L1.arg(importPathB + "MyModule/qmldir"_L1));
}

void tst_qmlls_qqmlcodemodel::withQmllsBuildIniRelativeImportPath()
{
    const QString defaultImportPath = QLibraryInfo::path(QLibraryInfo::QmlImportsPath);

    QTemporaryDir buildPathA;
    QVERIFY(buildPathA.isValid());

    QDir(buildPathA.path()).mkdir(".qt"_L1);
    QmlLsp::QQmllsBuildInformation buildIni;
    buildIni.addModuleSetting(QmlLsp::ModuleSetting{ testFile("twoWorkspaces/WorkSpaceA/"_L1),
                                                     { "../ImportPathA"_L1, defaultImportPath },
                                                     {} });
    buildIni.writeQmllsBuildIniContent(buildPathA.filePath(".qt/.qmlls.build.ini"_L1));

    const QByteArray rootAUrl = testFileUrl("twoWorkspaces/WorkSpaceA/"_L1).toEncoded();
    TestCodeModelManager manager;
    manager.addRootUrls({ rootAUrl });
    manager.setBuildPathsForRootUrl(rootAUrl, { buildPathA.path() });

    const QString importPathA = testFile("twoWorkspaces/ImportPathA"_L1);
    const QStringList expectedImportPathA{ importPathA, defaultImportPath };
    QCOMPARE(manager.findCodeModelForFile(rootAUrl)->importPaths(), expectedImportPathA);
}

void tst_qmlls_qqmlcodemodel::withQmllsIniRelativeImportPath()
{
    const QString defaultImportPath = QLibraryInfo::path(QLibraryInfo::QmlImportsPath);

    QQmlToolingSharedSettings settings("qmlls");
    QmlLsp::QQmlCodeModel model(QByteArray{}, nullptr, &settings);
    const QString importPathA = testFile("twoWorkspaces"_L1);
    const QStringList expectedImportPathA = (model.importPaths() << importPathA);
    QCOMPARE(model.importPathsForUrl(testFileUrl("FolderWithQmllsIni/SomeType.qml").toEncoded()),
             expectedImportPathA);
}

void tst_qmlls_qqmlcodemodel::withQmllsBuildIniWithoutRootUrls()
{
    const QByteArray projectRootUrl = testFileUrl("twoWorkspaces/"_L1).toEncoded();
    const QByteArray rootAUrl = testFileUrl("twoWorkspaces/WorkSpaceA/"_L1).toEncoded();
    const QString importPath = testFile("twoWorkspaces/ImportPathA/"_L1);
    const QString defaultImportPath = QLibraryInfo::path(QLibraryInfo::QmlImportsPath);
    const QByteArray fileUrl =
            testFileUrl("twoWorkspaces/WorkSpaceA/UseImportPathA.qml"_L1).toEncoded();

    QTemporaryDir buildPathA;
    QVERIFY(buildPathA.isValid());

    QDir(buildPathA.path()).mkdir(".qt"_L1);

    QmlLsp::QQmllsBuildInformation buildIni;
    buildIni.addModuleSetting(QmlLsp::ModuleSetting{
            testFile("twoWorkspaces/WorkSpaceA/"_L1), { importPath, defaultImportPath }, {} });
    buildIni.writeQmllsBuildIniContent(buildPathA.filePath(".qt/.qmlls.build.ini"_L1));

    TestCodeModelManager manager;
    manager.addRootUrls({ projectRootUrl });
    manager.setBuildPathsForRootUrl(projectRootUrl, { buildPathA.path() });

    QCOMPARE(manager.rootUrls(), QList<QByteArray>{} << QByteArray{} << projectRootUrl);

    // opening fileA should create the rootA workspace, even if rootA was never added as root url to
    // manager.
    manager.newOpenFile(fileUrl, 0, readFile("twoWorkspaces/WorkSpaceA/UseImportPathA.qml"_L1));
    QTRY_VERIFY_WITH_TIMEOUT(manager.snapshotByUrl(fileUrl).validDoc, 3000);
    QCOMPARE(manager.rootUrls(), QList<QByteArray>{} << QByteArray{} << projectRootUrl << rootAUrl);

    QCOMPARE_NE(manager.findCodeModelForFile(fileUrl),
                manager.findCodeModelForFile(projectRootUrl));
    const QByteArray defaultRoot;
    QCOMPARE_NE(manager.findCodeModelForFile(fileUrl), manager.findCodeModelForFile(defaultRoot));
    QCOMPARE(manager.findCodeModelForFile(rootAUrl)->importPaths(),
             QStringList{} << importPath << defaultImportPath);

    DomItem domItemA = manager.snapshotByUrl(fileUrl).validDoc;

    auto loadedQmldir = [](const DomItem &item) {
        return item[Fields::components]
                .key(QString())
                .index(0)[Fields::objects]
                .index(0)[Fields::prototypes]
                .index(0)[Fields::get][Fields::uri]
                .value()
                .toString();
    };
    QCOMPARE(loadedQmldir(domItemA),
             "\"%1\""_L1.arg(testFile("twoWorkspaces/ImportPathA/MyModule/qmldir")));
}

void tst_qmlls_qqmlcodemodel::shortestRootUrlForFile()
{
    QmlLsp::QQmlCodeModelManager manager;
    const QByteArray rootA{ testFileUrl("rootA").toEncoded() };
    const QByteArray rootB{ testFileUrl("rootA/sub/subsub/rootB").toEncoded() };
    const QByteArray rootC{ testFileUrl("rootA/sub/rootC").toEncoded() };
    const QByteArray rootD{ testFileUrl("rootD").toEncoded() };

    manager.addRootUrls({ rootA, rootB, rootC, rootD });
    QCOMPARE(manager.shortestRootUrlForFile(testFileUrl("rootA/MyFile.qml").toEncoded()), rootA);
    QCOMPARE(manager.shortestRootUrlForFile(
                     testFileUrl("rootA/sub/subsub/rootB/MyFile.qml").toEncoded()),
             rootA);
    QCOMPARE(manager.shortestRootUrlForFile(testFileUrl("rootA/sub/rootC/MyFile.qml").toEncoded()),
             rootA);
    QCOMPARE(manager.shortestRootUrlForFile(testFileUrl("rootD/sub/rootC/MyFile.qml").toEncoded()),
             rootD);

    QmlLsp::QQmlCodeModelManager empty;
    QCOMPARE(empty.shortestRootUrlForFile(testFileUrl("rootD/sub/rootC/MyFile.qml").toEncoded()),
             QByteArray{});
}

const constexpr char *filenameKey = "QT_TST_QMLLS_QQMLCODEMODEL_WRITE_FILES";

void tst_qmlls_qqmlcodemodel::qprocessSchedulerProcess()
{
    if (!qEnvironmentVariableIsSet(filenameKey))
        return;

    QFile f(qEnvironmentVariable(filenameKey));
    QVERIFY(f.open(QFile::ReadWrite | QFile::Text | QFile::Append));

    f.write("X\n");
}

void tst_qmlls_qqmlcodemodel::qprocessSchedulerSlowProcess()
{
    if (!qEnvironmentVariableIsSet(filenameKey))
        return;

    auto writeFile = [](const QByteArray &suffix) {
        QFile f(qEnvironmentVariable(filenameKey + suffix));
        QVERIFY(f.open(QFile::ReadWrite | QFile::Text | QFile::Append));
        f.write("X\n");
    };

    writeFile("-begin");
    using namespace std::chrono_literals;
    QThread::currentThread()->sleep(3s);
    writeFile("-end");
}

void tst_qmlls_qqmlcodemodel::qprocessScheduler_data()
{
    QTest::addColumn<QStringList>("fileNames");

    QTest::addRow("empty") << QStringList{};
    QTest::addRow("two") << QStringList{ "a"_L1, "b"_L1 };
    QTest::addRow("five") << QStringList{ "a"_L1, "b"_L1, "c"_L1, "d"_L1, "e"_L1 };
}

void tst_qmlls_qqmlcodemodel::qprocessScheduler()
{
    using QmlLsp::QProcessScheduler;
    QFETCH(QStringList, fileNames);

    QTemporaryDir dir;
    QVERIFY(dir.isValid());

    QList<QProcessScheduler::Command> list;
    for (const QString &fileName : fileNames) {
        QProcessScheduler::Command command{
            QCoreApplication::applicationFilePath(),
            { "qprocessSchedulerProcess"_L1 },
        };
        command.customEnvironment.insert(filenameKey, dir.filePath(fileName));
        list.append(command);
    }

    QProcessScheduler scheduler;
    QSignalSpy spy(&scheduler, &QProcessScheduler::done);
    QCOMPARE(spy.count(), 0);

    scheduler.schedule(list, QByteArray());

    QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 5000);

    // verify that the processes really ran and wrote something to disk:
    for (const QString &fileName : fileNames)
        QVERIFY(QFile::exists(dir.filePath(fileName)));
}

static constexpr QLatin1String s_slowProcessName = "qprocessSchedulerSlowProcess"_L1;
static constexpr QLatin1String s_processName = "qprocessSchedulerProcess"_L1;

struct IdAndCommands
{
    QByteArray id;
    QList<QmlLsp::QProcessScheduler::Command> commands;
};

void tst_qmlls_qqmlcodemodel::qprocessSchedulerCancel_data()
{
    QTest::addColumn<QList<IdAndCommands>>("commands");
    QTest::addColumn<QByteArray>("cancellationId");

    QTest::addColumn<QList<QString>>("writtenFiles");
    QTest::addColumn<QList<QString>>("unwrittenFiles");

    auto createList = [](const QString &fileName, QLatin1String executableName) {
        QList<QmlLsp::QProcessScheduler::Command> list;
        QmlLsp::QProcessScheduler::Command command{
            QCoreApplication::applicationFilePath(),
            { executableName },
        };
        command.customEnvironment.insert(filenameKey, fileName);
        list.append(command);
        return list;
    };

    QTest::addRow("cancelCurrent") << QList<IdAndCommands>{ {
            { "id", createList("a"_L1, s_slowProcessName) },
            { "id2", createList("b"_L1, s_processName) },
            { "id3", createList("c"_L1, s_processName) },
    } } << "id"_ba << QStringList{ "b"_L1, "c"_L1 }
                                   << QStringList{ "a-end"_L1 };

    QTest::addRow("cancelNext") << QList<IdAndCommands>{ {
            { "id", createList("a"_L1, s_processName) },
            { "id2", createList("b"_L1, s_slowProcessName) },
            { "id3", createList("c"_L1, s_processName) },
    } } << "id2"_ba << QStringList{ "a"_L1, "c"_L1 }
                                << QStringList{ "b-end"_L1 };
}

void tst_qmlls_qqmlcodemodel::qprocessSchedulerCancel()
{
    QFETCH(QList<IdAndCommands>, commands);
    QFETCH(QByteArray, cancellationId);

    QFETCH(QList<QString>, writtenFiles);
    QFETCH(QList<QString>, unwrittenFiles);

    using QmlLsp::QProcessScheduler;

    QTemporaryDir dir;
    QVERIFY(dir.isValid());

    // patch CommandsById to add the temporary directory path
    for (auto it = commands.begin(), end = commands.end(); it != end; ++it) {
        for (auto it2 = it->commands.begin(), end2 = it->commands.end(); it2 != end2; ++it2) {
            it2->customEnvironment.insert(filenameKey,
                                          dir.filePath(it2->customEnvironment.value(filenameKey)));
        }
    }

    QProcessScheduler scheduler;
    QSignalSpy doneSpy(&scheduler, &QProcessScheduler::done);
    QSignalSpy cancelledSpy(&scheduler, &QProcessScheduler::cancelled);
    QSignalSpy startedSpy(&scheduler, &QProcessScheduler::started);
    QCOMPARE(doneSpy.count(), 0);

    for (const auto &idAndCommand : commands)
        scheduler.schedule(idAndCommand.commands, idAndCommand.id);

    scheduler.cancel(cancellationId);

    // note: the scheduled processes should run in another event loop iteration during QTRY_COMPARE_WITH_TIMEOUT
    QCOMPARE(doneSpy.count(), 0);
    QCOMPARE(startedSpy.count(), 0);

    QTRY_COMPARE_WITH_TIMEOUT(doneSpy.count() + cancelledSpy.count(), commands.size(), 9000);
    QCOMPARE(cancelledSpy.count(), 1);

    // verify that the processes really ran and wrote something to disk:
    for (const QString &file : writtenFiles)
        QVERIFY2(QFile::exists(dir.filePath(file)), qPrintable(file));
    for (const QString &file : unwrittenFiles)
        QVERIFY2(!QFile::exists(dir.filePath(file)), qPrintable(file));
}

using Hash = QHash<QByteArray, QStringList>;

void tst_qmlls_qqmlcodemodel::multipleQProcessScheduler_data()
{
    QTest::addColumn<Hash>("fileNamesById");

    QTest::addRow("empty") << Hash{};
    QTest::addRow("empty2") << Hash{
        { "url", {} },
    };
    QTest::addRow("duplicated") << Hash{
        { "url1", { "a"_L1, "b"_L1 } },
        { "url2", { "a"_L1, "b"_L1 } },
    };
    QTest::addRow("two") << Hash{
        { "url1", { "a"_L1, "b"_L1 } },
        { "url2", { "c"_L1, "d"_L1 } },
    };
    QTest::addRow("five") << Hash{
        { "url1", { "a"_L1, "b"_L1 } },
        { "url2", { "c"_L1, "d"_L1 } },
        { "url3", { "e"_L1, "f"_L1, "g"_L1 } },
        { "url1-duplicate1", { "a"_L1, "b"_L1 } }, // duplicates url 1, is empty
        { "url1and2-duplicate2",
          { "a"_L1, "b"_L1, "c"_L1, "d"_L1 } }, // duplicates url 1 and 2, is empty
        { "url5", { "h"_L1 } },
    };
    QTest::addRow("duplicate") << Hash{
        { "url1", { "a"_L1, "a"_L1, "a"_L1, "a"_L1, "a"_L1, "a"_L1, "a"_L1 } },
    };
}

void tst_qmlls_qqmlcodemodel::multipleQProcessScheduler()
{
    using QmlLsp::QProcessScheduler;
    QFETCH(Hash, fileNamesById);

    QTemporaryDir dir;
    QVERIFY(dir.isValid());

    QProcessScheduler scheduler;
    QSignalSpy spy(&scheduler, &QProcessScheduler::done);
    QCOMPARE(spy.count(), 0);

    for (const auto &[id, fileNames] : fileNamesById.asKeyValueRange()) {
        QList<QProcessScheduler::Command> list;
        for (const QString &fileName : fileNames) {
            QProcessScheduler::Command command{
                QCoreApplication::applicationFilePath(),
                { "qprocessSchedulerProcess"_L1 },
            };
            command.customEnvironment.insert(filenameKey, dir.filePath(fileName));
            list.append(command);
        }

        scheduler.schedule(list, id);
    }

    QTRY_COMPARE_WITH_TIMEOUT(spy.count(), fileNamesById.size(), 5000);

    for (const auto &[id, fileNames] : fileNamesById.asKeyValueRange()) {
        for (const QString &fileName : fileNames)
            QVERIFY(QFile::exists(dir.filePath(fileName)));
    }

    // ensure that duplicates were filtered away and that the file was appended to only once
    if (QTest::currentTestFunction() == "duplicate"_L1) {
        QFile file(dir.filePath("a"_L1));
        QVERIFY(file.open(QFile::ReadOnly | QFile::Text));
        QCOMPARE(file.readAll().count("X"_L1), 1);
    }
}

void tst_qmlls_qqmlcodemodel::reloadQmllsBuildIniV2AfterBuild()
{
    QmlLsp::QQmlCodeModelManager manager;
    const QByteArray rootUrl{ testFileUrl("rootA").toEncoded() };
    const QByteArray fallbackUrl;

    QTemporaryDir temporaryDir;
    QVERIFY(temporaryDir.isValid());
    static constexpr QLatin1String fallbackImportPath = "/path/to/importPath"_L1;
    manager.setImportPaths({ fallbackImportPath });
    manager.addRootUrls({ rootUrl });

    manager.setBuildPathsForRootUrl(rootUrl, { temporaryDir.path() });
    QCOMPARE(manager.importPathsForUrl(rootUrl), QStringList{ fallbackImportPath });
    QCOMPARE(manager.importPathsForUrl(fallbackUrl), QStringList{ fallbackImportPath });

    QDir dir(temporaryDir.path());

    dir.mkdir(".qt"_L1);
    QmlLsp::QQmllsBuildInformation buildIni;
    buildIni.addModuleSetting(QmlLsp::ModuleSetting{ testFile("rootA"_L1), { "test"_L1 }, {} });
    buildIni.writeQmllsBuildIniContent(temporaryDir.filePath(".qt/.qmlls.build.ini"_L1));

    manager.onBuildFinished(rootUrl);

    const QStringList expectedImportPathsForRoot{ testFile("test"_L1), fallbackImportPath };
    QCOMPARE(manager.importPathsForUrl(rootUrl), expectedImportPathsForRoot);
    QCOMPARE(manager.importPathsForUrl(fallbackUrl), QStringList{ fallbackImportPath });

    QmlLsp::QQmllsBuildInformation buildIni2;
    buildIni2.addModuleSetting(QmlLsp::ModuleSetting{ testFile("rootA"_L1), { "test2"_L1 }, {} });
    buildIni2.writeQmllsBuildIniContent(temporaryDir.filePath(".qt/.qmlls.build.ini"_L1));

    manager.onBuildFinished(rootUrl);

    const QStringList expectedImportPathsForRoot2{ testFile("test2"_L1), fallbackImportPath };
    QCOMPARE(manager.importPathsForUrl(rootUrl), expectedImportPathsForRoot2);
    QCOMPARE(manager.importPathsForUrl(fallbackUrl), QStringList{ fallbackImportPath });
}

void tst_qmlls_qqmlcodemodel::reloadQmllsBuildIniV1AfterBuild()
{
    QmlLsp::QQmlCodeModelManager manager;
    const QByteArray rootUrl{ testFileUrl("rootA").toEncoded() };
    const QByteArray fallbackUrl;

    static constexpr QLatin1String fallbackImportPath = "/path/to/importPath"_L1;
    manager.setImportPaths({ fallbackImportPath });

    QTemporaryDir temporaryDir;
    QVERIFY(temporaryDir.isValid());
    manager.addRootUrls({ rootUrl });

    manager.setBuildPathsForRootUrl(rootUrl, { temporaryDir.path() });
    QCOMPARE(manager.importPathsForUrl(rootUrl), QStringList{ fallbackImportPath });

    QDir dir(temporaryDir.path());
    dir.mkdir(".qt"_L1);

    {
        QFile file(temporaryDir.filePath(".qt/.qmlls.build.ini"_L1));
        QVERIFY(file.open(QFile::WriteOnly | QFile::Text));
        file.write("[General]\n[%1]\nimportPaths=\"test\"\n"_L1
                           .arg(testFile("rootA").replace("/"_L1, "<SLASH>"_L1))
                           .toUtf8());
    }

    manager.onBuildFinished(rootUrl);

    const QStringList expectedImportPathsForRoot =
            QStringList{ testFile("test"_L1), fallbackImportPath };
    QCOMPARE(manager.importPathsForUrl(rootUrl), expectedImportPathsForRoot);
    QCOMPARE(manager.importPathsForUrl(fallbackUrl), QStringList{ fallbackImportPath });

    {
        QFile file(temporaryDir.filePath(".qt/.qmlls.build.ini"_L1));
        QVERIFY(file.open(QFile::WriteOnly | QFile::Truncate | QFile::Text));
        file.write("[General]\n[%1]\nimportPaths=\"test2\"\n"_L1
                           .arg(testFile("rootA").replace("/"_L1, "<SLASH>"_L1))
                           .toUtf8());
    }

    manager.onBuildFinished(rootUrl);

    const QStringList expectedImportPathsForRoot2 =
            QStringList{ testFile("test2"_L1), fallbackImportPath };
    QCOMPARE(manager.importPathsForUrl(rootUrl), expectedImportPathsForRoot2);
    QCOMPARE(manager.importPathsForUrl(fallbackUrl), QStringList{ fallbackImportPath });
}

void tst_qmlls_qqmlcodemodel::brokenQmllsBuildIniV2()
{
    QTemporaryDir temporaryDir;
    QVERIFY(temporaryDir.isValid());
    QDir dir(temporaryDir.path());

    dir.mkdir(".qt"_L1);
    QmlLsp::QQmllsBuildInformation buildIni;
    buildIni.loadSettingsFrom({ testFile("BrokenQmllsBuildIni") });

    QCOMPARE_EQ(buildIni.importPathsFor("/hello/src/File.qml"), { "/world/qml"_L1 });
    QCOMPARE_EQ(buildIni.resourceFilesFor("/hello/src/File.qml"), { "/123/a.qrc"_L1 });
}

QTEST_MAIN(tst_qmlls_qqmlcodemodel)
