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

#include "formeditor.h"

#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/systemsettings.h>

#include <cppeditor/builtineditordocumentparser.h>
#include <cppeditor/cppmodelmanager.h>
#include <cppeditor/cpptoolstestcase.h>
#include <cppeditor/editordocumenthandle.h>

#include <cplusplus/CppDocument.h>
#include <cplusplus/Overview.h>

#include "designersettings.h"

#include <texteditor/texteditor.h>
#include <texteditor/textdocument.h>

#include <QDesignerFormEditorInterface>
#include <QDesignerIntegrationInterface>
#include <QElapsedTimer>
#include <QStringList>
#include <QTest>

using namespace Core;
using namespace CppEditor;
using namespace CPlusPlus;
using namespace Designer;
using namespace Designer::Internal;
using namespace Utils;

namespace Designer::Internal {

class DocumentContainsFunctionDefinition: protected SymbolVisitor
{
public:
    bool operator()(Scope *scope, const QString function)
    {
        if (!scope)
            return false;

        m_referenceFunction = function;
        m_result = false;

        accept(scope);
        return m_result;
    }

protected:
    bool preVisit(Symbol *) { return !m_result; }

    bool visit(Function *symbol)
    {
        const QString function = m_overview.prettyName(symbol->name());
        if (function == m_referenceFunction)
            m_result = true;
        return false;
    }

private:
    bool m_result;
    QString m_referenceFunction;
    Overview m_overview;
};

class DocumentContainsDeclaration: protected SymbolVisitor
{
public:
    bool operator()(Scope *scope, const QString &function)
    {
        if (!scope)
            return false;

        m_referenceFunction = function;
        m_result = false;

        accept(scope);
        return m_result;
    }

protected:
    bool preVisit(Symbol *) { return !m_result; }

    void postVisit(Symbol *symbol)
    {
        if (symbol->asClass())
            m_currentClass.clear();
    }

    bool visit(Class *symbol)
    {
        m_currentClass = m_overview.prettyName(symbol->name());
        return true;
    }

    bool visit(Declaration *symbol)
    {
        QString declaration = m_overview.prettyName(symbol->name());
        if (!m_currentClass.isEmpty())
            declaration = m_currentClass + "::" + declaration;
        if (m_referenceFunction == declaration)
            m_result = true;
        return false;
    }

private:
    bool m_result;
    QString m_referenceFunction;
    QString m_currentClass;
    Overview m_overview;
};

static bool documentContainsFunctionDefinition(const Document::Ptr &document, const QString &function)
{
    return DocumentContainsFunctionDefinition()(document->globalNamespace(), function);
}

static bool documentContainsMemberFunctionDeclaration(const Document::Ptr &document,
                                               const QString &declaration)
{
    return DocumentContainsDeclaration()(document->globalNamespace(), declaration);
}

class GoToSlotTestCase : public CppEditor::Tests::TestCase
{
public:
    GoToSlotTestCase(const FilePaths &files, bool pointerToMember, const QString &expectedConnect)
    {
        QVERIFY(succeededSoFar());
        QCOMPARE(files.size(), 3);

        const bool savedPmf = designerSettings().generatePointerToMemberConnections.value();
        designerSettings().generatePointerToMemberConnections.setValue(pointerToMember);
        const QScopeGuard restorePmf([savedPmf] {
            designerSettings().generatePointerToMemberConnections.setValue(savedPmf);
        });

        QList<TextEditor::BaseTextEditor *> editors;
        for (const FilePath &file : files) {
            IEditor *editor = EditorManager::openEditor(file);
            TextEditor::BaseTextEditor *e = qobject_cast<TextEditor::BaseTextEditor *>(editor);
            QVERIFY(e);
            closeEditorAtEndOfTestCase(editor);
            editors << e;
        }

        const FilePath cppFile = files.at(0);
        const FilePath hFile = files.at(1);

        QCOMPARE(DocumentModel::openedDocuments().size(), files.size());
        waitForFilesInGlobalSnapshot({cppFile, hFile});

        // Execute "Go To Slot"
        QDesignerIntegrationInterface *integration = designerEditor()->integration();
        QVERIFY(integration);
        integration->emitNavigateToSlot("pushButton", "clicked()", QStringList());

        QCOMPARE(EditorManager::currentDocument()->filePath(), cppFile);
        QVERIFY(EditorManager::currentDocument()->isModified());

        // Wait for updated documents
        for (TextEditor::BaseTextEditor *editor : std::as_const(editors)) {
            QElapsedTimer t;
            t.start();
            const FilePath filePath = editor->document()->filePath();
            if (auto parser = BuiltinEditorDocumentParser::get(filePath)) {
                while (t.elapsed() < 2000) {
                    if (Document::Ptr document = parser->document()) {
                        if (document->editorRevision() == 2)
                            break;
                    }
                    QApplication::processEvents();
                }
            }
        }

        // Compare
        const auto cppDocumentParser = BuiltinEditorDocumentParser::get(cppFile);
        QVERIFY(cppDocumentParser);
        const Document::Ptr cppDocument = cppDocumentParser->document();
        QVERIFY(cppDocument->editorRevision() >= 2);
        QVERIFY(checkDiagsnosticMessages(cppDocument));

        const auto hDocumentParser = BuiltinEditorDocumentParser::get(hFile);
        QVERIFY(hDocumentParser);
        const Document::Ptr hDocument = hDocumentParser->document();
        QVERIFY(checkDiagsnosticMessages(hDocument));

        const QString cppText = editors.at(0)->textDocument()->plainText();
        if (pointerToMember) {
            // New default: camelCase slot + explicit pointer-to-member connect().
            QVERIFY(documentContainsFunctionDefinition(cppDocument, "Form::onPushButtonClicked"));
            QVERIFY(documentContainsMemberFunctionDeclaration(hDocument, "Form::onPushButtonClicked"));
            QVERIFY2(cppText.contains(expectedConnect), qPrintable(cppText));
        } else {
            // Legacy behavior: on_...() slot connected via connectSlotsByName().
            QVERIFY(documentContainsFunctionDefinition(cppDocument, "Form::on_pushButton_clicked"));
            QVERIFY(documentContainsMemberFunctionDeclaration(hDocument, "Form::on_pushButton_clicked"));
            QVERIFY(!cppText.contains("connect("));
        }
    }

    static bool checkDiagsnosticMessages(const Document::Ptr &document)
    {
        if (!document)
            return false;

        // Since no project is opened and the ui_*.h is not generated,
        // the following diagnostic messages will be ignored.
        const QStringList ignoreList = QStringList({"ui_form.h: No such file or directory",
                                                    "QWidget: No such file or directory"});
        QList<Document::DiagnosticMessage> cleanedDiagnosticMessages;
        const auto diagnosticMessages = document->diagnosticMessages();
        for (const Document::DiagnosticMessage &message : diagnosticMessages) {
            if (!ignoreList.contains(message.text()))
                cleanedDiagnosticMessages << message;
        }

        return cleanedDiagnosticMessages.isEmpty();
    }
};

class GoToSlotTest final : public QObject
{
    Q_OBJECT

private slots:
    void test_gotoslot();
    void test_gotoslot_data();
};

/// Check: Executes "Go To Slot..." on a QPushButton in a *.ui file and checks if the respective
/// header and source files are correctly updated.
void GoToSlotTest::test_gotoslot()
{
    class SystemSettingsMgr {
    public:
        SystemSettingsMgr()
            : m_saveAfterRefactor(Core::Internal::systemSettings().autoSaveAfterRefactoring.value())
        {
            Core::Internal::systemSettings().autoSaveAfterRefactoring.setValue(false);
        }
        ~SystemSettingsMgr()
        {
            Core::Internal::systemSettings().autoSaveAfterRefactoring.setValue(m_saveAfterRefactor);
        }
    private:
        const bool m_saveAfterRefactor;
    } systemSettingsMgr;

    QFETCH(FilePaths, files);
    QFETCH(bool, pointerToMember);
    QFETCH(QString, expectedConnect);
    (GoToSlotTestCase(files, pointerToMember, expectedConnect));
}

void GoToSlotTest::test_gotoslot_data()
{
    QTest::addColumn<FilePaths>("files");
    QTest::addColumn<bool>("pointerToMember");
    QTest::addColumn<QString>("expectedConnect");

    // The access prefix of the generated connect() is taken from the setupUi() call.
    const auto connectVia = [](const QString &prefix) {
        return QString("connect(%1pushButton, &QPushButton::clicked, "
                       "this, &Form::onPushButtonClicked);").arg(prefix);
    };

    const auto dataDir = [](const QString &subdir) {
        return FilePath::fromUserInput(SRCDIR "/../../../tests/designer/" + subdir);
    };

    FilePath testDataDirWithoutProject = dataDir("gotoslot_withoutProject");
    QVERIFY(testDataDirWithoutProject.exists());
    const FilePaths withoutProjectFiles({testDataDirWithoutProject / "form.cpp",
                                         testDataDirWithoutProject / "form.h",
                                         testDataDirWithoutProject / "form.ui"});
    QTest::newRow("withoutProject") << withoutProjectFiles << true << connectVia("ui->");
    QTest::newRow("withoutProject_legacy") << withoutProjectFiles << false << QString();

    const FilePath testDataDirMemberUi = dataDir("gotoslot_m_ui");
    QVERIFY(testDataDirMemberUi.exists());
    QTest::newRow("memberPrefixedUi")
        << FilePaths({testDataDirMemberUi / "form.cpp", testDataDirMemberUi / "form.h",
                      testDataDirWithoutProject / "form.ui"}) // reuse
        << true << connectVia("m_ui->");

    // Finding the right class for inserting definitions/declarations is based on
    // finding a class with a member whose type is the class from the "ui_xxx.h" header.
    // In the following test data the header files contain an extra class referencing
    // the same class name.

    FilePath testDataDir;

    testDataDir = dataDir("gotoslot_insertIntoCorrectClass_pointer");
    QVERIFY(testDataDir.exists());
    QTest::newRow("insertIntoCorrectClass_pointer")
        << FilePaths({testDataDir / "form.cpp", testDataDir / "form.h",
                      testDataDirWithoutProject / "form.ui"}) // reuse
        << true << connectVia("ui->");

    testDataDir = dataDir("gotoslot_insertIntoCorrectClass_non-pointer");
    QVERIFY(testDataDir.exists());
    QTest::newRow("insertIntoCorrectClass_non-pointer")
        << FilePaths({testDataDir / "form.cpp", testDataDir / "form.h",
                        testDataDirWithoutProject / "form.ui"}) // reuse
        << true << connectVia("ui.");

    testDataDir = dataDir("gotoslot_insertIntoCorrectClass_pointer_ns_using");
    QVERIFY(testDataDir.exists());
    QTest::newRow("insertIntoCorrectClass_pointer_ns_using")
        << FilePaths({testDataDir / "form.cpp", testDataDir / "form.h",
                      testDataDir / "form.ui"}) << true << connectVia("ui->");
}

QObject *createGoToSlotTest()
{
    return new GoToSlotTest;
}

} // Designer::Internal

#include "gotoslot_test.moc"
