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

#include <QtQmlDom/private/qqmldomitem_p.h>
#include <QtQmlDom/private/qqmldomtop_p.h>
#include <QtQmlDom/private/qqmldomelements_p.h>
#include <QtQmlLS/private/documentsymbolutils_p.h>
#include <QtLanguageServer/private/qlanguageserverspectypes_p.h>

#include "tst_document_symbol_utils.h"

using namespace QQmlJS::Dom;
using QLspSpecification::DocumentSymbol;
using SymbolsList = QList<DocumentSymbol>;

QT_BEGIN_NAMESPACE
// TODO move out somewhere?
template <typename DomClass>
static inline DomItem domItem(DomClass &&c)
{
    DomItem domEnv(DomEnvironment::create({}));
    constexpr auto kind = std::decay<DomClass>::type::kindValue;
    if constexpr (domTypeIsDomElement(kind)) {
        c.updatePathFromOwner(
                Path::fromCurrent()); // necessary for querying fields, a.k.a. init DomElement
        // For the "DomElement"-s owners are required,
        // hence creating Env as an owner and then call copy on it
        return domEnv.copy(&c);
    }
    if constexpr (kind == DomType::QmlFile) {
        return domEnv.copy(&c);
    }
    // it's helpful to use wrap on DomEnv instead of just DomItem() to make
    // .canonicalPath() usable
    return domEnv.wrap(QQmlJS::Dom::PathEls::PathComponent(), c);
}
QT_END_NAMESPACE

static bool compareDocumentSymbols(const DocumentSymbol &actual, const DocumentSymbol &expected)
{
    if (actual.name != expected.name) {
        return false;
    }
    // Compare children
    if (actual.children.has_value() != expected.children.has_value()) {
        return false;
    }
    if (!actual.children.has_value()) {
        return true;
    }
    return std::equal(actual.children->cbegin(), actual.children->cend(),
                      expected.children->cbegin(), expected.children->cend(),
                      compareDocumentSymbols);
}

static bool compareDocumentSymbolsLists(const QList<DocumentSymbol> &actual,
                                        const QList<DocumentSymbol> &expected)
{
    return std::equal(actual.cbegin(), actual.cend(), expected.cbegin(), expected.cend(),
                      compareDocumentSymbols);
}

/*
 * fakeQmlFileItem returns a DomItem representing a pseudo QmlFile like the following
 *
 * {
 *     id: fakeId
 *     property var
 *
 *     // bindings
 *     b :
 *     obj : {}
 *     objects: [{}, {}]
 *     //binding with ScriptExpression is not relevant to DocumentSymbolVisitor
 *
 *     // 2 default methodInfo objects
 *     (){}
 *     (){}
 *
 *     // 2 default QmlObject children
 *     {}
 *     {}
 *
 *     component inlCmp.withEnum {
 *         enum { , ,}
 *     }
 * }
 *
 * Ofc it's invalid QmlFile, however this is fine to make sure that DocumentSymbolVisitor
 * visits all relevant fields/entities and final QList<DocumentSymbol> has the same hierarchy
 * as this QmlFile item.
 */
QQmlJS::Dom::DomItem tst_document_symbol_utils::fakeQmlFileItem()
{
    auto qmlFile = std::make_shared<QmlFile>();
    auto rootQmlObject = QmlObject();

    // add explicit propertyDef to rootQmlObject
    auto propDef = PropertyDefinition();
    rootQmlObject.addPropertyDef(propDef, AddOption::KeepExisting);

    // add bindings to rootQmlObject
    auto binding = Binding("b");
    rootQmlObject.addBinding(binding, AddOption::KeepExisting);

    auto bindingWithObject = Binding("obj", QmlObject());
    rootQmlObject.addBinding(bindingWithObject, AddOption::KeepExisting);

    auto bindingWithArrayOfObjects = Binding("objects", QList<QmlObject>(2));
    rootQmlObject.addBinding(bindingWithArrayOfObjects, AddOption::KeepExisting);

    // add Methods to rootQmlObject
    rootQmlObject.setMethods(
            { std::pair<QString, MethodInfo>(), std::pair<QString, MethodInfo>() });

    // add children to rootQmlObject
    rootQmlObject.setChildren(QList<QmlObject>(2));

    // add root QmlObject to root component
    auto rootCmp = QmlComponent();
    rootCmp.addId(Id("fakeId"));
    rootCmp.addObject(rootQmlObject);
    qmlFile->addComponent(rootCmp);

    // add inline component with Enum
    auto inlCmp = QmlComponent("inlCmp.withEnum");
    auto enumDecl = EnumDecl();
    enumDecl.setValues(QList<EnumItem>(2));
    inlCmp.addEnumeration(enumDecl);
    qmlFile->addComponent(inlCmp);

    // create env and return qmlFileItem
    DomItem envItem = DomEnvironment::create({});
    return envItem.copy(qmlFile);
}

SymbolsList tst_document_symbol_utils::expectedSymbolsOfFakeQmlFile()
{
    // Main QmlComponent

    DocumentSymbol qmlObjectSymbol = fakeSymbol(DomType::QmlObject);

    DocumentSymbol bindingSymbol = fakeSymbol(DomType::Binding);

    DocumentSymbol bindingWithObjectSymbol(bindingSymbol);
    bindingWithObjectSymbol.children = { qmlObjectSymbol };

    DocumentSymbol bindingWithArrayOfObjectsSymbol(bindingSymbol);
    bindingWithArrayOfObjectsSymbol.children = { qmlObjectSymbol, qmlObjectSymbol };

    // Root QmlObject
    DocumentSymbol rootQmlObjectSymbol(qmlObjectSymbol);
    rootQmlObjectSymbol.children = {
        fakeSymbol(DomType::PropertyDefinition), // property var
        std::move(bindingSymbol), // b :
        std::move(bindingWithObjectSymbol), // obj : {}
        std::move(bindingWithArrayOfObjectsSymbol), // objects: [{}, {}]
        fakeSymbol(DomType::MethodInfo), // (){}
        fakeSymbol(DomType::MethodInfo), // (){}
        qmlObjectSymbol, // {}
        qmlObjectSymbol // {}
    };

    DocumentSymbol mainQmlCompSymbol = fakeSymbol(DomType::QmlComponent);
    mainQmlCompSymbol.children = { // BEWARE
                                   // because Id is manually handled
                                   // outside of visiting logic, before it becomes a child of
                                   // Object, it's not present here
                                   std::move(rootQmlObjectSymbol)
    };

    // Inline Component

    DocumentSymbol enumDeclSymbol = fakeSymbol(DomType::EnumDecl);
    enumDeclSymbol.children = { fakeSymbol(DomType::EnumItem), fakeSymbol(DomType::EnumItem) };

    DocumentSymbol inlQmlCompSymbol = fakeSymbol(DomType::QmlComponent);
    inlQmlCompSymbol.children = { std::move(enumDeclSymbol) };

    // QmlFile symbol
    DocumentSymbol qmlFileSymbol = fakeSymbol(DomType::QmlFile);
    qmlFileSymbol.children = { std::move(mainQmlCompSymbol), std::move(inlQmlCompSymbol) };
    return { std::move(qmlFileSymbol) };
}

void tst_document_symbol_utils::assembleSymbolsForQmlFile()
{
    const auto qmlFileItem = fakeQmlFileItem();
    // This assembling function simply transforms Item to a FakeSymbol with children
    const auto af = [](const DomItem &item, SymbolsList &&children) -> SymbolsList {
        if (domTypeIsContainer(item.internalKind())
            || item.internalKind() == QQmlJS::Dom::DomType::Empty) {
            // skip these wrappers to simplify manual construction of symbols a bit
            return std::move(children);
        }
        return SymbolsList{ fakeSymbol(item.internalKind(), std::move(children)) };
    };
    const auto expectedSymbols = expectedSymbolsOfFakeQmlFile();

    const auto symbols = DocumentSymbolUtils::assembleSymbolsForQmlFile(qmlFileItem, af);

    QVERIFY(compareDocumentSymbolsLists(symbols, expectedSymbols));
}

void tst_document_symbol_utils::symbolNameOf()
{
    { // Id
        // for the DomItem's representing Id-s, DocumentSymbol::name = ByteArray("id"),
        // the Dom::Id::name will be used as a DocumentSymbol::detail
        Id id;
        auto expectedName = QByteArray("id");
        auto name = DocumentSymbolUtils::symbolNameOf(domItem(id));
        QCOMPARE(name, expectedName);
        id.name = "name";
        name = DocumentSymbolUtils::symbolNameOf(domItem(id));
        QCOMPARE(name, expectedName);
    }

    { // EnumItem
        const QString enumItemName("enumItem");
        EnumItem enumItem(enumItemName);
        auto name = DocumentSymbolUtils::symbolNameOf(domItem(enumItem));
        QCOMPARE(name, enumItemName.toUtf8());
    }

    { // EnumDecl
        const QString enumDeclName("enumDecl");
        EnumDecl enumDecl(enumDeclName);
        auto name = DocumentSymbolUtils::symbolNameOf(domItem(enumDecl));
        QCOMPARE(name, enumDeclName.toUtf8());
    }

    { // PropertyDefinition
        const QString propertyName("propName");
        PropertyDefinition propDef;
        propDef.name = propertyName;
        auto name = DocumentSymbolUtils::symbolNameOf(domItem(propDef));
        QCOMPARE(name, propertyName.toUtf8());
    }

    { // Binding
        const QString bindingName("bindingName");
        Binding binding(bindingName);
        auto name = DocumentSymbolUtils::symbolNameOf(domItem(binding));
        QCOMPARE(name, bindingName.toUtf8());
    }

    { // Method
        const QString methodName("methodName");
        MethodInfo mInfo;
        mInfo.name = methodName;
        auto name = DocumentSymbolUtils::symbolNameOf(domItem(mInfo));
        QCOMPARE(name, methodName.toUtf8());
    }

    { // QmlObject
        const QString objName("objName");
        QmlObject obj;
        obj.setName(objName);
        const auto name = DocumentSymbolUtils::symbolNameOf(domItem(obj));
        QCOMPARE(name, objName.toUtf8());
    }

    { // QmlComponent
        const QString cmpName("cmpName");
        QmlComponent cmp(cmpName);
        const auto name = DocumentSymbolUtils::symbolNameOf(domItem(cmp));
        QCOMPARE(name, cmpName.toUtf8());
    }
}

void tst_document_symbol_utils::symbolKindOf()
{
    { // Method && signal
        MethodInfo mInfo;
        auto kind = DocumentSymbolUtils::symbolKindOf(domItem(mInfo));
        QCOMPARE(kind, QLspSpecification::SymbolKind::Method);

        mInfo.methodType = MethodInfo::Signal;
        kind = DocumentSymbolUtils::symbolKindOf(domItem(mInfo));
        QCOMPARE(kind, QLspSpecification::SymbolKind::Event);
    }

    QCOMPARE(DocumentSymbolUtils::symbolKindOf(domItem(Binding())),
             QLspSpecification::SymbolKind::Variable);
    QCOMPARE(DocumentSymbolUtils::symbolKindOf(domItem(PropertyDefinition())),
             QLspSpecification::SymbolKind::Property);
    QCOMPARE(DocumentSymbolUtils::symbolKindOf(domItem(Id())), QLspSpecification::SymbolKind::Key);
    QCOMPARE(DocumentSymbolUtils::symbolKindOf(domItem(EnumDecl())),
             QLspSpecification::SymbolKind::Enum);
    QCOMPARE(DocumentSymbolUtils::symbolKindOf(domItem(EnumItem())),
             QLspSpecification::SymbolKind::EnumMember);
    QCOMPARE(DocumentSymbolUtils::symbolKindOf(domItem(QmlObject())),
             QLspSpecification::SymbolKind::Object);
    QCOMPARE(DocumentSymbolUtils::symbolKindOf(domItem(QmlComponent())),
             QLspSpecification::SymbolKind::Module);
    QCOMPARE(DocumentSymbolUtils::symbolKindOf(domItem(QmlFile())),
             QLspSpecification::SymbolKind::File);
    QCOMPARE(DocumentSymbolUtils::symbolKindOf(DomItem()), QLspSpecification::SymbolKind::Null);
}

void tst_document_symbol_utils::tryGetDetailOf()
{
    { // Id
        Id id;
        auto detail = DocumentSymbolUtils::tryGetDetailOf(domItem(id));
        QVERIFY(!detail.has_value());

        const QString name("name");
        id.name = name;
        const auto expectedDetail = name.toUtf8();
        detail = DocumentSymbolUtils::tryGetDetailOf(domItem(id));
        QVERIFY(detail.has_value());
        QCOMPARE(detail, expectedDetail);
    }
    { // EnumItem
        const int value = 4;
        const auto expectedDetail = QByteArray::number(value);
        EnumItem enumItem("a", value);
        const auto detail = DocumentSymbolUtils::tryGetDetailOf(domItem(enumItem));
        QCOMPARE(detail, expectedDetail);
    }
    { // Binding
        Binding b;
        auto detail = DocumentSymbolUtils::tryGetDetailOf(domItem(b));
        QCOMPARE(detail, std::nullopt);

        b.setValue(std::make_unique<BindingValue>());
        detail = DocumentSymbolUtils::tryGetDetailOf(domItem(b));
        QCOMPARE(detail, std::nullopt);

        b.setValue(std::make_unique<BindingValue>(QmlObject()));
        detail = DocumentSymbolUtils::tryGetDetailOf(domItem(b));
        QCOMPARE(detail, std::nullopt);

        b.setValue(std::make_unique<BindingValue>(QList<QmlObject>()));
        detail = DocumentSymbolUtils::tryGetDetailOf(domItem(b));
        QCOMPARE(detail, std::nullopt);

        {
            const QString bindingValue("4");
            const auto expectedDetail = bindingValue.toUtf8();
            const QString bindingExpr = bindingValue + ";";
            const auto exprPtr = std::make_shared<ScriptExpression>(
                    bindingExpr, ScriptExpression::ExpressionType::BindingExpression);
            b.setValue(std::make_unique<BindingValue>(exprPtr));
            detail = DocumentSymbolUtils::tryGetDetailOf(domItem(b));
            QCOMPARE(detail, expectedDetail);
        }
        {
            const QString bindingExpr("12345678901234567890123456"); // 26 symbols
            const auto expectedDetail = QString("1234567890123456789012...").toUtf8();
            const auto exprPtr = std::make_shared<ScriptExpression>(
                    bindingExpr, ScriptExpression::ExpressionType::BindingExpression);
            b.setValue(std::make_unique<BindingValue>(exprPtr));
            detail = DocumentSymbolUtils::tryGetDetailOf(domItem(b));
            QCOMPARE(detail, expectedDetail);
        }
    }
    { // MethodInfo
        { // Method
            MethodParameter intA;
            intA.name = "a";
            intA.typeName = "int";
            MethodParameter stringB;
            stringB.name = "b";
            stringB.typeName = "string";
            MethodInfo method;
            method.parameters = { intA, stringB };
            method.typeName = "bool";

            const auto expectedDetail = QString("(a: int, b: string): bool").toUtf8();
            const auto detail = DocumentSymbolUtils::tryGetDetailOf(domItem(method));
            QVERIFY(detail.has_value());
            QCOMPARE(detail.value(), expectedDetail);
        }
        { // Signal
            MethodParameter intA;
            intA.name = "a";
            intA.typeName = "int";
            MethodInfo method;
            method.methodType = MethodInfo::MethodType::Signal;
            method.parameters = { intA };

            const auto expectedDetail = QString("(a: int)").toUtf8();
            const auto detail = DocumentSymbolUtils::tryGetDetailOf(domItem(method));
            QVERIFY(detail.has_value());
            QCOMPARE(detail.value(), expectedDetail);
        }
    }
    { // QmlObject
        QmlObject obj;
        auto detail = DocumentSymbolUtils::tryGetDetailOf(domItem(obj));
        QCOMPARE(detail, std::nullopt);

        const QString objId("objId");
        obj.setIdStr(objId);
        detail = DocumentSymbolUtils::tryGetDetailOf(domItem(obj));
        QCOMPARE(detail, objId.toUtf8());

        /*
         * Unfortunately because of the way DomItem::component() and filterUp() are working
         * (using full path from the root(env/top) and non-trivially trimming it)
         * I can't properly construct a fake hierarchy of components and objects
         * to verify the "root" case.
         */
    }
}

static std::pair<QList<QLspSpecification::DocumentSymbol>, QList<QLspSpecification::DocumentSymbol>>
reorganizeForOutlineViewTestData()
{
    /* The following fake QmlFile is used as testing data:
     * 0|12345678901234567890123456789012345678901234
     * 1|Item1 { Enum{}
     * 2|component c.1{Item2{}}
     * 3|Item3{Item4{ id: item4; component c.2{} } }
     *
     * inside DOM, SourceLocations, hence Ranges, of all objects/components, etc
     * are adhearing "structural" view, meaning that even though they are not
     * in "structural" hierarchy inside DOM, their Locations following "structural" hierarchy
     * order of children will be fixed/sorted by the client
     */

    using QLspSpecification::DocumentSymbol;
    using QLspSpecification::SymbolKind;
    // Construct Document Symbols, corresponding to the fakeQmlFile, that needs to be reorganized
    DocumentSymbol enumSymbol;
    enumSymbol.name = "Enum";
    enumSymbol.kind = DocumentSymbolUtils::symbolKindOf(domItem(EnumDecl()));
    enumSymbol.range = { { 1, 9 }, { 1, 14 } };

    DocumentSymbol item4Id;
    item4Id.name = "item4";
    item4Id.kind = DocumentSymbolUtils::symbolKindOf(domItem(Id()));
    item4Id.range = { { 3, 14 }, { 3, 23 } };

    DocumentSymbol item4;
    item4.range = { { 3, 7 }, { 3, 41 } };
    item4.kind = DocumentSymbolUtils::symbolKindOf(domItem(QmlObject()));
    item4.name = "Item4";
    item4.children.emplace({ item4Id });

    DocumentSymbol item3;
    item3.range = { { 3, 1 }, { 3, 43 } };
    item3.kind = DocumentSymbolUtils::symbolKindOf(domItem(QmlObject()));
    item3.name = "Item3";
    item3.children.emplace({ item4 });

    DocumentSymbol item1;
    item1.range = { { 1, 1 }, { 3, 43 } };
    item1.kind = DocumentSymbolUtils::symbolKindOf(domItem(QmlObject()));
    item1.name = "Item1";
    item1.children.emplace({ item3 });

    DocumentSymbol mainCmp;
    mainCmp.range = { { 1, 1 }, { 3, 43 } };
    mainCmp.kind = DocumentSymbolUtils::symbolKindOf(domItem(QmlComponent()));
    mainCmp.name = "mainCmp";
    // Create and add an fictional Id just to verify that it's not touched by the reorganisation
    auto fictionalId(item4Id);
    mainCmp.children.emplace({ enumSymbol, fictionalId, item1 });

    DocumentSymbol item2;
    item2.range = { { 2, 15 }, { 3, 21 } };
    item2.kind = DocumentSymbolUtils::symbolKindOf(domItem(QmlObject()));
    item2.name = "Item2";

    DocumentSymbol cmpC1;
    cmpC1.range = { { 2, 1 }, { 3, 22 } };
    cmpC1.kind = DocumentSymbolUtils::symbolKindOf(domItem(QmlComponent()));
    cmpC1.name = "c.1";
    cmpC1.children.emplace({ item2 });

    DocumentSymbol cmpC2;
    cmpC2.range = { { 3, 25 }, { 3, 39 } };
    cmpC2.kind = DocumentSymbolUtils::symbolKindOf(domItem(QmlComponent()));
    cmpC2.name = "c.2";

    DocumentSymbol qmlFile;
    qmlFile.range = { { 1, 1 }, { 3, 43 } };
    qmlFile.kind = DocumentSymbolUtils::symbolKindOf(domItem(QmlFile()));
    qmlFile.name = "fakeQmlFile";
    qmlFile.children.emplace({ mainCmp, cmpC1, cmpC2 });

    QList<DocumentSymbol> fakeQmlFileSymbols{ qmlFile };
    //-----------------------------------------------------------------------
    // during reorganization cmpC2 should be added as a child
    DocumentSymbol reorganizedItem4(item4);
    reorganizedItem4.children->push_back(cmpC2);

    // should contain reorganizedItem4
    DocumentSymbol reorganizedItem3(item3);
    reorganizedItem3.children = { reorganizedItem4 };

    // should contain reorganizedItem3, enum, cmpC1 as children
    DocumentSymbol reorganizedItem1(item1);
    reorganizedItem1.children = { reorganizedItem3, enumSymbol, cmpC1 };

    // verify fictionalId stays where it was added, not touched by the reorg
    QList<DocumentSymbol> reorganizedFakeQmlFileSymbols{ fictionalId, reorganizedItem1 };
    return std::make_pair(fakeQmlFileSymbols, reorganizedFakeQmlFileSymbols);
}

void tst_document_symbol_utils::reorganizeForOutlineView()
{
    auto [fakeQmlFileSymbols, expectedReorganizedFakeQmlFileSymbols] =
            reorganizeForOutlineViewTestData();
    DocumentSymbolUtils::reorganizeForOutlineView(fakeQmlFileSymbols);
    QVERIFY(compareDocumentSymbolsLists(fakeQmlFileSymbols, expectedReorganizedFakeQmlFileSymbols));
}

QTEST_MAIN(tst_document_symbol_utils)
