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

#include "tst_qmlls_cli.h"

using namespace Qt::StringLiterals;

void tst_qmlls_cli::initTestCase()
{
    QQmlDataTest::initTestCase();

    m_qmllsPath = QLibraryInfo::path(QLibraryInfo::BinariesPath) + QLatin1String("/qmlls");
#ifdef Q_OS_WIN
    m_qmllsPath += QLatin1String(".exe");
#endif
    // allow overriding of the executable, to be able to use a qmlEcho script (as described in
    // qmllanguageservertool.cpp)
    m_qmllsPath = qEnvironmentVariable("QMLLS", m_qmllsPath);
    m_server.setProgram(m_qmllsPath);
}

// Helper structs to avoid confusions between expected and unexpected messages and between expected
// and unexpected diagnostics.
struct ExpectedMessages : public QStringList
{
    using QStringList::QStringList;
};
struct UnexpectedMessages : public QStringList
{
    using QStringList::QStringList;
};
struct ExpectedDiagnostics : public QStringList
{
    using QStringList::QStringList;
};
struct UnexpectedDiagnostics : public QStringList
{
    using QStringList::QStringList;
};

// Extra environment variables to be added to qmlls's environment.
struct Environment : public QList<std::pair<QString, QString>>
{
    using QList<std::pair<QString, QString>>::QList;
};

void tst_qmlls_cli::warnings_data()
{
    QTest::addColumn<QStringList>("args");
    QTest::addColumn<Environment>("environment");
    QTest::addColumn<QString>("filePath");
    // messages are printed to stderr and not shown in editor:
    QTest::addColumn<ExpectedMessages>("expectedMessages");
    QTest::addColumn<UnexpectedMessages>("unexpectedMessages");
    // diagnostics are passed via LSP to be shown in editor:
    QTest::addColumn<ExpectedDiagnostics>("expectedDiagnostics");
    QTest::addColumn<UnexpectedDiagnostics>("unexpectedDiagnostics");

    const Environment defaultEnv;
    const QString dir1 = testFile(u"ImportPath1"_s);
    const QString dir2 = testFile(u"ImportPath2"_s);
    const QString notDir = testFile(u"ImportPath1/SomeModule/qmldir"_s);
    const QString wrongDir = testFile(u"ImportPathInexistent"_s);

    const QString fileImportingDir1 = testFile(u"sourceFolder/ImportFromImportPath1.qml"_s);
    const QString fileImportingBothDirs = testFile(u"sourceFolder/ImportFromBothPaths.qml"_s);
    const QString fileImportingQtQuick = testFile(u"sourceFolder/ImportQtQuick.qml"_s);

    const QString importWarningDir1 = u"Warnings occurred while importing module \"SomeModule\""_s;
    const QString importWarningDir2 = u"Warnings occurred while importing module \"AnotherModule\""_s;
    const QString importWarningQtQuick = u"Warnings occurred while importing module \"QtQuick\""_s;

    const UnexpectedMessages noUnexpectedMessages;
    const QString warnAboutQmllsIniFiles{
        u"Using the build directories found in the .qmlls.ini file. Your build folder might not be found if no .qmlls.ini files are present in the root source folder."_s
    };

    QTest::addRow("2-build-dirs")
            << QStringList{ u"--build-dir"_s, dir1, u"-b"_s, dir2 } << defaultEnv
            << fileImportingDir1
            << ExpectedMessages{ u"Using build directories passed by -b: \"%1\", \"%2\"."_s.arg(
                       dir1, dir2) }
            << UnexpectedMessages{ warnAboutQmllsIniFiles } << ExpectedDiagnostics{}
            << UnexpectedDiagnostics{ importWarningDir1 };

    QTest::addRow("build-dir-not-dir")
            << QStringList{ u"--build-dir"_s, notDir, u"-b"_s, dir2 } << defaultEnv
            << fileImportingBothDirs
            << ExpectedMessages{ u"Argument \"%1\" passed to -b is not a directory."_s.arg(notDir) }
            << UnexpectedMessages{ warnAboutQmllsIniFiles }
            << ExpectedDiagnostics{ importWarningDir1 }
            << UnexpectedDiagnostics{ importWarningDir2 };

    QTest::addRow("build-dir-not-existing")
            << QStringList{ u"--build-dir"_s, wrongDir, u"-b"_s, dir2 } << defaultEnv
            << fileImportingBothDirs
            << ExpectedMessages{ u"Argument \"%1\" passed to -b does not exist."_s.arg(wrongDir) }
            << UnexpectedMessages{ warnAboutQmllsIniFiles } << ExpectedDiagnostics{}
            << UnexpectedDiagnostics{};

    QTest::addRow("build-dir-from-environment")
            << QStringList{}
            << Environment{ { u"QMLLS_BUILD_DIRS"_s,
                              u"%1%2%3"_s.arg(dir1, QDir::listSeparator(), dir2) } }
            << fileImportingBothDirs
            << ExpectedMessages{ u"Using build directories passed from environment variable \"QMLLS_BUILD_DIRS\": \"%1\", \"%2\"."_s
                                         .arg(dir1, dir2) }
            << UnexpectedMessages{ warnAboutQmllsIniFiles } << ExpectedDiagnostics{}
            << UnexpectedDiagnostics{ importWarningDir1, importWarningDir2 };

    QTest::addRow("build-dir-from-environment-not-existing")
            << QStringList{}
            << Environment{ { u"QMLLS_BUILD_DIRS"_s,
                              QStringList{ dir1, wrongDir, notDir }.join(QDir::listSeparator()) } }
            << fileImportingDir1
            << ExpectedMessages{ u"Argument \"%1\" from environment variable \"QMLLS_BUILD_DIRS\" does not exist."_s
                                         .arg(wrongDir),
                                 u"Argument \"%1\" from environment variable \"QMLLS_BUILD_DIRS\" is not a directory."_s
                                         .arg(notDir) }
            << UnexpectedMessages{ warnAboutQmllsIniFiles } << ExpectedDiagnostics{}
            << UnexpectedDiagnostics{ importWarningDir1, importWarningDir2 };

    QTest::addRow("ignore-environment-with-option")
            << QStringList{ u"--build-dir"_s, dir1 }
            << Environment{ { u"QMLLS_BUILD_DIRS"_s, dir2 } } << fileImportingBothDirs
            << ExpectedMessages{ u"Using build directories passed by -b: \"%1\"."_s.arg(dir1) }
            << UnexpectedMessages{ dir2, warnAboutQmllsIniFiles }
            << ExpectedDiagnostics{ importWarningDir2 }
            << UnexpectedDiagnostics{ importWarningDir1 };

    QTest::addRow("loadFromConfigFile")
            << QStringList{} << Environment{} << fileImportingDir1
            << ExpectedMessages{ warnAboutQmllsIniFiles } << UnexpectedMessages{}
            << ExpectedDiagnostics{ importWarningDir1 } << UnexpectedDiagnostics{};

    QTest::addRow("2-import-paths")
            << QStringList{ u"-I"_s, dir1, u"-I"_s, dir2 } << Environment{} << fileImportingBothDirs
            << ExpectedMessages{ u"Using import directories passed by -I: \"%1\", \"%2\"."_s.arg(
                       dir1, dir2) }
            << UnexpectedMessages{} << ExpectedDiagnostics{}
            << UnexpectedDiagnostics{ importWarningDir1, importWarningDir2 };

    QTest::addRow("import-paths-ignore-env")
            << QStringList{ u"-I"_s, dir1, } << Environment{ { u"QML_IMPORT_PATH"_s, dir2 } }
            << fileImportingBothDirs
            << ExpectedMessages{ u"Using import directories passed by -I: \"%1\"."_s.arg(dir1) }
            << UnexpectedMessages{ u"Using import directories passed from environment variable \"QML_IMPORT_PATH\": \"%1\"."_s.arg(dir2)}
            << ExpectedDiagnostics{importWarningDir2} << UnexpectedDiagnostics{ importWarningDir1 };

    QTest::addRow("2-import-paths-mixed")
            << QStringList{ u"-I"_s, dir1, u"-E"_s }
            << Environment{ { u"QML_IMPORT_PATH"_s, dir2 } } << fileImportingBothDirs
            << ExpectedMessages{ u"Using import directories passed by -I: \"%1\"."_s.arg(dir1),
                                 u"Using import directories passed from environment variable \"QML_IMPORT_PATH\": \"%1\"."_s
                                         .arg(dir2) }
            << UnexpectedMessages{} << ExpectedDiagnostics{}
            << UnexpectedDiagnostics{ importWarningDir1, importWarningDir2 };

    QTest::addRow("2-import-paths-deprecated")
            << QStringList{ u"-I"_s, dir1, u"-E"_s }
            << Environment{ { u"QML2_IMPORT_PATH"_s, dir2 } } << fileImportingBothDirs
            << ExpectedMessages{ u"Using import directories passed by -I: \"%1\"."_s.arg(dir1),
                                 u"Using import directories passed from the deprecated environment variable \"QML2_IMPORT_PATH\": \"%1\"."_s
                                         .arg(dir2) }
            << UnexpectedMessages{} << ExpectedDiagnostics{}
            << UnexpectedDiagnostics{ importWarningDir1, importWarningDir2 };

    QTest::addRow("bare")
            << QStringList{ u"--bare"_s, } << Environment{ { u"QML_IMPORT_PATH"_s, dir2 } }
            << fileImportingQtQuick
            << ExpectedMessages{}
            << UnexpectedMessages{ u"Using import directories passed from environment variable \"QML_IMPORT_PATH\": \"%1\"."_s.arg(dir2)}
            << ExpectedDiagnostics{ importWarningQtQuick } << UnexpectedDiagnostics {} ;

    QTest::addRow("unbare")
            << QStringList{} << Environment{ { u"QML_IMPORT_PATH"_s, dir2 } }
            << fileImportingQtQuick << ExpectedMessages{}
            << UnexpectedMessages{ u"Using import directories passed from environment variable \"QML_IMPORT_PATH\": \"%1\"."_s
                                           .arg(dir2) }
            << ExpectedDiagnostics{} << UnexpectedDiagnostics{ importWarningQtQuick };

    QTest::addRow("cmake-jobs-commandline")
            << QStringList{ "-j"_L1, "8"_L1 } << Environment{ { "QMLLS_CMAKE_JOBS"_L1, "42"_L1 } }
            << fileImportingQtQuick
            << ExpectedMessages{ "Using 8 jobs for CMake, set via --cmake-jobs."_L1 }
            << UnexpectedMessages{ "QMLLS_CMAKE_JOBS environment variable"_L1 }
            << ExpectedDiagnostics{} << UnexpectedDiagnostics{};
    QTest::addRow("cmake-jobs-commandline-bad")
            << QStringList{ "-j"_L1, "8.5"_L1 } << Environment{} << fileImportingQtQuick
            << ExpectedMessages{ "Value \"8.5\" passed to --cmake-jobs is not a number greater than 0 and not \"max\", using default value of 1 instead."_L1 }
            << UnexpectedMessages{} << ExpectedDiagnostics{}
            << UnexpectedDiagnostics{ importWarningQtQuick };
    QTest::addRow("cmake-jobs-environment")
            << QStringList{} << Environment{ { "QMLLS_CMAKE_JOBS"_L1, "42"_L1 } }
            << fileImportingQtQuick
            << ExpectedMessages{ "Using 42 jobs for CMake, set via QMLLS_CMAKE_JOBS environment variable."_L1 }
            << UnexpectedMessages{} << ExpectedDiagnostics{} << UnexpectedDiagnostics{};
    QTest::addRow("cmake-jobs-environment-bad")
            << QStringList{} << Environment{ { "QMLLS_CMAKE_JOBS"_L1, "8.5"_L1 } }
            << fileImportingQtQuick
            << ExpectedMessages{ "Value \"8.5\" passed to QMLLS_CMAKE_JOBS is not a number greater than 0 and not \"max\", using default value of 1 instead."_L1 }
            << UnexpectedMessages{} << ExpectedDiagnostics{}
            << UnexpectedDiagnostics{ importWarningQtQuick };
    QTest::addRow("cmake-jobs-default")
            << QStringList{} << Environment{} << fileImportingQtQuick
            << ExpectedMessages{ "Using 1 job for CMake" } << UnexpectedMessages{}
            << ExpectedDiagnostics{} << UnexpectedDiagnostics{};
    QTest::addRow("cmake-jobs-max")
            << QStringList{ "-j"_L1, "max"_L1 } << Environment{} << fileImportingQtQuick
            << ExpectedMessages{ "Using max (%1) jobs for CMake"_L1.arg(
                       QString::number(QThread::idealThreadCount())) }
            << UnexpectedMessages{} << ExpectedDiagnostics{} << UnexpectedDiagnostics{};
}

auto tst_qmlls_cli::startServerRAII()
{
    startServerImpl();
    return qScopeGuard([this]() { this->stopServerImpl(); });
}

void tst_qmlls_cli::startServerImpl()
{
    m_protocol = std::make_unique<QLanguageServerProtocol>(
            [this](const QByteArray &data) { m_server.write(data); });

    connect(&m_server, &QProcess::readyReadStandardOutput, this, [this]() {
        QByteArray data = m_server.readAllStandardOutput();
        m_protocol->receiveData(data);
    });

    m_server.start();

    QLspSpecification::InitializeParams clientInfo;
    clientInfo.rootUri = QUrl::fromLocalFile(dataDirectory() + "/default").toString().toUtf8();

    QLspSpecification::TextDocumentClientCapabilities tDoc;
    tDoc.typeDefinition = QLspSpecification::TypeDefinitionClientCapabilities{ false, false };

    QLspSpecification::PublishDiagnosticsClientCapabilities pDiag;
    tDoc.publishDiagnostics = pDiag;
    pDiag.versionSupport = true;
    clientInfo.capabilities.textDocument = tDoc;
    bool didInit = false;
    m_protocol->requestInitialize(
            clientInfo, [this, &didInit](const QLspSpecification::InitializeResult &serverInfo) {
                Q_UNUSED(serverInfo);
                m_protocol->notifyInitialized(QLspSpecification::InitializedParams());
                didInit = true;
            });
    QTRY_COMPARE_WITH_TIMEOUT(didInit, true, 10000);
}

void tst_qmlls_cli::stopServerImpl()
{
    // note: the lambdas used in the "connect"-call of the tests might reference local variables, so
    // disconnect the lambda via QScopedGuard to avoid the lambda to be called during
    // waitForFinished();
    disconnect(&m_server, nullptr, nullptr, nullptr);
    m_server.closeWriteChannel();
    m_server.waitForFinished();
    QTRY_COMPARE(m_server.state(), QProcess::NotRunning);
    QCOMPARE(m_server.exitStatus(), QProcess::NormalExit);
}

void tst_qmlls_cli::warnings()
{
    QFETCH(QStringList, args);
    QFETCH(Environment, environment);
    QFETCH(ExpectedMessages, expectedMessages);
    QFETCH(UnexpectedMessages, unexpectedMessages);
    QFETCH(QString, filePath);
    QFETCH(ExpectedDiagnostics, expectedDiagnostics);
    QFETCH(UnexpectedDiagnostics, unexpectedDiagnostics);

    QProcessEnvironment processEnvironment = QProcessEnvironment::systemEnvironment();
    for (const auto &entry : environment)
        processEnvironment.insert(entry.first, entry.second);
    m_server.setProcessEnvironment(processEnvironment);
    m_server.setArguments(args);

    QList<int> countExpectedMessages(expectedMessages.size(), 0);
    QList<int> countUnexpectedMessages(unexpectedMessages.size(), 0);
    QList<int> countExpectedDiagnostics(expectedDiagnostics.size(), 0);
    QList<int> countUnexpectedDiagnostics(unexpectedDiagnostics.size(), 0);

    connect(&m_server, &QProcess::readyReadStandardError, this,
            [this, &expectedMessages, &countExpectedMessages, &unexpectedMessages,
             &countUnexpectedMessages]() {
                const auto data = QString::fromUtf8(m_server.readAllStandardError());
                if (data.isEmpty())
                    return;

                for (int i = 0; i < expectedMessages.size(); ++i) {
                    if (data.contains(expectedMessages[i]))
                        ++countExpectedMessages[i];
                }
                for (int i = 0; i < unexpectedMessages.size(); ++i) {
                    if (data.contains(unexpectedMessages[i]))
                        ++countUnexpectedMessages[i];
                }
            });

    auto guard = startServerRAII();

    // each expected message should appear exactly one time
    QTRY_COMPARE_WITH_TIMEOUT(countExpectedMessages, QList<int>(expectedMessages.size(), 1), 500);
    // each unexpected message should appear exactly zero times
    QCOMPARE(countUnexpectedMessages, QList<int>(unexpectedMessages.size(), 0));

    bool diagnosticOk = false;
    m_protocol->registerPublishDiagnosticsNotificationHandler(
            [&diagnosticOk, &expectedDiagnostics, &countExpectedDiagnostics, &unexpectedDiagnostics,
             &countUnexpectedDiagnostics](const QByteArray &,
                                          const QLspSpecification::PublishDiagnosticsParams &p) {
                for (const auto &d : p.diagnostics) {
                    const QString message = QString::fromUtf8(d.message);
                    for (int i = 0; i < expectedDiagnostics.size(); ++i) {
                        if (message.contains(expectedDiagnostics[i]))
                            ++countExpectedDiagnostics[i];
                    }
                    for (int i = 0; i < unexpectedDiagnostics.size(); ++i) {
                        if (message.contains(unexpectedDiagnostics[i]))
                            ++countUnexpectedDiagnostics[i];
                    }
                }
                diagnosticOk = true;
            });

    QFile file(filePath);
    QVERIFY(file.open(QIODevice::ReadOnly));

    QLspSpecification::DidOpenTextDocumentParams oParams;
    QLspSpecification::TextDocumentItem textDocument;
    QByteArray uri = QUrl::fromLocalFile(filePath).toEncoded();
    textDocument.uri = uri;
    textDocument.text = file.readAll();
    oParams.textDocument = textDocument;
    m_protocol->notifyDidOpenTextDocument(oParams);

    QTRY_VERIFY_WITH_TIMEOUT(diagnosticOk, 3000);
    // each expected diagnostic should appear exactly one time
    QCOMPARE(countExpectedDiagnostics, QList<int>(expectedDiagnostics.size(), 1));
    // each unexpected diagnostic should appear exactly zero times
    QCOMPARE(countUnexpectedDiagnostics, QList<int>(unexpectedDiagnostics.size(), 0));
}

void tst_qmlls_cli::inputFile()
{
    m_server.setArguments({ "--inputFile"_L1, testFile("serverInput.jsonrpc") });
    m_protocol =
            std::make_unique<QLanguageServerProtocol>([](const QByteArray &) { /* ignored */ });

    connect(&m_server, &QProcess::readyReadStandardOutput, this, [this]() {
        QByteArray data = m_server.readAllStandardOutput();
        m_protocol->receiveData(data);
    });

    bool didInit = false;

    QLspSpecification::InitializeParams params;
    m_protocol->requestInitialize(
            params, [&didInit](const QLspSpecification::InitializeResult &) { didInit = true; });

    m_server.start();
    QTRY_COMPARE_WITH_TIMEOUT(didInit, true, 10000);

    disconnect(&m_server, &QProcess::readyReadStandardOutput, nullptr, nullptr);
    m_server.close();
    m_server.waitForFinished();
    QTRY_COMPARE(m_server.state(), QProcess::NotRunning);
}

void tst_qmlls_cli::dontShutdownOnStartup()
{
    m_server.setArguments({});
    m_server.start();
    // it shouldn't shutdown before the close() call.
    m_server.waitForFinished(3000);
    QCOMPARE(m_server.state(), QProcess::Running);
    m_server.close();
    m_server.waitForFinished();
    QTRY_COMPARE(m_server.state(), QProcess::NotRunning);
}

QTEST_MAIN(tst_qmlls_cli)
