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

#include <QtCore/qregularexpression.h>
#include <QtGui/qpalette.h>
#include <QtGui/private/qguiapplication_p.h>
#include <QtGui/qpa/qplatformtheme.h>
#include <QtTest/qtest.h>
#include <QtQml/qqmlcomponent.h>
#include <QtQml/qqmlcontext.h>
#include <QtQml/qqmlapplicationengine.h>
#include <QtQml/qqmlengine.h>
#include <QtQml/private/qqmldata_p.h>
#include <QtQuick/qquickwindow.h>
#include <QtQuickTestUtils/private/qmlutils_p.h>
#include <QtQuickControls2/qquickstyle.h>
#include <QtQuickControls2/private/qquickstyle_p.h>
#include <QtQuickControls2Impl/private/qquickiconlabel_p.h>
#include <QtQuickControlsTestUtils/private/controlstestutils_p.h>
#include <QtQuickControlsTestUtils/private/qtest_quickcontrols_p.h>
#include <QtQuickTemplates2/private/qquickapplicationwindow_p.h>
#include <QtQuickTemplates2/private/qquickbutton_p.h>

using namespace QQuickControlsTestUtils;

class tst_StyleImports : public QQmlDataTest
{
    Q_OBJECT

public:
    tst_StyleImports();

private slots:
    void initTestCase() override;

    void cleanup();

    void select_data();
    void select();

    void platformSelectors();
    void customStyleSelector();

    void fallbackStyleShouldNotOverwriteTheme_data();
    void fallbackStyleShouldNotOverwriteTheme();

    void fallbackStyleThemeRespected_data();
    void fallbackStyleThemeRespected();

    void attachedTypesAvailable_data();
    void attachedTypesAvailable();

    void nativeStyle();
};

tst_StyleImports::tst_StyleImports()
    : QQmlDataTest(QT_QMLTEST_DATADIR)
{
}

void tst_StyleImports::initTestCase()
{
    QQmlDataTest::initTestCase();
}

void tst_StyleImports::cleanup()
{
    qmlClearTypeRegistrations();
}

void tst_StyleImports::select_data()
{
    QTest::addColumn<QString>("file");
    QTest::addColumn<QString>("style");
    QTest::addColumn<QString>("fallback");
    QTest::addColumn<QString>("expected");

    // Action.qml exists in the FileSystemStyle style and the Basic style.
    QTest::newRow("control=Action,style=basic,fallback=empty") << "Action.qml" << "Basic" << "" << "Basic";
    QTest::newRow("control=Action,style=fs,fallback=empty") << "Action.qml" << "FileSystemStyle" << "" << "FileSystemStyle";
    QTest::newRow("control=Action,style=qrc,fallback=empty") << "Action.qml" << "ResourceStyle" << "" << "Basic";
    QTest::newRow("control=Action,style=nosuch,fallback=empty") << "Action.qml" << "NoSuchStyle" << "" << "Basic";
    QTest::newRow("control=Action,style=import,fallback=empty") << "Action.qml" << "StyleThatImportsFusion" << "" << "StyleThatImportsFusion";

    QTest::newRow("control=Action,style=basic,fallback=mat") << "Action.qml" << "Basic" << "Material" << "";
    QTest::newRow("control=Action,style=fs,fallback=mat") << "Action.qml" << "FileSystemStyle" << "Material" << "FileSystemStyle";
    QTest::newRow("control=Action,style=qrc,fallback=mat") << "Action.qml" << "ResourceStyle" << "Material" << "Basic";
    QTest::newRow("control=Action,style=nosuch,fallback=mat") << "Action.qml" << "NoSuchStyle" << "Material" << "Basic";
    QTest::newRow("control=Action,style=import,fallback=mat") << "Action.qml" << "StyleThatImportsFusion" << "Material" << "StyleThatImportsFusion";

    // Amongst the styles we're testing here, ScrollView.qml only exists in the Basic style.
    QTest::newRow("control=ScrollView,style=basic,fallback=empty") << "ScrollView.qml" << "Basic" << "" << "Basic";
    QTest::newRow("control=ScrollView,style=fs,fallback=empty") << "ScrollView.qml" << "FileSystemStyle" << "" << "Basic";
    QTest::newRow("control=ScrollView,style=qrc,fallback=empty") << "ScrollView.qml" << "ResourceStyle" << "" << "Basic";
    QTest::newRow("control=ScrollView,style=nosuch,fallback=empty") << "ScrollView.qml" << "NoSuchStyle" << "" << "Basic";
    QTest::newRow("control=ScrollView,style=import,fallback=empty") << "ScrollView.qml" << "StyleThatImportsFusion" << "" << "Fusion";

    QTest::newRow("control=ScrollView,style=basic,fallback=mat") << "ScrollView.qml" << "Basic" << "Material" << "Basic";
    QTest::newRow("control=ScrollView,style=fs,fallback=mat") << "ScrollView.qml" << "FileSystemStyle" << "Material" << "Material";
    QTest::newRow("control=ScrollView,style=qrc,fallback=mat") << "ScrollView.qml" << "ResourceStyle" << "Material" << "Material";
    QTest::newRow("control=ScrollView,style=nosuch,fallback=mat") << "ScrollView.qml" << "NoSuchStyle" << "Material" << "Basic";
    QTest::newRow("control=ScrollView,style=import,fallback=mat") << "ScrollView.qml" << "StyleThatImportsFusion" << "Material" << "Material";

    // Label.qml exists in the FileSystemStyle, Basic and Material styles.
    QTest::newRow("control=Label,style=basic,fallback=empty") << "Label.qml" << "Basic" << "" << "Basic";
    QTest::newRow("control=Label,style=fs,fallback=empty") << "Label.qml" << "FileSystemStyle" << "" << "FileSystemStyle";
    QTest::newRow("control=Label,style=qrc,fallback=empty") << "Label.qml" << "ResourceStyle" << "" << "Basic";
    QTest::newRow("control=Label,style=nosuch,fallback=empty") << "Label.qml" << "NoSuchStyle" << "" << "Basic";
    QTest::newRow("control=Label,style=import,fallback=empty") << "Label.qml" << "StyleThatImportsFusion" << "" << "Fusion";

    QTest::newRow("control=Label,style=basic,fallback=mat") << "Label.qml" << "Basic" << "Material" << "Basic";
    QTest::newRow("control=Label,style=fs,fallback=mat") << "Label.qml" << "FileSystemStyle" << "Material" << "FileSystemStyle";
    QTest::newRow("control=Label,style=qrc,fallback=mat") << "Label.qml" << "ResourceStyle" << "Material" << "Material";
    QTest::newRow("control=Label,style=nosuch,fallback=mat") << "Label.qml" << "NoSuchStyle" << "Material" << "Basic";
    QTest::newRow("control=Label,style=import,fallback=mat") << "Label.qml" << "StyleThatImportsFusion" << "Material" << "Material";

    // Button.qml exists in all styles including the fs and qrc styles
    QTest::newRow("control=Button,style=basic,fallback=empty") << "Button.qml" << "Basic" << "" << "Basic";
    QTest::newRow("control=Button,style=fs,fallback=empty") << "Button.qml" << "FileSystemStyle" << "" << "FileSystemStyle";
    QTest::newRow("control=Button,style=qrc,fallback=empty") << "Button.qml" << "ResourceStyle" << "" << "ResourceStyle";
    QTest::newRow("control=Button,style=nosuch,fallback=empty") << "Button.qml" << "NoSuchStyle" << "" << "Basic";
    QTest::newRow("control=Button,style=import,fallback=empty") << "Button.qml" << "StyleThatImportsFusion" << "" << "Fusion";

    QTest::newRow("control=Button,style=basic,fallback=mat") << "Button.qml" << "Basic" << "Material" << "Basic";
    QTest::newRow("control=Button,style=fs,fallback=mat") << "Button.qml" << "FileSystemStyle" << "Material" << "FileSystemStyle";
    QTest::newRow("control=Button,style=qrc,fallback=mat") << "Button.qml" << "ResourceStyle" << "Material" << "ResourceStyle";
    QTest::newRow("control=Button,style=nosuch,fallback=mat") << "Button.qml" << "NoSuchStyle" << "Material" << "Basic";
    QTest::newRow("control=Button,style=import,fallback=mat") << "Button.qml" << "StyleThatImportsFusion" << "Material" << "Material";
}

void tst_StyleImports::select()
{
    QFETCH(QString, file);
    QFETCH(QString, style);
    QFETCH(QString, fallback);
    QFETCH(QString, expected);

    // In Qt 5, there were several accepted forms for style names.
    // In Qt 6, the only accepted form is the base name of the style directory.
    const bool invalidStyleName = style.contains(QLatin1Char('/'));
    if (invalidStyleName)
        QTest::ignoreMessage(QtWarningMsg,
            "Style names must not contain paths; see the \"Definition of a Style\" documentation for more information");
    QQuickStyle::setStyle(style);
    QQuickStyle::setFallbackStyle(fallback);

    QQmlEngine engine;
    engine.addImportPath(QLatin1String(":/"));
//    engine.addImportPath(directory());
    engine.addImportPath(dataDirectory() + QLatin1String("/styles"));
    QQmlComponent component(&engine);
    const QString controlName = file.mid(0, file.indexOf(QLatin1Char('.')));
    component.setData(QString::fromLatin1("import QtQuick; import QtQuick.Controls; %1 { }").arg(controlName).toUtf8(), QUrl());

    const bool nonExistentStyle = style == QLatin1String("NoSuchStyle");
    if (nonExistentStyle)
        QTest::ignoreMessage(QtWarningMsg, "QQmlComponent: Component is not ready");
    QScopedPointer<QObject> object(component.create());
    if (nonExistentStyle) {
        QVERIFY(object.isNull());
        return;
    }

    QVERIFY2(!object.isNull(), qPrintable(component.errorString()));

    if (!QQuickStylePrivate::builtInStyles().contains(expected)) {
        // We're expecting a custom style.
        QCOMPARE(object->objectName(), expected);
    } else {
        const QUrl expectedUrl(
                    QLatin1String("qrc:///qt-project.org/imports/QtQuick/Controls/%1/%2")
                    .arg(expected, file));
        QQmlComponent c2(&engine, expectedUrl);
        QVERIFY2(c2.isReady(), qPrintable(c2.errorString()));
        QScopedPointer<QObject> o2(c2.create());
        QCOMPARE(object->metaObject(), o2->metaObject());
    }
}

// Tests that the various platforms are available as selectors.
void tst_StyleImports::platformSelectors()
{
    QQuickStyle::setStyle(QLatin1String("PlatformStyle"));

    QQmlApplicationEngine engine;
    engine.addImportPath(dataDirectory() + QLatin1String("/styles"));
    engine.load(testFileUrl("applicationWindowWithButton.qml"));
    QVERIFY(!engine.rootObjects().isEmpty());
    QQuickWindow *window = qobject_cast<QQuickWindow*>(engine.rootObjects().first());
    QVERIFY(window);

    QObject *button = window->property("button").value<QObject*>();
    QVERIFY(button);

#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
    QCOMPARE(button->objectName(), "PlatformStyle/+linux");
#elif defined(Q_OS_MACOS)
    QCOMPARE(button->objectName(), "PlatformStyle/+macos");
#elif defined(Q_OS_WIN)
    QCOMPARE(button->objectName(), "PlatformStyle/+windows");
#else
    QCOMPARE(button->objectName(), "PlatformStyle/Button.qml");
#endif
}

// Tests that a file selector is added for custom styles.
// Note that this is different to the regular QML import mechanism
// that results in e.g. FileSystemStyle/Button.qml being found;
// it allows non-template (Controls), custom user types to be
// picked up via selectors.
void tst_StyleImports::customStyleSelector()
{
    QQuickStyle::setStyle(QLatin1String("FileSystemStyle"));

    QQmlApplicationEngine engine;
    engine.addImportPath(dataDirectory() + QLatin1String("/styles"));
    engine.load(testFileUrl("customStyleSelector.qml"));
    QVERIFY(!engine.rootObjects().isEmpty());
    QQuickWindow *window = qobject_cast<QQuickWindow*>(engine.rootObjects().first());
    QVERIFY(window);

    QObject *customComponent = window->property("customComponent").value<QObject*>();
    QVERIFY(customComponent);
    QCOMPARE(customComponent->objectName(), "+FileSystemStyle/CustomComponent.qml");
}

QT_BEGIN_NAMESPACE
extern QPalette qt_fusionPalette();
QT_END_NAMESPACE

void tst_StyleImports::fallbackStyleShouldNotOverwriteTheme_data()
{
    QTest::addColumn<QString>("style");
    QTest::addColumn<QString>("fallbackStyle");
    QTest::addColumn<QColor>("expectedContentItemColor");

    QTest::addRow("style=ResourceStyle,fallbackStyle=Material")
        << QString::fromLatin1("ResourceStyle")
        << QString::fromLatin1("Material") << QColor("salmon");

    // A button's contentItem property will reflect the inactive button color,
    // when the button is not shown.
    // => read that color from the platform theme's ButtonPalette.
    // => fall back to the SystemPalette, if no ButtonPalette is available
    // => skip data row, if no platform theme is found
    QPlatformTheme *platformTheme = QGuiApplicationPrivate::platformTheme();
    if (!platformTheme) {
        qWarning() << "No platform theme available from QGuiApplicationPrivate::platformTheme()";
        return;
    }

    const QPalette *buttonPalette = platformTheme->palette(QPlatformTheme::ButtonPalette);

    if (!buttonPalette) {
        qWarning() << "No ButtonPalette found. Falling back to SystemPalette.";
        buttonPalette = platformTheme->palette(QPlatformTheme::SystemPalette);
    }

    QTest::addRow("style=Fusion,fallbackStyle=Material")
        << QString::fromLatin1("Fusion") << QString::fromLatin1("Material")
        << buttonPalette->color(QPalette::Inactive, QPalette::ButtonText);
}

void tst_StyleImports::fallbackStyleShouldNotOverwriteTheme()
{
    QFETCH(QString, style);
    QFETCH(QString, fallbackStyle);
    QFETCH(QColor, expectedContentItemColor);

    QQuickStyle::setStyle(style);
    QQuickStyle::setFallbackStyle(fallbackStyle);

    QQmlApplicationEngine engine;
    engine.addImportPath(QLatin1String(":/"));
    engine.addImportPath(dataDirectory());
    engine.load(testFileUrl("applicationWindowWithButton.qml"));
    QVERIFY(!engine.rootObjects().isEmpty());
    QQuickWindow *window = qobject_cast<QQuickWindow*>(engine.rootObjects().first());
    QVERIFY(window);

    QObject *button = window->property("button").value<QObject*>();
    QVERIFY(button);

    QQuickIconLabel *contentItem = button->property("contentItem").value<QQuickIconLabel*>();
    QVERIFY(contentItem);

    // For example: the Fusion style provides Button.qml, so the Button's text color
    // should be that of QPalette::ButtonText from QQuickFusionTheme.
    QCOMPARE(contentItem->color(), expectedContentItemColor);
}

enum FallbackMethod {
    QmlDirImport,
    EnvVar
};

void tst_StyleImports::fallbackStyleThemeRespected_data()
{
    QTest::addColumn<QString>("qmlFilePath");
    QTest::addColumn<QString>("runtimeStyle");
    QTest::addColumn<FallbackMethod>("fallbackMethod");
    QTest::addColumn<Qt::ColorScheme>("colorScheme");
    QTest::addColumn<QColor>("expectedButtonTextColor");
    QTest::addColumn<QColor>("expectedWindowColor");

    // Taken from qquickmaterialstyle.cpp.
    static const QRgb materialBackgroundColorLight = 0xFFFFFBFE;
    static const QRgb materialBackgroundColorDark = 0xFF1C1B1F;

    // Notes:
    // - FileSystemStyle has blue button text.
    // - StyleThatImportsMaterial has red button text.
    // - All rows result in Material being the fallback.

    QTest::newRow("import controls, env var fallback, light") << "applicationWindowWithButton.qml"
        << "FileSystemStyle" << EnvVar << Qt::ColorScheme::Light
        << QColor::fromRgb(0x0000ff) << QColor::fromRgba(materialBackgroundColorLight);
    QTest::newRow("import controls, env var fallback, dark") << "applicationWindowWithButton.qml"
        << "FileSystemStyle" << EnvVar << Qt::ColorScheme::Dark
        << QColor::fromRgb(0x0000ff) << QColor::fromRgba(materialBackgroundColorDark);

    QTest::newRow("import style, qmldir fallback, light") << "importStyleWithQmlDirFallback.qml"
        << "" << QmlDirImport << Qt::ColorScheme::Light
        << QColor::fromRgb(0xff0000) << QColor::fromRgba(materialBackgroundColorLight);
    QTest::newRow("import style, qmldir fallback, dark") << "importStyleWithQmlDirFallback.qml"
        << "" << QmlDirImport << Qt::ColorScheme::Dark
        << QColor::fromRgb(0xff0000) << QColor::fromRgba(materialBackgroundColorDark);
}

// Tests that a fallback style's (the Material style, in this case) theme settings
// are respected for both run-time and compile-time style selection.
void tst_StyleImports::fallbackStyleThemeRespected()
{
    QFETCH(QString, qmlFilePath);
    QFETCH(QString, runtimeStyle);
    QFETCH(FallbackMethod, fallbackMethod);
    QFETCH(Qt::ColorScheme, colorScheme);
    QFETCH(QColor, expectedButtonTextColor);
    QFETCH(QColor, expectedWindowColor);

    const char *materialThemeEnvVarName = "QT_QUICK_CONTROLS_MATERIAL_THEME";
    const QString originalMaterialTheme = qgetenv(materialThemeEnvVarName);
    qputenv(materialThemeEnvVarName, colorScheme == Qt::ColorScheme::Light ? "Light" : "Dark");

    // Only set this if it's not empty, because setting an empty style
    // will still cause it be resolved and we end up using the platform default.
    if (!runtimeStyle.isEmpty())
        QQuickStyle::setStyle(runtimeStyle);

    const char *fallbackStyleEnvVarName = "QT_QUICK_CONTROLS_FALLBACK_STYLE";
    const QString originalFallbackStyle = qgetenv(fallbackStyleEnvVarName);
    if (fallbackMethod == EnvVar)
        qputenv(fallbackStyleEnvVarName, "Material");

    auto cleanup = qScopeGuard([&]() {
        qputenv(materialThemeEnvVarName, qPrintable(originalMaterialTheme));
        qputenv(fallbackStyleEnvVarName, qPrintable(originalFallbackStyle));
    });

    QQuickControlsApplicationHelper helper(this, qmlFilePath, {},
        QStringList() << dataDirectory() + QLatin1String("/styles"));
    QVERIFY2(helper.ready, helper.failureMessage());

    auto button = helper.window->property("button").value<QQuickButton*>();
    QVERIFY(button);
    // contentItem should be a label with "salmon" text color.
    QCOMPARE(button->contentItem()->property("color").value<QColor>(), expectedButtonTextColor);
    QCOMPARE(helper.appWindow->color(), expectedWindowColor);

    // If using run-time style selection, check that QQuickStyle reports the correct values.
    // QQuickStyle is not supported when using compile-time style selection.
    if (!runtimeStyle.isEmpty()) {
        QCOMPARE(QQuickStyle::name(), runtimeStyle);
        QCOMPARE(QQuickStylePrivate::fallbackStyle(), "Material");
    }
}

void tst_StyleImports::attachedTypesAvailable_data()
{
    QTest::addColumn<QString>("import");

    // QtQuick.Controls import.
    QTest::newRow("Controls") << "";

    // use builtins style, otherwise testStyles() returns the style last set by a previous test
    QQuickStyle::setStyle(QLatin1String(""));
    const QStringList styles = testStyles();
    for (const QString &styleImport : styles)
        QTest::newRow(qPrintable(styleImport)) << styleImport;
}

void tst_StyleImports::attachedTypesAvailable()
{
    QFETCH(QString, import);

    // If it's QtQuick.Controls, don't prepend anything.
    if (!import.isEmpty())
        import.prepend(QLatin1Char('.'));

    // Should not warn about missing types.
    QTest::failOnWarning(QRegularExpression(".?"));

    QQmlEngine engine;
    QQmlComponent c(&engine);
    c.setData(QString::fromLatin1(R"(
        import QtQuick
        import QtQuick.Controls%1

        Item {
            SplitView {
                handle: Rectangle {
                    opacity: SplitHandle.hovered || SplitHandle.pressed ? 1.0 : 0.0
                }
                Item {} // Need these to ensure the handle is actually created, otherwise we won't get warnings.
                Item {}
            }

            Dialog {
                anchors.centerIn: Overlay.overlay
            }
        }
    )").arg(import).toLatin1(), QUrl());
    QVERIFY2(c.isReady(), qPrintable(c.errorString()));
    QScopedPointer<QObject> o(c.create());
    QVERIFY(!o.isNull());
}

void tst_StyleImports::nativeStyle()
{
    QQmlEngine engine;
    QQmlComponent c(&engine, testFileUrl("nativeStyle.qml"));
    QVERIFY2(c.isReady(), qPrintable(c.errorString()));
    std::unique_ptr<QObject> o(c.create());
    QQmlData *ddata = QQmlData::get(o.get());

#if defined(Q_OS_ANDROID)
    const QByteArray defaultStyle = "Material";
#elif defined(Q_OS_IOS)
    const QByteArray defaultStyle = "iOS";
#elif defined(Q_OS_LINUX)
    const QByteArray defaultStyle = "Fusion";
#elif defined(Q_OS_MACOS)
    const QByteArray defaultStyle = "macOS";
#elif defined(Q_OS_WINDOWS)
    const QByteArray defaultStyle = "Windows";
#else
    const QByteArray defaultStyle = "Basic";
#endif

    // We have a separate Button.qml for each platform default style.
    const QUrl url(QLatin1String("qrc:/qt-project.org/imports/QtQuick/Controls/%1/Button.qml")
                           .arg(defaultStyle));

    // nativeStyle.qml does not form its own QQmlType because it has no own members
    // and isn't addresssible. Therefore we directly get the base QQmlType here.
    QCOMPARE(ddata->compilationUnit->baseCompilationUnit()->qmlType.sourceUrl(), url);
}

QTEST_MAIN(tst_StyleImports)

#include "tst_styleimports.moc"
