// 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 "testtreemodel.h"

#include "autotestconstants.h"
#include "autotestplugin.h"
#include "autotesttr.h"
#include "testcodeparser.h"
#include "testconfiguration.h"
#include "testframeworkmanager.h"
#include "testprojectsettings.h"
#include "testrunner.h"

#include <coreplugin/messagemanager.h>

#include <cppeditor/cppmodelmanager.h>

#include <extensionsystem/pluginmanager.h>

#include <projectexplorer/buildconfiguration.h>
#include <projectexplorer/buildmanager.h>
#include <projectexplorer/buildsystem.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>

#include <qmljs/qmljsmodelmanagerinterface.h>

#include <utils/algorithm.h>
#include <utils/qtcassert.h>

using namespace Autotest::Internal;
using namespace ProjectExplorer;
using namespace Utils;

namespace Autotest {

static Q_LOGGING_CATEGORY(LOG, "qtc.autotest.frameworkmanager", QtWarningMsg)

static TestTreeModel *s_instance = nullptr;

TestTreeModel::TestTreeModel(TestCodeParser *parser)
    : m_parser(parser)
{
    s_instance = this;

    connect(m_parser, &TestCodeParser::aboutToPerformFullParse, this,
            &TestTreeModel::removeAllTestItems, Qt::QueuedConnection);
    connect(m_parser, &TestCodeParser::testParseResultsReady,
            this, &TestTreeModel::onParseResultsReady);
    connect(m_parser, &TestCodeParser::parsingFinished,
            this, &TestTreeModel::sweep, Qt::QueuedConnection);
    connect(m_parser, &TestCodeParser::parsingFailed,
            this, &TestTreeModel::sweep, Qt::QueuedConnection);
    connect(m_parser, &TestCodeParser::requestRemoveAllFrameworkItems,
            this, &TestTreeModel::markAllFrameworkItemsForRemoval);
    connect(m_parser, &TestCodeParser::requestRemoval,
            this, &TestTreeModel::markForRemoval);
    connect(this, &QAbstractItemModel::dataChanged,
            this, &TestTreeModel::onDataChanged);
    setupParsingConnections();
}

TestTreeModel *TestTreeModel::instance()
{
    return s_instance;
}

TestTreeModel::~TestTreeModel()
{
    s_instance = nullptr;
}

void TestTreeModel::setupParsingConnections()
{
    static bool connectionsInitialized = false;
    if (connectionsInitialized)
        return;
    m_parser->setDirty();
    m_parser->setState(TestCodeParser::Idle);

    connect(ProjectManager::instance(), &ProjectManager::startupProjectChanged, this,
            [this](Project *project) {
        synchronizeTestFrameworks(); // we might have project settings
        m_parser->onStartupProjectChanged(project);
        removeAllTestToolItems();
        synchronizeTestTools();
        m_checkStateCache = project ? projectSettings(project)->checkStateCache() : nullptr;
        onBuildSystemTestsUpdated(); // we may have old results if project was open before switching
        m_failedStateCache.clear();
        if (project) {
            if (auto buildSystem = activeBuildSystemForActiveProject()) {
                connect(buildSystem, &BuildSystem::testInformationUpdated,
                        this, &TestTreeModel::onBuildSystemTestsUpdated, Qt::UniqueConnection);
                connect(buildSystem, &BuildSystem::testRunRequested,
                        this, &TestTreeModel::onTestRunRequested, Qt::UniqueConnection);
            } else {
                connect(project, &Project::activeBuildConfigurationChanged,
                        this, &TestTreeModel::onBuildConfigChanged);
            }
        }
    });

    CppEditor::CppModelManager *cppMM = CppEditor::CppModelManager::instance();
    connect(cppMM, &CppEditor::CppModelManager::documentUpdated,
            m_parser, &TestCodeParser::onCppDocumentUpdated, Qt::QueuedConnection);
    connect(cppMM, &CppEditor::CppModelManager::aboutToRemoveFiles,
            this, [this](const FilePaths &filePaths) {
                markForRemoval(Utils::toSet(filePaths));
                sweep();
            }, Qt::QueuedConnection);
    connect(cppMM, &CppEditor::CppModelManager::projectPartsUpdated,
            m_parser, &TestCodeParser::onProjectPartsUpdated);

    QmlJS::ModelManagerInterface *qmlJsMM = QmlJS::ModelManagerInterface::instance();
    connect(qmlJsMM, &QmlJS::ModelManagerInterface::documentUpdated,
            m_parser, &TestCodeParser::onQmlDocumentUpdated, Qt::QueuedConnection);
    connect(qmlJsMM, &QmlJS::ModelManagerInterface::aboutToRemoveFiles,
            this, [this](const FilePaths &filePaths) {
                markForRemoval(Utils::toSet(filePaths));
                sweep();
            }, Qt::QueuedConnection);
    connectionsInitialized = true;
}

bool TestTreeModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (!index.isValid())
        return false;

    ITestTreeItem *item = static_cast<ITestTreeItem *>(index.internalPointer());
    if (item && item->setData(index.column(), value, role)) {
        emit dataChanged(index, index, {role});
        if (role == Qt::CheckStateRole) {
            Qt::CheckState checked = item->checked();
            if (item->hasChildren() && checked != Qt::PartiallyChecked) {
                // handle the new checkstate for children as well...
                for (TreeItem *child : *item) {
                    const QModelIndex &idx = indexForItem(child);
                    setData(idx, checked ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole);
                }
            }
            if (item->parent() != rootItem()) {
                auto parent = static_cast<ITestTreeItem *>(item->parent());
                if (parent->checked() != checked)
                    revalidateCheckState(parent); // handle parent too
            }
            return true;
        } else if (role == FailedRole) {
            if (item->testBase()->type() == ITestBase::Framework)
                m_failedStateCache.insert(static_cast<TestTreeItem *>(item), true);
        }
    }
    return false;
}

Qt::ItemFlags TestTreeModel::flags(const QModelIndex &index) const
{
    if (!index.isValid())
        return Qt::ItemIsEnabled | Qt::ItemIsSelectable;

    ITestTreeItem *item = static_cast<ITestTreeItem *>(itemForIndex(index));
    return item->flags(index.column());
}

bool TestTreeModel::hasTests() const
{
    for (TreeItem *frameworkRoot : *rootItem()) {
        if (frameworkRoot->hasChildren())
            return true;
    }
    return false;
}

QList<ITestConfiguration *> TestTreeModel::getAllTestCases() const
{
    QList<ITestConfiguration *> result;
    forItemsAtLevel<1>([&result](ITestTreeItem *testRoot) {
        result.append(testRoot->getAllTestConfigurations());
    });
    return result;
}

QList<ITestConfiguration *> TestTreeModel::getSelectedTests() const
{
    QList<ITestConfiguration *> result;
    forItemsAtLevel<1>([&result](ITestTreeItem *testRoot) {
        result.append(testRoot->getSelectedTestConfigurations());
    });
    return result;
}

QList<ITestConfiguration *> TestTreeModel::getFailedTests() const
{
    QList<ITestConfiguration *> result;
    forItemsAtLevel<1>([&result](ITestTreeItem *testRoot) {
        result.append(testRoot->getFailedTestConfigurations());
    });
    return result;
}

QList<ITestConfiguration *> TestTreeModel::getTestsForFile(const FilePath &fileName) const
{
    QList<ITestConfiguration *> result;
    forItemsAtLevel<1>([&result, &fileName](ITestTreeItem *testRoot) {
        if (testRoot->testBase()->type() == ITestBase::Framework)
            result.append(static_cast<TestTreeItem *>(testRoot)->getTestConfigurationsForFile(fileName));
    });
    return result;
}

static QList<ITestTreeItem *> testItemsByName(TestTreeItem *root, const QString &testName)
{
    QList<ITestTreeItem *> result;

    root->forFirstLevelChildItems([&testName, &result](TestTreeItem *node) {
        if (node->type() == TestTreeItem::TestSuite || node->type() == TestTreeItem::TestCase) {
            if (node->name() == testName) {
                result << node;
                return; // prioritize test suites and cases over test functions
            }
            TestTreeItem *testCase = node->findFirstLevelChildItem([&testName](TestTreeItem *it) {
                QTC_ASSERT(it, return false);
                return (it->type() == TestTreeItem::TestCase
                        || it->type() == TestTreeItem::TestFunction) && it->name() == testName;
            }); // collect only actual tests, not special functions like init, cleanup etc.
            if (testCase)
                result << testCase;
        } else {
            result << testItemsByName(node, testName);
        }
    });
    return result;
}

void TestTreeModel::onBuildConfigChanged(BuildConfiguration *bc)
{
    if (bc) {
        connect(bc->buildSystem(), &BuildSystem::testInformationUpdated,
                this, &TestTreeModel::onBuildSystemTestsUpdated, Qt::UniqueConnection);
        connect(bc->buildSystem(), &BuildSystem::testRunRequested,
                this, &TestTreeModel::onTestRunRequested, Qt::UniqueConnection);
        disconnect(bc->project(), &Project::activeBuildConfigurationChanged,
                   this, &TestTreeModel::onBuildConfigChanged);
    }
}

void TestTreeModel::onBuildSystemTestsUpdated()
{
    const BuildSystem *bs = activeBuildSystemForActiveProject();
    if (!bs || !bs->project())
        return;

    QTC_ASSERT(m_checkStateCache, return);
    m_checkStateCache->evolve(ITestBase::Tool);

    ITestTool *testTool = TestFrameworkManager::testToolForBuildSystemId(bs->project()->type());
    if (!testTool)
        return;
    // FIXME
    const TestProjectSettings *projectSettings = Internal::projectSettings(bs->project());
    if ((projectSettings->useGlobalSettings() && !testTool->active())
            || !projectSettings->activeTestTools().contains(testTool)) {
        return;
    }

    ITestTreeItem *rootNode = testTool->rootNode();
    QTC_ASSERT(rootNode, return);
    rootNode->removeChildren();
    for (const auto &tci : bs->testcasesInfo()) {
        ITestTreeItem *item = testTool->createItemFromTestCaseInfo(tci);
        QTC_ASSERT(item, continue);
        if (std::optional<Qt::CheckState> cached = m_checkStateCache->get(item))
            item->setData(0, cached.value(), Qt::CheckStateRole);
        m_checkStateCache->insert(item, item->checked());
        rootNode->appendChild(item);
    }
    revalidateCheckState(rootNode);
    emit testTreeModelChanged();
}

void TestTreeModel::onTestRunRequested(const TestCaseInfo &testInfo,
                                       const QStringList &additionalOptions,
                                       const TestCaseEnvironment &testEnvironment)
{
    // this should be avoided already on the caller side, but we need to ensure this anyway
    if (BuildManager::isBuilding()  || TestRunner::instance()->isTestRunning()) {
        Core::MessageManager::writeFlashing(
                    Tr::tr("Test run requests from the build system get processed only if there "
                           "is no running build or test run."));
        return;
    }

    BuildSystem *bs = qobject_cast<BuildSystem *>(sender());
    QTC_ASSERT(bs, return);
    QTC_ASSERT(bs->project(), return);
    ITestTool *testTool = TestFrameworkManager::testToolForBuildSystemId(bs->project()->type());
    if (!testTool)
        return;

    ITestTreeItem *tmpItem = testTool->createItemFromTestCaseInfo(testInfo);
    QTC_ASSERT(tmpItem, return);
    ITestConfiguration *config = tmpItem->testConfiguration();
    delete tmpItem;
    QTC_ASSERT(config, return);
    // apply options, working directory and environment changes requested by the build system
    if (!testEnvironment.workingDirectory.isEmpty())
        config->setWorkingDirectory(testEnvironment.workingDirectory);
    if (!additionalOptions.isEmpty())
        config->runnable().command.addArgs(additionalOptions);
    config->setEnvironment(
                testEnvironment.environment.appliedToEnvironment(config->runnable().environment));

    connect(
        TestRunner::instance(), &TestRunner::testRunFinished,
        this, [testEnvironment] {
            if (testEnvironment.onTestsRunFinished)
                testEnvironment.onTestsRunFinished();
        },
        Qt::SingleShotConnection
    );

    TestRunner::instance()->runTests(TestRunMode::Run, {config});
}

const QList<TestTreeItem *> TestTreeModel::frameworkRootNodes() const
{
    QList<TestTreeItem *> result;
    forItemsAtLevel<1>([&result](ITestTreeItem *rootNode) {
        if (auto framework = rootNode->testBase()->asFramework())
            result.append(framework->rootNode());
    });
    return result;
}

const QList<ITestTreeItem *> TestTreeModel::testToolRootNodes() const
{
    QList<ITestTreeItem *> result;
    forItemsAtLevel<1>([&result](ITestTreeItem *rootNode) {
        if (auto testTool = rootNode->testBase()->asTestTool())
            result.append(testTool->rootNode());
    });
    return result;
}

QList<ITestTreeItem *> TestTreeModel::testItemsByName(const QString &testName)
{
    QList<ITestTreeItem *> result;
    for (TestTreeItem *frameworkRoot : frameworkRootNodes())
        result << Autotest::testItemsByName(frameworkRoot, testName);

    return result;
}

void TestTreeModel::synchronizeTestFrameworks()
{
    // we may get triggered by the timer during shutdown - avoid crash in that case
    if (ExtensionSystem::PluginManager::isShuttingDown())
        return;

    const TestFrameworks sorted = activeTestFrameworks();
    qCDebug(LOG) << "Active frameworks sorted by priority" << Utils::transform(sorted, &ITestBase::displayName);
    const auto sortedParsers = Utils::transform(sorted, &ITestFramework::testParser);
    // pre-check to avoid further processing when frameworks are unchanged
    TreeItem *invisibleRoot = rootItem();
    QSet<ITestParser *> newlyAdded;
    QList<ITestTreeItem *> oldFrameworkRoots;
    for (TreeItem *oldFrameworkRoot : *invisibleRoot)
        oldFrameworkRoots.append(static_cast<ITestTreeItem *>(oldFrameworkRoot));

    for (ITestTreeItem *oldFrameworkRoot : oldFrameworkRoots)
        takeItem(oldFrameworkRoot);  // do NOT delete the ptr is still held by TestFrameworkManager

    for (ITestParser *parser : sortedParsers) {
        TestTreeItem *frameworkRootNode = parser->framework()->rootNode();
        invisibleRoot->appendChild(frameworkRootNode);
        if (!oldFrameworkRoots.removeOne(frameworkRootNode))
            newlyAdded.insert(parser);
    }
    for (ITestTreeItem *oldFrameworkRoot : oldFrameworkRoots) {
        if (oldFrameworkRoot->testBase()->type() == ITestBase::Framework)
            oldFrameworkRoot->removeChildren();
        else // re-add the test tools - they are handled separately
            invisibleRoot->appendChild(oldFrameworkRoot);
    }

    m_parser->syncTestFrameworks(sortedParsers);
    if (!newlyAdded.isEmpty())
        m_parser->updateTestTree(newlyAdded);
    emit updatedActiveFrameworks(invisibleRoot->childCount());
}

void TestTreeModel::synchronizeTestTools()
{
    ProjectExplorer::Project *project = ProjectExplorer::ProjectManager::startupProject();
    TestTools tools;
    if (!project || Internal::projectSettings(project)->useGlobalSettings()) {
        tools = Utils::filtered(TestFrameworkManager::registeredTestTools(),
                                &ITestFramework::active);
        qCDebug(LOG) << "Active test tools" << Utils::transform(tools, &ITestBase::displayName); // FIXME tools aren't sorted
    } else { // we've got custom project settings
        const TestProjectSettings *settings = Internal::projectSettings(project);
        const QHash<ITestTool *, bool> active = settings->activeTestTools();
        tools = Utils::filtered(TestFrameworkManager::registeredTestTools(),
                                [active](ITestTool *testTool) {
            return active.value(testTool, false);
        });
    }

    // pre-check to avoid further processing when test tools are unchanged
    TreeItem *invisibleRoot = rootItem();
    QSet<ITestTool *> newlyAdded;
    QList<ITestTreeItem *> oldFrameworkRoots;
    for (TreeItem *oldFrameworkRoot : *invisibleRoot) {
        auto item = static_cast<ITestTreeItem *>(oldFrameworkRoot);
        if (item->testBase()->type() == ITestBase::Tool)
            oldFrameworkRoots.append(item);
    }

    for (ITestTreeItem *oldFrameworkRoot : oldFrameworkRoots)
        takeItem(oldFrameworkRoot);  // do NOT delete the ptr is still held by TestFrameworkManager

    for (ITestTool *testTool : std::as_const(tools)) {
        ITestTreeItem *testToolRootNode = testTool->rootNode();
        invisibleRoot->appendChild(testToolRootNode);
        if (!oldFrameworkRoots.removeOne(testToolRootNode))
            newlyAdded.insert(testTool);
    }

    if (project) {
        BuildConfiguration * const bc = project->activeBuildConfiguration();
        if (bc) {
            auto bs = bc->buildSystem();
            for (ITestTool *testTool : newlyAdded) {
                ITestTreeItem *rootNode = testTool->rootNode();
                QTC_ASSERT(rootNode, return);
                rootNode->removeChildren();
                for (const auto &tci : bs->testcasesInfo()) {
                    ITestTreeItem *item = testTool->createItemFromTestCaseInfo(tci);
                    QTC_ASSERT(item, continue);
                    if (std::optional<Qt::CheckState> cached = m_checkStateCache->get(item))
                        item->setData(0, cached.value(), Qt::CheckStateRole);
                    m_checkStateCache->insert(item, item->checked());
                    rootNode->appendChild(item);
                }
                revalidateCheckState(rootNode);
            }
        }
    }
    emit updatedActiveFrameworks(invisibleRoot->childCount());
}

void TestTreeModel::filterAndInsert(TestTreeItem *item, TestTreeItem *root, bool groupingEnabled)
{
    TestTreeItem *filtered = item->applyFilters();
    if (item->shouldBeAddedAfterFiltering())
        insertItemInParent(item, root, groupingEnabled);
    else // might be that all children have been filtered out
        delete item;
    if (filtered)
        insertItemInParent(filtered, root, groupingEnabled);
}

void TestTreeModel::rebuild(const QList<Id> &frameworkIds)
{
    for (const Id &id : frameworkIds) {
        ITestFramework *framework = TestFrameworkManager::frameworkForId(id);
        TestTreeItem *frameworkRoot = framework->rootNode();
        const bool groupingEnabled = framework->grouping();
        for (int row = frameworkRoot->childCount() - 1; row >= 0; --row) {
            auto testItem = frameworkRoot->childItem(row);
            if (testItem->type() == TestTreeItem::GroupNode) {
                // process children of group node and delete it afterwards if necessary
                for (int childRow = testItem->childCount() - 1; childRow >= 0; --childRow) {
                    // FIXME should this be done recursively until we have a non-GroupNode?
                    TestTreeItem *childTestItem = testItem->childItem(childRow);
                    takeItem(childTestItem);
                    filterAndInsert(childTestItem, frameworkRoot, groupingEnabled);
                }
                if (!groupingEnabled || testItem->childCount() == 0)
                    delete takeItem(testItem);
            } else {
                takeItem(testItem);
                filterAndInsert(testItem, frameworkRoot, groupingEnabled);
            }
        }
        revalidateCheckState(frameworkRoot);
    }
}

void TestTreeModel::updateCheckStateCache()
{
    m_checkStateCache->evolve(ITestBase::Framework);

    for (TestTreeItem *rootNode : frameworkRootNodes()) {
        rootNode->forAllChildItems([this](TestTreeItem *childItem) {
            m_checkStateCache->insert(childItem, childItem->checked());
        });
    }
}

bool TestTreeModel::hasFailedTests() const
{
    auto failedItem = rootItem()->findAnyChild([](TreeItem *it) {
        return it->data(0, FailedRole).toBool();
    });
    return failedItem != nullptr;
}

void TestTreeModel::clearFailedMarks()
{
    for (TreeItem *rootNode : *rootItem()) {
        rootNode->forAllChildren([](TreeItem *child) {
            child->setData(0, false, FailedRole);
        });
    }
    m_failedStateCache.clear();
}

void TestTreeModel::markAllFrameworkItemsForRemoval()
{
    for (TestTreeItem *frameworkRoot : frameworkRootNodes()) {
        frameworkRoot->forFirstLevelChildItems([](TestTreeItem *child) {
            child->markForRemovalRecursively(true);
        });
    }
}

void TestTreeModel::markForRemoval(const QSet<Utils::FilePath> &filePaths)
{
    for (TestTreeItem *frameworkRoot : frameworkRootNodes()) {
        for (int childRow = frameworkRoot->childCount() - 1; childRow >= 0; --childRow) {
            TestTreeItem *child = frameworkRoot->childItem(childRow);
            child->markForRemovalRecursively(filePaths);
        }
    }
}

void TestTreeModel::sweep()
{
    for (TestTreeItem *frameworkRoot : frameworkRootNodes()) {
        if (frameworkRoot->m_status == TestTreeItem::ForcedRootRemoval) {
            frameworkRoot->framework()->resetRootNode();
            continue;
        }
        sweepChildren(frameworkRoot);
        revalidateCheckState(frameworkRoot);
    }
    // even if nothing has changed by the sweeping we might had parse which added or modified items
    emit testTreeModelChanged();
#ifdef WITH_TESTS
    if (m_parser->state() == TestCodeParser::Idle && !m_parser->furtherParsingExpected())
        emit sweepingDone();
#endif
}

QString TestTreeModel::report(bool full) const
{
    QString result;
    int items = 0;
    QString tree;
    for (TestTreeItem *rootNode : frameworkRootNodes()) {
        int itemsPerRoot = 0;
        result.append("\n");
        result += rootNode->name();
        result.append(" > ");

        if (full) {
            TestTreeSortFilterModel sortFilterModel;
            sortFilterModel.sort(0);
            tree = "\n" + sortFilterModel.report();
            rootNode->forAllChildren([&itemsPerRoot](TreeItem *) {
                ++itemsPerRoot;
            });

        } else {
            rootNode->forAllChildren([&itemsPerRoot](TreeItem *) {
                ++itemsPerRoot;
            });
        }
        result.append(QString::number(itemsPerRoot));
        items += itemsPerRoot;
    }
    result.append("\nItems: " + QString::number(items));
    if (full)
        return tree + '\n' + result;
    return result;
}

/**
 * @note after calling this function emit testTreeModelChanged() if it returns true
 */
bool TestTreeModel::sweepChildren(TestTreeItem *item)
{
    bool hasChanged = false;
    for (int row = item->childCount() - 1; row >= 0; --row) {
        TestTreeItem *child = item->childItem(row);

        if (child->type() != TestTreeItem::Root && child->markedForRemoval()) {
            destroyItem(child);
            revalidateCheckState(item);
            hasChanged = true;
        } else if (child->hasChildren()) {
            hasChanged |= sweepChildren(child);
            if (!child->hasChildren() && child->removeOnSweepIfEmpty()) {
                destroyItem(child);
                revalidateCheckState(item);
            }
        } else {
            hasChanged |= child->newlyAdded();
        }
    }
    return hasChanged;
}

static TestTreeItem *fullCopyOf(TestTreeItem *other)
{
    QTC_ASSERT(other, return nullptr);
    TestTreeItem *result = other->copyWithoutChildren();
    for (int row = 0, count = other->childCount(); row < count; ++row)
        result->appendChild(fullCopyOf(other->childItem(row)));
    return result;
}

static void applyParentCheckState(ITestTreeItem *parent, ITestTreeItem *newItem)
{
    QTC_ASSERT(parent && newItem, return);

    if (parent->checked() != newItem->checked()) {
        const Qt::CheckState checkState = parent->checked() == Qt::Unchecked ? Qt::Unchecked
                                                                             : Qt::Checked;
        newItem->setData(0, checkState, Qt::CheckStateRole);
        newItem->forAllChildren([checkState](TreeItem *it) {
            it->setData(0, checkState, Qt::CheckStateRole);
        });
    }
}

void TestTreeModel::insertItemInParent(TestTreeItem *item, TestTreeItem *root, bool groupingEnabled)
{
    TestTreeItem *parentNode = root;
    if (groupingEnabled && item->isGroupable()) {
        parentNode = root->findFirstLevelChildItem([item](const TestTreeItem *it) {
            return it->isGroupNodeFor(item);
        });
        if (!parentNode) {
            parentNode = item->createParentGroupNode();
            if (!QTC_GUARD(parentNode)) // we might not get a group node at all
                parentNode = root;
            else
                root->appendChild(parentNode);
        }
    }
    // check if a similar item is already present (can happen for rebuild())
    if (auto otherItem = parentNode->findChild(item)) {
        // only handle item's children and add them to the already present one
        for (int row = 0, count = item->childCount(); row < count; ++row) {
            TestTreeItem *child = fullCopyOf(item->childItem(row));
            // use check state of the original
            child->setData(0, item->childAt(row)->checked(), Qt::CheckStateRole);
            otherItem->appendChild(child);
            revalidateCheckState(child);
        }
        delete item;
    } else {
        // restore former check state if available
        std::optional<Qt::CheckState> cached = m_checkStateCache ? m_checkStateCache->get(item)
                                                                 : std::optional<Qt::CheckState>{};
        if (cached.has_value())
            item->setData(0, cached.value(), Qt::CheckStateRole);
        else
            applyParentCheckState(parentNode, item);
        // ..and the failed state if available
        std::optional<bool> failed = m_failedStateCache.get(item);
        if (failed.has_value())
            item->setData(0, *failed, FailedRole);
        parentNode->appendChild(item);
        revalidateCheckState(parentNode);
    }
}

static Qt::CheckState computeCheckStateByChildren(ITestTreeItem *item)
{
    Qt::CheckState newState = Qt::Checked;
    bool foundChecked = false;
    bool foundUnchecked = false;
    bool foundPartiallyChecked = false;

    item->forFirstLevelChildren([&](ITestTreeItem *child) {
        switch (child->type()) {
        case TestTreeItem::TestDataFunction:
        case TestTreeItem::TestSpecialFunction:
            return;
        default:
            break;
        }

        foundChecked |= (child->checked() == Qt::Checked);
        foundUnchecked |= (child->checked() == Qt::Unchecked);
        foundPartiallyChecked |= (child->checked() == Qt::PartiallyChecked);

        if (foundPartiallyChecked || (foundChecked && foundUnchecked)) {
            newState = Qt::PartiallyChecked;
            return;
        }
    });

    if (newState != Qt::PartiallyChecked)
        newState = foundUnchecked ? Qt::Unchecked : Qt::Checked;
    return newState;
}

void TestTreeModel::revalidateCheckState(ITestTreeItem *item)
{
    QTC_ASSERT(item, return);

    const ITestTreeItem::Type type = item->type();
    if (type == ITestTreeItem::TestSpecialFunction || type == ITestTreeItem::TestDataFunction
            || type == ITestTreeItem::TestDataTag) {
        return;
    }
    const Qt::CheckState oldState = Qt::CheckState(item->data(0, Qt::CheckStateRole).toInt());
    Qt::CheckState newState = computeCheckStateByChildren(item);
    if (oldState != newState) {
        item->setData(0, newState, Qt::CheckStateRole);
        emit dataChanged(item->index(), item->index(), {Qt::CheckStateRole});
        if (item->parent() != rootItem()) {
            auto parent = static_cast<ITestTreeItem *>(item->parent());
            if (parent->checked() != newState)
                revalidateCheckState(parent);
        }
    }
}

void TestTreeModel::onParseResultsReady(const QList<TestParseResultPtr> &results)
{
    for (const auto &result : results) {
        ITestFramework *framework = result->framework;
        QTC_ASSERT(framework, return);
        TestTreeItem *rootNode = framework->rootNode();
        QTC_ASSERT(rootNode, return);
        handleParseResult(result.get(), rootNode);
    }
}

void Autotest::TestTreeModel::onDataChanged(const QModelIndex &topLeft,
                                            const QModelIndex &bottomRight,
                                            const QList<int> &roles)
{
    const QModelIndex parent = topLeft.parent();
    QTC_ASSERT(parent == bottomRight.parent(), return);
    if (!roles.isEmpty() && !roles.contains(Qt::CheckStateRole))
        return;

    if (!m_checkStateCache) // dataChanged() may be triggered by closing a project
        return;
    for (int row = topLeft.row(), endRow = bottomRight.row(); row <= endRow; ++row) {
        if (auto item = static_cast<ITestTreeItem *>(itemForIndex(index(row, 0, parent))))
            m_checkStateCache->insert(item, item->checked());
    }
}

void TestTreeModel::handleParseResult(const TestParseResult *result, TestTreeItem *parentNode)
{
    const bool groupingEnabled = result->framework->grouping();
    // lookup existing items
    if (TestTreeItem *toBeModified = parentNode->find(result)) {
        // found existing item... Do not remove
        toBeModified->markForRemoval(false);
        // if it's a reparse we need to mark the group node as well to avoid purging it in sweep()
        if (groupingEnabled) {
            if (auto directParent = toBeModified->parentItem()) {
                if (directParent->type() == TestTreeItem::GroupNode)
                    directParent->markForRemoval(false);
            }
        }
        // modify and when content has changed inform ui
        if (toBeModified->modify(result)) {
            const QModelIndex &idx = indexForItem(toBeModified);
            emit dataChanged(idx, idx);
        }
        // recursively handle children of this item
        for (const TestParseResult *child : result->children)
            handleParseResult(child, toBeModified);
        return;
    }
    // if there's no matching item, add the new one
    TestTreeItem *newItem = result->createTestTreeItem();
    QTC_ASSERT(newItem, return);

    // restore former check state and fail state if available
    newItem->forAllChildItems([this](TestTreeItem *childItem) {
        if (!m_checkStateCache) // parse results may arrive after session switch / project close
            return;
        std::optional<Qt::CheckState> cached = m_checkStateCache->get(childItem);
        if (cached.has_value())
            childItem->setData(0, cached.value(), Qt::CheckStateRole);
        std::optional<bool> failed = m_failedStateCache.get(childItem);
        if (failed.has_value())
            childItem->setData(0, *failed, FailedRole);
    });
    // it might be necessary to "split" created item
    filterAndInsert(newItem, parentNode, groupingEnabled);
}

void TestTreeModel::removeAllTestItems()
{
    for (TestTreeItem *item : frameworkRootNodes()) {
        item->removeChildren();
        if (item->checked() == Qt::PartiallyChecked)
            item->setData(0, Qt::Checked, Qt::CheckStateRole);
    }
    emit testTreeModelChanged();
}

void TestTreeModel::removeAllTestToolItems()
{
    for (ITestTreeItem *item : testToolRootNodes()) {
        item->removeChildren();
        if (item->checked() == Qt::PartiallyChecked)
            item->setData(0, Qt::Checked, Qt::CheckStateRole);
    }
    emit testTreeModelChanged();
}

#ifdef WITH_TESTS
// we're inside tests - so use some internal knowledge to make testing easier
static TestTreeItem *qtRootNode()
{
    const Id id = Id(Constants::FRAMEWORK_PREFIX).withSuffix("QtTest");
    return TestFrameworkManager::frameworkForId(id)->rootNode();
}

static TestTreeItem *quickRootNode()
{
    const Id id = Id(Constants::FRAMEWORK_PREFIX).withSuffix("QtQuickTest");
    return TestFrameworkManager::frameworkForId(id)->rootNode();
}

static TestTreeItem *gtestRootNode()
{
    const Id id = Id(Constants::FRAMEWORK_PREFIX).withSuffix("GTest");
    return TestFrameworkManager::frameworkForId(id)->rootNode();
}

static TestTreeItem *boostTestRootNode()
{
    const Id id = Id(Constants::FRAMEWORK_PREFIX).withSuffix("Boost");
    return TestFrameworkManager::frameworkForId(id)->rootNode();
}

int TestTreeModel::autoTestsCount() const
{
    TestTreeItem *rootNode = qtRootNode();
    return rootNode ? rootNode->childCount() : 0;
}

bool TestTreeModel::hasUnnamedQuickTests(const ITestTreeItem* rootNode) const
{
    for (int row = 0, count = rootNode->childCount(); row < count; ++row) {
        if (rootNode->childAt(row)->name().isEmpty())
            return true;
    }
    return false;
}

ITestTreeItem *TestTreeModel::unnamedQuickTests() const
{
    TestTreeItem *rootNode = quickRootNode();
    if (!rootNode)
        return nullptr;

    return rootNode->findFirstLevelChildItem([](TestTreeItem *it) { return it->name().isEmpty(); });
}

int TestTreeModel::namedQuickTestsCount() const
{
    TestTreeItem *rootNode = quickRootNode();
    return rootNode
            ? rootNode->childCount() - (hasUnnamedQuickTests(rootNode) ? 1 : 0)
            : 0;
}

int TestTreeModel::unnamedQuickTestsCount() const
{
    if (ITestTreeItem *unnamed = unnamedQuickTests())
        return unnamed->childCount();
    return 0;
}

int TestTreeModel::dataTagsCount() const
{
    TestTreeItem *rootNode = qtRootNode();
    if (!rootNode)
        return 0;

    int dataTagCount = 0;
    rootNode->forFirstLevelChildren([&dataTagCount](ITestTreeItem *classItem) {
        classItem->forFirstLevelChildren([&dataTagCount](ITestTreeItem *functionItem) {
            dataTagCount += functionItem->childCount();
        });
    });
    return dataTagCount;
}

int TestTreeModel::gtestNamesCount() const
{
    TestTreeItem *rootNode = gtestRootNode();
    return rootNode ? rootNode->childCount() : 0;
}

QMultiMap<QString, int> TestTreeModel::gtestNamesAndSets() const
{
    QMultiMap<QString, int> result;

    if (TestTreeItem *rootNode = gtestRootNode()) {
        rootNode->forFirstLevelChildren([&result](ITestTreeItem *child) {
            result.insert(child->name(), child->childCount());
        });
    }
    return result;
}

int TestTreeModel::boostTestNamesCount() const
{
    TestTreeItem *rootNode = boostTestRootNode();
    return rootNode ? rootNode->childCount() : 0;
}

QMap<QString, int> TestTreeModel::boostTestSuitesAndTests() const
{
    QMap<QString, int> result;

    if (TestTreeItem *rootNode = boostTestRootNode()) {
        rootNode->forFirstLevelChildItems([&result](TestTreeItem *child) {
            result.insert(child->name() + '|' + child->proFile().toUrlishString(), child->childCount());
        });
    }
    return result;
}

#endif

/***************************** Sort/Filter Model **********************************/

TestTreeSortFilterModel::TestTreeSortFilterModel()
{
    setSourceModel(TestTreeModel::instance());
    setDynamicSortFilter(true);
}

void TestTreeSortFilterModel::setSortMode(ITestTreeItem::SortMode sortMode)
{
    m_sortMode = sortMode;
    invalidate();
}

void TestTreeSortFilterModel::updateFilterString(const QString &filter)
{
    m_filterString = filter;
    invalidateFilter();
}

void TestTreeSortFilterModel::toggleFilter(FilterMode filterMode)
{
    m_filterMode = toFilterMode(m_filterMode ^ filterMode);
    invalidateFilter();
}

TestTreeSortFilterModel::FilterMode TestTreeSortFilterModel::toFilterMode(int f)
{
    switch (f) {
    case TestTreeSortFilterModel::ShowInitAndCleanup:
        return TestTreeSortFilterModel::ShowInitAndCleanup;
    case TestTreeSortFilterModel::ShowTestData:
        return TestTreeSortFilterModel::ShowTestData;
    case TestTreeSortFilterModel::ShowAll:
        return TestTreeSortFilterModel::ShowAll;
    case TestTreeSortFilterModel::FilterByText:
        return TestTreeSortFilterModel::FilterByText;
    default:
        return TestTreeSortFilterModel::Basic;
    }
}

static QString dumpIndex(const QModelIndex &idx, int level = 0)
{
    QString result;
    result.append(QString(level, ' '));
    result.append(idx.data().toString() + '\n');
    for (int row = 0, end = idx.model()->rowCount(idx); row < end; ++row)
        result.append(dumpIndex(idx.model()->index(row, 0, idx), level + 1));
    return result;
}

QString TestTreeSortFilterModel::report() const
{
    QString result;
    for (int row = 0, end = rowCount(); row < end; ++row) {
        auto idx = index(row, 0);
        result.append(dumpIndex(idx));
    }
    return result;
}

bool TestTreeSortFilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
{
    // root items keep the intended order
    const ITestTreeItem *leftItem = static_cast<ITestTreeItem *>(left.internalPointer());
    if (leftItem->type() == ITestTreeItem::Root)
        return left.row() > right.row();

    const ITestTreeItem *rightItem = static_cast<ITestTreeItem *>(right.internalPointer());
    return leftItem->lessThan(rightItem, m_sortMode);
}

bool TestTreeSortFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
    const QModelIndex index = sourceModel()->index(sourceRow, 0,sourceParent);
    if (!index.isValid())
        return false;

    const ITestTreeItem *item = static_cast<ITestTreeItem *>(index.internalPointer());

    bool accept = true;
    switch (item->type()) {
    case Autotest::ITestTreeItem::Root:
        return true;
    case ITestTreeItem::TestDataFunction:
        accept = m_filterMode & ShowTestData;
        break;
    case ITestTreeItem::TestSpecialFunction:
        accept = m_filterMode & ShowInitAndCleanup;
        break;
    default:
        break;
    }

    if (!accept)
        return false;
    if ((m_filterMode & FilterByText) == 0)
        return accept;
    const QString display = item->data(0, Qt::DisplayRole).toString();
    return m_filterString.isEmpty() || display.contains(m_filterString, Qt::CaseInsensitive);
}

} // namespace Autotest
