// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <qtest.h>
#include <QtTest/QSignalSpy>
#include <QtQml/qqmlcomponent.h>
#include <QtQml/qqmlcontext.h>
#include <QtQuick/qquickview.h>
#include <QtQuick/qquickitem.h>
#include <QtQuickTestUtils/private/qmlutils_p.h>
#include <QtGui/QWindow>
#include <QtCore/QDebug>
#include <QtQml/qqmlengine.h>
#include <private/qv4engine_p.h>
#include <private/qv4mm_p.h>

#include <QtQuickTestUtils/private/geometrytestutils_p.h>

using namespace Qt::StringLiterals;

class tst_QQuickView : public QQmlDataTest
{
    Q_OBJECT
public:
    tst_QQuickView();

private slots:
    void gc();
    void resizemodeitem();
    void errors();
    void engine();
    void findChild();
    void setInitialProperties();
    void fromModuleCtor();
    void loadFromModule_data();
    void loadFromModule();
    void setSourceRelativeQrcPath();
    void overlay();
};


tst_QQuickView::tst_QQuickView()
    : QQmlDataTest(QT_QMLTEST_DATADIR)
{
}

void tst_QQuickView::gc()
{
    QQuickView view;
    QQmlEngine *engine = view.engine();
    QV4::ExecutionEngine *v4 = engine->handle();

    v4->memoryManager->gcStateMachine->deadline = QDeadlineTimer(QDeadlineTimer::Forever);
    auto sm = v4->memoryManager->gcStateMachine.get();
    sm->reset();
    while (sm->state != QV4::GCState::CallDestroyObjects) {
        QV4::GCStateInfo& stateInfo = sm->stateInfoMap[int(sm->state)];
        sm->state = stateInfo.execute(sm, sm->stateData);
    }
    view.loadFromModule("test", "TestQml");
    auto root = view.rootObject();
    QVERIFY(root);
    auto ddata = QQmlData::get(root, false);
    while (sm->state != QV4::GCState::DoSweep) {
        if (sm->state > QV4::GCState::InitCallDestroyObjects) {
            sm->mm->collectFromJSStack(sm->mm->markStack());
            sm->mm->m_markStack->drain();
        }
        QV4::GCStateInfo& stateInfo = sm->stateInfoMap[int(sm->state)];
        sm->state = stateInfo.execute(sm, sm->stateData);
    }
    QVERIFY(ddata);
    QVERIFY(ddata->jsWrapper.asManaged());
    QVERIFY(ddata->jsWrapper.asManaged()->markBit());
}

void tst_QQuickView::resizemodeitem()
{
    QWindow window;
    window.setGeometry(0, 0, 400, 400);

    QQuickView *view = new QQuickView(&window);
    QVERIFY(view);
    view->setResizeMode(QQuickView::SizeRootObjectToView);
    QCOMPARE(QSize(0,0), view->initialSize());
    view->setSource(testFileUrl("resizemodeitem.qml"));
    QQuickItem* item = qobject_cast<QQuickItem*>(view->rootObject());
    QVERIFY(item);
    window.show();

    view->showNormal();

    // initial size from root object
    QCOMPARE(item->width(), 200.0);
    QCOMPARE(item->height(), 200.0);
    QCOMPARE(view->size(), QSize(200, 200));
    QCOMPARE(view->size(), view->sizeHint());
    QCOMPARE(view->size(), view->initialSize());

    // size update from view
    view->resize(QSize(80,100));

    QTRY_COMPARE(item->width(), 80.0);
    QCOMPARE(item->height(), 100.0);
    QCOMPARE(view->size(), QSize(80, 100));
    QCOMPARE(view->size(), view->sizeHint());

    view->setResizeMode(QQuickView::SizeViewToRootObject);

    // size update from view disabled
    view->resize(QSize(60,80));
    QCOMPARE(item->width(), 80.0);
    QCOMPARE(item->height(), 100.0);
    QTest::qWait(50);
    QCOMPARE(view->size(), QSize(60, 80));

    // size update from root object
    item->setWidth(250);
    item->setHeight(350);
    QCOMPARE(item->width(), 250.0);
    QCOMPARE(item->height(), 350.0);
    QTRY_COMPARE(view->size(), QSize(250, 350));
    QCOMPARE(view->size(), QSize(250, 350));
    QCOMPARE(view->size(), view->sizeHint());

    // reset window
    window.hide();
    delete view;
    view = new QQuickView(&window);
    QVERIFY(view);
    view->setResizeMode(QQuickView::SizeViewToRootObject);
    view->setSource(testFileUrl("resizemodeitem.qml"));
    item = qobject_cast<QQuickItem*>(view->rootObject());
    QVERIFY(item);
    window.show();

    view->showNormal();

    // initial size for root object
    QCOMPARE(item->width(), 200.0);
    QCOMPARE(item->height(), 200.0);
    QCOMPARE(view->size(), view->sizeHint());
    QCOMPARE(view->size(), view->initialSize());

    // size update from root object
    item->setWidth(80);
    item->setHeight(100);
    QCOMPARE(item->width(), 80.0);
    QCOMPARE(item->height(), 100.0);
    QTRY_COMPARE(view->size(), QSize(80, 100));
    QCOMPARE(view->size(), view->sizeHint());

    // size update from root object disabled
    view->setResizeMode(QQuickView::SizeRootObjectToView);
    item->setWidth(60);
    item->setHeight(80);
    QCOMPARE(view->width(), 80);
    QCOMPARE(view->height(), 100);
    QCOMPARE(QSize(item->width(), item->height()), view->sizeHint());

    // size update from view
    QCoreApplication::processEvents(); // make sure the last resize events are gone
    QSizeChangeListener sizeListener(item);
    view->resize(QSize(200,300));
    QTRY_COMPARE(item->width(), 200.0);

    for (int i = 0; i < sizeListener.size(); ++i) {
        // Check that we have the correct geometry on all signals
        QCOMPARE(sizeListener.at(i), view->size());
    }

    QCOMPARE(item->height(), 300.0);
    QCOMPARE(view->size(), QSize(200, 300));
    QCOMPARE(view->size(), view->sizeHint());

    window.hide();
    delete view;

    // if we set a specific size for the view then it should keep that size
    // for SizeRootObjectToView mode.
    view = new QQuickView(&window);
    view->resize(300, 300);
    view->setResizeMode(QQuickView::SizeRootObjectToView);
    QCOMPARE(QSize(0,0), view->initialSize());
    view->setSource(testFileUrl("resizemodeitem.qml"));
    view->resize(300, 300);
    item = qobject_cast<QQuickItem*>(view->rootObject());
    QVERIFY(item);
    window.show();

    view->showNormal();
    QTest::qWait(50);

    // initial size from root object
    QCOMPARE(item->width(), 300.0);
    QCOMPARE(item->height(), 300.0);
    QCOMPARE(view->size(), QSize(300, 300));
    QCOMPARE(view->size(), view->sizeHint());
    QCOMPARE(view->initialSize(), QSize(200, 200)); // initial object size

    delete view;
}

void tst_QQuickView::errors()
{
    {
        QQuickView view;
        QVERIFY(view.errors().isEmpty()); // don't crash
    }
    {
        QQuickView view;
        QQmlTestMessageHandler messageHandler;
        view.setSource(testFileUrl("error1.qml"));
        QCOMPARE(view.status(), QQuickView::Error);
        QCOMPARE(view.errors().size(), 1);
    }

    {
        QQuickView view;
        QQmlTestMessageHandler messageHandler;
        view.setSource(testFileUrl("error2.qml"));
        QCOMPARE(view.status(), QQuickView::Error);
        QCOMPARE(view.errors().size(), 1);
        view.show();
    }
}

void tst_QQuickView::engine()
{
    QScopedPointer<QQmlEngine> engine(new QQmlEngine);
    QVERIFY(!engine->incubationController());

    QScopedPointer<QQuickView> view(new QQuickView(engine.get(), nullptr));
    QVERIFY(view);
    QCOMPARE(engine->incubationController(), view->incubationController());

    QScopedPointer<QQuickView> view2(new QQuickView(engine.get(), nullptr));
    QVERIFY(view);
    QCOMPARE(engine->incubationController(), view->incubationController());
    view.reset();
    QVERIFY(!engine->incubationController());

    engine->setIncubationController(view2->incubationController());
    QCOMPARE(engine->incubationController(), view2->incubationController());
    view2.reset();
    QVERIFY(!engine->incubationController());

    QScopedPointer<QQuickView> view3(new QQuickView);
    QScopedPointer<QQuickView> view4(new QQuickView(view3->engine(), nullptr));

    QVERIFY(view3->engine());
    QVERIFY(view4->engine());
    QCOMPARE(view3->engine(), view4->engine());
    view3.reset();
    QVERIFY(!view4->engine());
    QTest::ignoreMessage(QtWarningMsg, "QQuickView: invalid qml engine.");
    view4->setSource(QUrl());

    QCOMPARE(view4->status(), QQuickView::Error);
    QVERIFY(!view4->errors().isEmpty());
    QCOMPARE(view4->errors().back().description(), QLatin1String("QQuickView: invalid qml engine."));
}

void tst_QQuickView::findChild()
{
    QQuickView view;
    view.setSource(testFileUrl("findChild.qml"));

    // QQuickView
    // |_ QQuickWindow::contentItem
    // |  |_ QQuickView::rootObject: QML Item("rootObject") (findChild.qml)
    // |  |  |_ QML Item("rootObjectChild") (findChild.qml)
    // |  |_ QObject("contentItemChild")
    // |_ QObject("viewChild")

    QObject *viewChild = new QObject(&view);
    viewChild->setObjectName("viewChild");

    QObject *contentItemChild = new QObject(view.contentItem());
    contentItemChild->setObjectName("contentItemChild");

    QObject *rootObject = view.rootObject();
    QVERIFY(rootObject);

    QObject *rootObjectChild = rootObject->findChild<QObject *>("rootObjectChild");
    QVERIFY(rootObjectChild);

    QCOMPARE(view.findChild<QObject *>("viewChild"), viewChild);
    QCOMPARE(view.findChild<QObject *>("contentItemChild"), contentItemChild);
    QCOMPARE(view.findChild<QObject *>("rootObject"), rootObject);
    QCOMPARE(view.findChild<QObject *>("rootObjectChild"), rootObjectChild);

    QVERIFY(!view.contentItem()->findChild<QObject *>("viewChild")); // sibling
    QCOMPARE(view.contentItem()->findChild<QObject *>("contentItemChild"), contentItemChild);
    QCOMPARE(view.contentItem()->findChild<QObject *>("rootObject"), rootObject);
    QCOMPARE(view.contentItem()->findChild<QObject *>("rootObjectChild"), rootObjectChild);

    QVERIFY(!view.rootObject()->findChild<QObject *>("viewChild")); // ancestor
    QVERIFY(!view.rootObject()->findChild<QObject *>("contentItemChild")); // cousin
    QVERIFY(!view.rootObject()->findChild<QObject *>("rootObject")); // self
}

void tst_QQuickView::setInitialProperties()
{
    QQuickView view;
    view.setInitialProperties({{"z", 4}, {"width", 100}});
    view.setSource(testFileUrl("resizemodeitem.qml"));
    QObject *rootObject = view.rootObject();
    QVERIFY(rootObject);
    QCOMPARE(rootObject->property("z").toInt(), 4);
    QCOMPARE(rootObject->property("width").toInt(), 100);
}

void tst_QQuickView::fromModuleCtor()
{
    QQuickView view("QtQuick", "Rectangle");
    // creation is always synchronous for C++ defined types, so we don't need _TRY
    QObject *rootObject = view.rootObject();
    QVERIFY(rootObject);
    QCOMPARE(rootObject->metaObject()->className(), "QQuickRectangle");
}

void tst_QQuickView::loadFromModule_data()
{
    QTest::addColumn<QString>("module");
    QTest::addColumn<QString>("typeName");
    QTest::addColumn<QUrl>("url");
    QTest::addColumn<QQuickView::Status>("status");

    QTest::addRow("Item") << u"QtQuick"_s << u"Item"_s << QUrl() << QQuickView::Ready;
    QTest::addRow("composite") << u"test"_s << u"TestQml"_s << QUrl("qrc:/qt/qml/test/data/TestQml.qml") << QQuickView::Ready;
    QTest::addRow("nonexistent") << u"missing"_s << u"Type"_s << QUrl() << QQuickView::Error;
}

void tst_QQuickView::loadFromModule()
{
    QFETCH(QString, module);
    QFETCH(QString, typeName);
    QFETCH(QUrl, url);
    QFETCH(QQuickView::Status, status);

    QQuickView view;
    view.loadFromModule(module, typeName);
    QTRY_COMPARE(view.status(), status);
    QCOMPARE(view.source(), url);
}

void tst_QQuickView::setSourceRelativeQrcPath()
{
    QQuickView view;
    const auto url = QUrl("qrc:main.qml");
    QTest::ignoreMessage(
            QtMsgType::QtWarningMsg,
            "QQmlComponent: attempted to load via a relative URL '%1' in resource file system. "
            "This is not fully supported and may not work"_L1.arg(url.toString())
                    .toLocal8Bit()
                    .data());
    QTest::ignoreMessage(QtMsgType::QtWarningMsg, "qrc:main.qml: No such file or directory");
    view.setSource(url);
}

void tst_QQuickView::overlay()
{
    QTest::ignoreMessage(QtWarningMsg,
                         QRegularExpression(".*: Cannot set properties on overlay as it is null"));
    QQuickView view;
    view.setSource(testFileUrl("overlay.qml"));
    QObject *rootObject = view.rootObject();
    QVERIFY(!rootObject);
}

QTEST_MAIN(tst_QQuickView)

#include "tst_qquickview.moc"
