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

#include <QtTest/qtest.h>

#ifndef QTEST_THROW_ON_FAIL
# error This test requires QTEST_THROW_ON_FAIL being active.
#endif

#include <bitset>
#include <QtTest/qsignalspy.h>
#include <QtQml/qqmlfile.h>
#include <QtQuick/private/qquickmousearea_p.h>
#include <QtQuickDialogs2/private/qquickmessagedialog_p.h>
#include <QtQuickDialogs2QuickImpl/private/qquickmessagedialogimpl_p.h>
#include <QtQuickTest/quicktest.h>
#include <QtQuickControls2/qquickstyle.h>
#include <QtQuickTemplates2/private/qquickdialog_p_p.h>
#include <QtQuickControlsTestUtils/private/controlstestutils_p.h>
#include <QtQuickControlsTestUtils/private/dialogstestutils_p.h>

using namespace QQuickVisualTestUtils;
using namespace QQuickDialogTestUtils;
using namespace QQuickControlsTestUtils;

class tst_QQuickMessageDialogImpl : public QQmlDataTest
{
    Q_OBJECT

public:
    tst_QQuickMessageDialogImpl();
    static void initMain()
    {
        // We need to set this attribute.
        QCoreApplication::setAttribute(Qt::AA_DontUseNativeDialogs);
        // We don't want to run this test for every style, as each one will have
        // different ways of implementing the dialogs.
        // For now we only test one style.
        // TODO: use Basic
        QQuickStyle::setStyle("Fusion");
    }

private slots:
    void changeText_data();
    void changeText();
    void changeInformativeText_data();
    void changeInformativeText();
    void changeStandardButtons();
    void detailedText();
    void resultReflectsLastStandardButtonPressed();
    void checkModality_data();
    void checkModality();
    void checkFrameless();
};

// We don't want to fail on warnings until QTBUG-98964 is fixed,
// as we deliberately prevent deferred execution in some of the tests here,
// which causes warnings.
tst_QQuickMessageDialogImpl::tst_QQuickMessageDialogImpl()
    : QQmlDataTest(QT_QMLTEST_DATADIR, FailOnWarningsPolicy::DoNotFailOnWarnings)
{
}

void tst_QQuickMessageDialogImpl::changeText_data()
{
    QTest::addColumn<QString>("testString1");
    QTest::addColumn<QString>("testString2");
    QTest::newRow("data") << "the quick brown fox jumped over the lazy dog"
                          << "the lazy dog jumped over the quick brown fox";
}

void tst_QQuickMessageDialogImpl::changeText()
{
    QTest::failOnWarning(QRegularExpression(".*"));
    QFETCH(QString, testString1);
    QFETCH(QString, testString2);
    DialogTestHelper<QQuickMessageDialog, QQuickMessageDialogImpl> dialogHelper(
            this, "messageDialog.qml");
    QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage());
    QVERIFY(dialogHelper.waitForWindowActive());
    QVERIFY(dialogHelper.openDialog());
    QTRY_VERIFY(dialogHelper.isQuickDialogOpen());

    QSignalSpy textSpy(dialogHelper.dialog, SIGNAL(textChanged()));
    QVERIFY(textSpy.isValid());

    auto textLabel = dialogHelper.quickDialog->findChild<QQuickLabel *>("textLabel");
    QVERIFY(textLabel);

    // default value is empty string
    QCOMPARE(dialogHelper.dialog->text(), "");

    // update the text property
    dialogHelper.dialog->setText(testString1);
    QCOMPARE(textSpy.size(), 1);

    // The textLabel is empty until dialog is re-opened
    QCOMPARE(dialogHelper.dialog->text(), testString1);
    QCOMPARE(textLabel->text(), "");
    dialogHelper.dialog->close();
    dialogHelper.dialog->open();
    QCOMPARE(dialogHelper.dialog->text(), testString1);
    QCOMPARE(textLabel->text(), testString1);

    // The textLabel isn't updated immediately
    dialogHelper.dialog->setText(testString2);
    QCOMPARE(textSpy.size(), 2);
    QCOMPARE(textLabel->text(), testString1);

    dialogHelper.dialog->close();
}

void tst_QQuickMessageDialogImpl::changeInformativeText_data()
{
    QTest::addColumn<QString>("testString1");
    QTest::addColumn<QString>("testString2");
    QTest::newRow("data") << "the quick brown fox jumped over the lazy dog"
                          << "the lazy dog jumped over the quick brown fox";
}

void tst_QQuickMessageDialogImpl::changeInformativeText()
{
    QFETCH(QString, testString1);
    QFETCH(QString, testString2);
    DialogTestHelper<QQuickMessageDialog, QQuickMessageDialogImpl> dialogHelper(
            this, "messageDialog.qml");
    QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage());
    QVERIFY(dialogHelper.waitForWindowActive());
    QVERIFY(dialogHelper.openDialog());
    QTRY_VERIFY(dialogHelper.isQuickDialogOpen());

    QSignalSpy informativeTextSpy(dialogHelper.dialog, SIGNAL(informativeTextChanged()));
    QVERIFY(informativeTextSpy.isValid());

    auto informativeTextLabel =
            dialogHelper.quickDialog->findChild<QQuickLabel *>("informativeTextLabel");
    QVERIFY(informativeTextLabel);

    // default value is empty string
    QCOMPARE(dialogHelper.dialog->informativeText(), "");

    // update the informativeText property
    dialogHelper.dialog->setInformativeText(testString1);
    QCOMPARE(informativeTextSpy.size(), 1);

    // The textLabel is empty until dialog is re-opened
    QCOMPARE(dialogHelper.dialog->informativeText(), testString1);
    QCOMPARE(informativeTextLabel->text(), "");
    dialogHelper.dialog->close();
    dialogHelper.dialog->open();
    QCOMPARE(dialogHelper.dialog->informativeText(), testString1);
    QCOMPARE(informativeTextLabel->text(), testString1);

    // The textLabel shouldn't update immediately
    dialogHelper.dialog->setInformativeText(testString2);
    QCOMPARE(informativeTextSpy.size(), 2);
    QCOMPARE(informativeTextLabel->text(), testString1);

    dialogHelper.dialog->close();
}

void tst_QQuickMessageDialogImpl::changeStandardButtons()
{
    DialogTestHelper<QQuickMessageDialog, QQuickMessageDialogImpl> dialogHelper(
            this, "messageDialog.qml");
    QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage());
    QVERIFY(dialogHelper.waitForWindowActive());
    QVERIFY(dialogHelper.openDialog());
    QTRY_VERIFY(dialogHelper.isQuickDialogOpen());

    QSignalSpy buttonBoxSpy(dialogHelper.dialog, SIGNAL(buttonsChanged()));
    QVERIFY(buttonBoxSpy.isValid());

    auto buttonBox = dialogHelper.quickDialog->findChild<QQuickDialogButtonBox *>("buttonBox");
    QVERIFY(buttonBox);

    std::bitset<QPlatformDialogHelper::NRoles> availableButtonRoles;
    auto verifyButtonBox = [&]() {
        availableButtonRoles.reset();
        for (int i = 0; i < buttonBox->count(); ++i) {
            const auto button = qobject_cast<QQuickAbstractButton *>(buttonBox->itemAt(i));
            const auto role = QQuickDialogPrivate::buttonRole(button);
            QVERIFY(role > QPlatformDialogHelper::InvalidRole && role < QPlatformDialogHelper::NRoles);
            availableButtonRoles.set(role);
        }
    };

    QCOMPARE(buttonBox->count(), 1);
    QVERIFY2(dialogHelper.dialog->buttons() & QPlatformDialogHelper::StandardButton::Ok,
             "The QMessageDialogOptionsPrivate constructor assures that the Ok button will be "
             "added by default, if no other buttons are set");

    dialogHelper.dialog->setButtons(
            QPlatformDialogHelper::StandardButtons(QPlatformDialogHelper::StandardButton::Save
                                                   | QPlatformDialogHelper::StandardButton::Cancel
                                                   | QPlatformDialogHelper::StandardButton::Apply));
    QCOMPARE(buttonBoxSpy.size(), 1);
    QCOMPARE(buttonBox->count(), 1);
    dialogHelper.dialog->close();
    dialogHelper.dialog->open();
    QCOMPARE(buttonBox->count(), 3);

    verifyButtonBox();
    QVERIFY(availableButtonRoles.test(QPlatformDialogHelper::AcceptRole));
    QVERIFY(availableButtonRoles.test(QPlatformDialogHelper::RejectRole));
    QVERIFY(availableButtonRoles.test(QPlatformDialogHelper::ApplyRole));

    // change buttons when the dialog is closed
    dialogHelper.dialog->close();
    dialogHelper.dialog->setButtons(
            QPlatformDialogHelper::StandardButton(QPlatformDialogHelper::StandardButton::Ok
                                                  | QPlatformDialogHelper::StandardButton::Close));
    QCOMPARE(buttonBoxSpy.size(), 2);
    QCOMPARE(buttonBox->count(), 3);
    dialogHelper.dialog->open();
    QCOMPARE(buttonBox->count(), 2);

    verifyButtonBox();
    QVERIFY(availableButtonRoles.test(QPlatformDialogHelper::AcceptRole));
    QVERIFY(availableButtonRoles.test(QPlatformDialogHelper::RejectRole));

    dialogHelper.dialog->close();
}

void tst_QQuickMessageDialogImpl::detailedText()
{
    const QString emptyString;
    const QString nonEmptyString("the quick brown fox jumped over the lazy dog");
    DialogTestHelper<QQuickMessageDialog, QQuickMessageDialogImpl> dialogHelper(
            this, "messageDialog.qml");
    QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage());
    QVERIFY(dialogHelper.waitForWindowActive());
    QVERIFY(dialogHelper.openDialog());
    QTRY_VERIFY(dialogHelper.isQuickDialogOpen());

    QSignalSpy detailedTextSpy(dialogHelper.dialog, SIGNAL(detailedTextChanged()));
    QVERIFY(detailedTextSpy.isValid());

    auto detailedTextArea = dialogHelper.quickDialog->findChild<QQuickTextArea *>("detailedText");
    QVERIFY(detailedTextArea);
    auto detailedTextButton =
            dialogHelper.quickDialog->findChild<QQuickAbstractButton *>("detailedTextButton");
    QVERIFY(detailedTextButton);

    // Should be empty by default
    QCOMPARE(dialogHelper.dialog->detailedText(), emptyString);
    QCOMPARE(detailedTextArea->text(), emptyString);
    QVERIFY(!detailedTextButton->isVisible());

    // Set the detailed text to a non-empty string
    dialogHelper.dialog->setDetailedText(nonEmptyString);
    QCOMPARE(dialogHelper.dialog->detailedText(), nonEmptyString);
    QCOMPARE(detailedTextSpy.size(), 1);
    QCOMPARE(detailedTextArea->text(), emptyString);
    QVERIFY(!detailedTextButton->isVisible());
    dialogHelper.dialog->close();
    dialogHelper.dialog->open();
    QCOMPARE(dialogHelper.dialog->detailedText(), nonEmptyString);
    QCOMPARE(detailedTextArea->text(), nonEmptyString);
    QVERIFY2(detailedTextButton->isVisible(),
             "The 'show details' button should be visible when the detailedText property is not "
             "empty.");

    // Set the detailed text to an empty string
    dialogHelper.dialog->setDetailedText(emptyString);
    QCOMPARE(detailedTextSpy.size(), 2);
    QCOMPARE(dialogHelper.dialog->detailedText(), emptyString);
    QCOMPARE(detailedTextArea->text(), nonEmptyString);
    QVERIFY(detailedTextButton->isVisible());
    dialogHelper.dialog->close();
    dialogHelper.dialog->open();
    QCOMPARE(dialogHelper.dialog->detailedText(), emptyString);
    QCOMPARE(detailedTextArea->text(), emptyString);
    QVERIFY2(!detailedTextButton->isVisible(),
             "The 'show details' button should be invisible when the detailedText property is an empty string");

    // Change the detailed text property while the dialog is already open, should not immediately
    // update the dialog ui
    dialogHelper.dialog->setDetailedText(nonEmptyString);
    QCOMPARE(detailedTextSpy.size(), 3);
    QCOMPARE(dialogHelper.dialog->detailedText(), nonEmptyString);
    QCOMPARE(detailedTextArea->text(), emptyString);
    QVERIFY2(!detailedTextButton->isVisible(),
             "The 'show details' button should only become visible when the dialog is re-opened.");

    dialogHelper.dialog->close();
}

void tst_QQuickMessageDialogImpl::resultReflectsLastStandardButtonPressed()
{
    DialogTestHelper<QQuickMessageDialog, QQuickMessageDialogImpl> dialogHelper(
            this, "messageDialogWithButtons.qml");
    QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage());
    QVERIFY(dialogHelper.waitForWindowActive());

    QVERIFY(dialogHelper.openDialog());
    QTRY_VERIFY(dialogHelper.isQuickDialogOpen());

    QVERIFY(dialogHelper.waitForPopupWindowActiveAndPolished());

    QSignalSpy acceptedSpy(dialogHelper.dialog, SIGNAL(accepted()));
    QSignalSpy rejectedSpy(dialogHelper.dialog, SIGNAL(rejected()));
    QSignalSpy resultChangedSpy(dialogHelper.dialog, SIGNAL(resultChanged()));

    auto buttonBox = dialogHelper.quickDialog->findChild<QQuickDialogButtonBox *>("buttonBox");
    QVERIFY(buttonBox);

    bool yesFound = false;
    bool noFound = false;
    bool discardFound = false;
    bool applyFound = false;

    int expectedNumberOfAcceptedSignals = 0;
    int expectedNumberOfRejectedSignals = 0;

    // The dialogButtonBox has different layouts depending on platform. This tries to account for all possible layouts.
    // If the role of a button is YesRole, AcceptRole, NoRole or RejectRole, then pressing that button should emit accepted, or rejected.
    // And since this is a MessageDialog, the result property should reflect the last button pressed, rather than the StandardCode.
    for (int i = 0; i < buttonBox->count(); ++i) {
        auto button = qobject_cast<QQuickAbstractButton *>(buttonBox->itemAt(i));
        switch (QQuickDialogPrivate::buttonRole(button)) {
        case QPlatformDialogHelper::YesRole:
            yesFound = true;
            expectedNumberOfAcceptedSignals++;

            QTest::mouseClick(dialogHelper.popupWindow(), Qt::LeftButton, Qt::NoModifier, button->mapToScene({ button->width() / 2, button->height() / 2 }).toPoint());
            QTRY_VERIFY(!dialogHelper.isQuickDialogOpen());

            QCOMPARE(dialogHelper.dialog->result(), QPlatformDialogHelper::StandardButton::Yes);
            QCOMPARE(resultChangedSpy.count(), i + 1);

            QVERIFY(dialogHelper.openDialog());
            QTRY_VERIFY(dialogHelper.isQuickDialogOpen());
            break;
        case QPlatformDialogHelper::NoRole:
            noFound = true;
            expectedNumberOfRejectedSignals++;

            QTest::mouseClick(dialogHelper.popupWindow(), Qt::LeftButton, Qt::NoModifier, button->mapToScene({ button->width() / 2, button->height() / 2 }).toPoint());
            QTRY_VERIFY(!dialogHelper.isQuickDialogOpen());

            QCOMPARE(dialogHelper.dialog->result(), QPlatformDialogHelper::StandardButton::No);
            QCOMPARE(resultChangedSpy.count(), i + 1);

            QVERIFY(dialogHelper.openDialog());
            QTRY_VERIFY(dialogHelper.isQuickDialogOpen());
            break;
        case QPlatformDialogHelper::DestructiveRole:
            discardFound = true;

            QTest::mouseClick(dialogHelper.popupWindow(), Qt::LeftButton, Qt::NoModifier, button->mapToScene({ button->width() / 2, button->height() / 2 }).toPoint());
            QTRY_VERIFY(!dialogHelper.isQuickDialogOpen());

            QCOMPARE(dialogHelper.dialog->result(), QPlatformDialogHelper::StandardButton::Discard);
            QCOMPARE(resultChangedSpy.count(), i + 1);

            QVERIFY(dialogHelper.openDialog());
            QTRY_VERIFY(dialogHelper.isQuickDialogOpen());
            break;
        case QPlatformDialogHelper::ApplyRole:
            applyFound = true;

            QTest::mouseClick(dialogHelper.popupWindow(), Qt::LeftButton, Qt::NoModifier, button->mapToScene({ button->width() / 2, button->height() / 2 }).toPoint());
            QTRY_VERIFY(!dialogHelper.isQuickDialogOpen());

            QCOMPARE(dialogHelper.dialog->result(), QPlatformDialogHelper::StandardButton::Apply);
            QCOMPARE(resultChangedSpy.count(), i + 1);

            QVERIFY(dialogHelper.openDialog());
            QTRY_VERIFY(dialogHelper.isQuickDialogOpen());
            break;
        default:
            QFAIL(qPrintable(QStringLiteral("Unexpected role %1").arg(QQuickDialogPrivate::buttonRole(button))));
        }
    }

    QVERIFY2(yesFound && noFound && discardFound && applyFound, "A button that was expected to be present, wasn't found when iterating over all of them.");
    QCOMPARE(acceptedSpy.count(), expectedNumberOfAcceptedSignals);
    QCOMPARE(rejectedSpy.count(), expectedNumberOfRejectedSignals);

    dialogHelper.dialog->close();
}

void tst_QQuickMessageDialogImpl::checkModality_data()
{
    QTest::addColumn<Qt::WindowModality>("modality");
    QTest::addColumn<int>("expectedRootWindowChildCount");
    QTest::addColumn<int>("expectedChildWindowClickCount");

    QTest::newRow("nonModal") << Qt::NonModal << 1 << 1;
    QTest::newRow("windowModal") << Qt::WindowModal << 0 << 1;
    // Verify application modal case only for the platform which supports multiple windows
    if (QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::MultipleWindows))
        QTest::newRow("applicationModal") << Qt::ApplicationModal << 0 << 0;
}

void tst_QQuickMessageDialogImpl::checkModality()
{
    QFETCH(Qt::WindowModality, modality);
    QFETCH(int, expectedRootWindowChildCount);
    QFETCH(int, expectedChildWindowClickCount);

    DialogTestHelper<QQuickMessageDialog, QQuickMessageDialogImpl> dialogHelper(this, "checkModality.qml");
    QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage());
    QVERIFY(dialogHelper.waitForWindowActive());

    dialogHelper.dialog->setModality(modality);
    QCOMPARE(dialogHelper.dialog->modality(), modality);

    OPEN_QUICK_DIALOG();
    QVERIFY(dialogHelper.waitForPopupWindowActiveAndPolished());

    auto *childWindow = dialogHelper.window()->property("childWindow").value<QQuickWindow *>();
    QVERIFY(childWindow);

    // Setting the child window transient parent as root window won't allow it to receive mouse
    // events, causing WindowModal case not to be tested. Thus, resetting the transient parent.
    if (modality == Qt::WindowModal)
        childWindow->setTransientParent(nullptr);

    // Platforms such as Android have system bars obscuring the content item on
    // top and bottom, which can lead to a mouse event being delivered to the
    // system bar instead of the content item. To avoid this, safe area margin
    // has been considered.
    const QMargins safeAreaMargin = dialogHelper.window()->safeAreaMargins();
    const auto *rootMouseArea = dialogHelper.window()->property("rootMArea").value<QQuickMouseArea *>();
    QVERIFY(rootMouseArea);
    QSignalSpy rmaMouseSpy(rootMouseArea, &QQuickMouseArea::clicked);
    QTest::mouseClick(dialogHelper.window(), Qt::LeftButton, Qt::NoModifier,
                    QPoint(5 + safeAreaMargin.left(), 5 + safeAreaMargin.top()));
    QCOMPARE(rmaMouseSpy.size(), expectedRootWindowChildCount);

    const auto *childMouseArea = dialogHelper.window()->property("childMArea").value<QQuickMouseArea *>();
    QVERIFY(childMouseArea);
    QSignalSpy cmaMouseSpy(childMouseArea, &QQuickMouseArea::clicked);
    QTest::mouseClick(childWindow, Qt::LeftButton, Qt::NoModifier,
                    QPoint(5 + safeAreaMargin.left(), 5 + safeAreaMargin.top()));
    QCOMPARE(cmaMouseSpy.size(), expectedChildWindowClickCount);
}

void tst_QQuickMessageDialogImpl::checkFrameless()
{
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
    QSKIP("Frameless window is not supported on Android/IOS");
#endif
    DialogTestHelper<QQuickMessageDialog, QQuickMessageDialogImpl> dialogHelper(this, "messageDialogFrameless.qml");
    OPEN_QUICK_DIALOG();
    QVERIFY(dialogHelper.waitForPopupWindowActiveAndPolished());

    QVERIFY(dialogHelper.popupWindow()->flags().testFlag(Qt::FramelessWindowHint));
}

QTEST_MAIN(tst_QQuickMessageDialogImpl)

#include "tst_qquickmessagedialogimpl.moc"
