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


#include <QTest>
#include <QtGui>
#include <QtWidgets>

#include <QtTest/private/qtesthelpers_p.h>

using namespace QTestPrivate;

class tst_QBoxLayout : public QObject
{
    Q_OBJECT

private slots:
    void cleanup();
    void insertSpacerItem();
    void insertLayout();
    void sizeHint();
    void sizeConstraints();
    void setGeometry();
    void setStyleShouldChangeSpacing();
    void widgetSurplus();

    void testLayoutEngine_data();
    void testLayoutEngine();

    void taskQTBUG_7103_minMaxWidthNotRespected();
    void taskQTBUG_27420_takeAtShouldUnparentLayout();
    void taskQTBUG_40609_addingWidgetToItsOwnLayout();
    void taskQTBUG_40609_addingLayoutToItself();
    void replaceWidget();
    void indexOf();
};

class CustomLayoutStyle : public QProxyStyle
{
    Q_OBJECT
public:
    CustomLayoutStyle() : QProxyStyle(QStyleFactory::create("windows"))
    {
        hspacing = 5;
        vspacing = 10;
    }

    virtual int pixelMetric(PixelMetric metric, const QStyleOption * option = nullptr,
                            const QWidget * widget = nullptr ) const override;

    int hspacing;
    int vspacing;
};

int CustomLayoutStyle::pixelMetric(PixelMetric metric, const QStyleOption * option /*= nullptr*/,
                                   const QWidget * widget /*= nullptr*/ ) const
{
    switch (metric) {
        case PM_LayoutLeftMargin:
            return 0;
        break;
        case PM_LayoutTopMargin:
            return 3;
        break;
        case PM_LayoutRightMargin:
            return 6;
        break;
        case PM_LayoutBottomMargin:
            return 9;
        break;
        case PM_LayoutHorizontalSpacing:
            return hspacing;
        case PM_LayoutVerticalSpacing:
            return vspacing;
        break;
        default:
            break;
    }
    return QProxyStyle::pixelMetric(metric, option, widget);
}

void tst_QBoxLayout::cleanup()
{
    QVERIFY(QApplication::topLevelWidgets().isEmpty());
}

void tst_QBoxLayout::insertSpacerItem()
{
    QWidget window;
    window.setWindowTitle(QTest::currentTestFunction());

    QSpacerItem *spacer1 = new QSpacerItem(20, 10, QSizePolicy::Expanding, QSizePolicy::Expanding);
    QSpacerItem *spacer2 = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Expanding);

    QBoxLayout *layout = new QHBoxLayout;
    layout->addWidget(new QLineEdit("Foooooooooooooooooooooooooo"));
    layout->addSpacerItem(spacer1);
    layout->addWidget(new QLineEdit("Baaaaaaaaaaaaaaaaaaaaaaaaar"));
    layout->insertSpacerItem(0, spacer2);
    window.setLayout(layout);

    QCOMPARE(layout->itemAt(0), spacer2);
    QCOMPARE(layout->itemAt(2), spacer1);

    window.show();
}

void tst_QBoxLayout::insertLayout()
{
    QWidget window;
    QVBoxLayout *vbox = new QVBoxLayout(&window);
    QScopedPointer<QVBoxLayout> dummyParentLayout(new QVBoxLayout);
    QHBoxLayout *subLayout = new QHBoxLayout;
    dummyParentLayout->addLayout(subLayout);
    QCOMPARE(subLayout->parent(), dummyParentLayout.data());
    QCOMPARE(dummyParentLayout->count(), 1);

    // add subLayout to another layout
    QTest::ignoreMessage(QtWarningMsg, "QLayout::addChildLayout: layout QHBoxLayout \"\" already has a parent");
    vbox->addLayout(subLayout);
    QCOMPARE((subLayout->parent() == vbox), (vbox->count() == 1));
}


void tst_QBoxLayout::sizeHint()
{
    QWidget window;
    window.setWindowTitle(QTest::currentTestFunction());
    QHBoxLayout *lay1 = new QHBoxLayout;
    QHBoxLayout *lay2 = new QHBoxLayout;
    QLabel *label = new QLabel("widget twooooooooooooooooooooooooooooooooooooooooooooooooooooooo");
    lay2->addWidget(label);
    lay1->addLayout(lay2);
    window.setLayout(lay1);
    window.show();
    QVERIFY(QTest::qWaitForWindowExposed(&window));
    label->setText("foooooooo baaaaaaar");
    QSize sh = lay1->sizeHint();
    QApplication::processEvents();
    // Note that this is not strictly required behaviour - actually
    // the preferred behaviour would be that sizeHint returns
    // the same value regardless of what's lying in the event queue.
    // (i.e. we would check for equality here instead)
    QVERIFY(lay1->sizeHint() != sh);
}

void tst_QBoxLayout::sizeConstraints()
{
    QWidget window;
    window.setWindowTitle(QTest::currentTestFunction());
    QHBoxLayout *lay = new QHBoxLayout;
    lay->addWidget(new QLabel("foooooooooooooooooooooooooooooooooooo"));
    lay->addWidget(new QLabel("baaaaaaaaaaaaaaaaaaaaaaaaaaaaaar"));
    lay->setSizeConstraint(QLayout::SetFixedSize);
    window.setLayout(lay);
    window.show();
    QVERIFY(QTest::qWaitForWindowExposed(&window));
    QSize sh = window.sizeHint();
    delete lay->takeAt(1);
    QVERIFY(sh.width() >= window.sizeHint().width() &&
            sh.height() >= window.sizeHint().height());

}

void tst_QBoxLayout::setGeometry()
{
    QWidget toplevel;
    toplevel.setWindowTitle(QTest::currentTestFunction());
    setFrameless(&toplevel);
    QWidget w(&toplevel);
    QVBoxLayout *lay = new QVBoxLayout;
    lay->setContentsMargins(0, 0, 0, 0);
    lay->setSpacing(0);
    QHBoxLayout *lay2 = new QHBoxLayout;
    QDial *dial = new QDial;
    lay2->addWidget(dial);
    lay2->setAlignment(Qt::AlignTop);
    lay2->setAlignment(Qt::AlignRight);
    lay->addLayout(lay2);
    w.setLayout(lay);
    toplevel.show();

    QRect newGeom(0, 0, 70, 70);
    lay2->setGeometry(newGeom);
    QVERIFY2(newGeom.contains(dial->geometry()), "dial->geometry() should be smaller and within newGeom");
}

void tst_QBoxLayout::setStyleShouldChangeSpacing()
{

    QWidget window;
    window.setWindowTitle(QTest::currentTestFunction());
    QHBoxLayout *hbox = new QHBoxLayout(&window);
    QPushButton *pb1 = new QPushButton(tr("The spacing between this"));
    QPushButton *pb2 = new QPushButton(tr("and this button should depend on the style of the parent widget"));;
    pb1->setAttribute(Qt::WA_LayoutUsesWidgetRect);
    pb2->setAttribute(Qt::WA_LayoutUsesWidgetRect);
    hbox->addWidget(pb1);
    hbox->addWidget(pb2);
    QScopedPointer<CustomLayoutStyle> style1(new CustomLayoutStyle);
    style1->hspacing = 6;
    window.setStyle(style1.data());
    window.show();
    QVERIFY(QTest::qWaitForWindowExposed(&window));

    auto spacing = [&]() { return pb2->geometry().left() - pb1->geometry().right() - 1; };
    QCOMPARE(spacing(), 6);

    QScopedPointer<CustomLayoutStyle> style2(new CustomLayoutStyle());
    style2->hspacing = 10;
    window.setStyle(style2.data());
    QTRY_COMPARE(spacing(), 10);
}

class MarginEatingStyle : public QProxyStyle
{
public:
    MarginEatingStyle() : QProxyStyle(QStyleFactory::create("windows"))
    {
    }

    virtual QRect subElementRect(SubElement sr, const QStyleOption *opt,
                                const QWidget *widget) const override
    {
        QRect rect = opt->rect;
        switch (sr) {
        case SE_GroupBoxLayoutItem:
            // this is a simplifed version of what the macOS style does
            rect.setTop(rect.top() + 20);
            rect.setLeft(rect.left() + 20);
            rect.setRight(rect.right() - 20);
            rect.setBottom(rect.bottom() - 20);
            break;
        default:
            return QProxyStyle::subElementRect(sr, opt, widget);
        }

        return rect;
    }
};

void tst_QBoxLayout::widgetSurplus()
{
    // Test case for QTBUG-67608 - a style requests space in the margin

    QDialog window;
    QScopedPointer<MarginEatingStyle> marginEater(new MarginEatingStyle);
    QVBoxLayout *vbox = new QVBoxLayout(&window);
    vbox->setContentsMargins(0, 0, 0, 0);
    vbox->setSpacing(0);

    QLabel *hiddenLabel = new QLabel(tr("Invisible label"));
    hiddenLabel->setVisible(false);

    QGroupBox *groupBox = new QGroupBox(tr("Groupbox Title"));
    groupBox->setStyle(marginEater.data());
    groupBox->setObjectName("Test group box");
    QPushButton *button1 = new QPushButton(tr("Button 1"));
    QPushButton *button2 = new QPushButton(tr("Button 2"));
    QVBoxLayout *groupLayout = new QVBoxLayout;
    groupLayout->addWidget(button1);
    groupLayout->addWidget(button2);
    groupBox->setLayout(groupLayout);

    QLabel *label = new QLabel(tr("Visible label"));

    vbox->addWidget(hiddenLabel);
    vbox->addWidget(groupBox);
    vbox->addWidget(label);
    window.setLayout(vbox);

    window.show();
    QVERIFY(QTest::qWaitForWindowExposed(&window));
    QCOMPARE(groupBox->y(), 0);
    QCOMPARE(groupBox->x(), 0);
}

void tst_QBoxLayout::taskQTBUG_7103_minMaxWidthNotRespected()
{
    QLabel *label = new QLabel("Qt uses standard C++, but makes extensive use of the C pre-processor to enrich the language. Qt can also be used in several other programming languages via language bindings. It runs on all major platforms, and has extensive internationalization support. Non-GUI features include SQL database access, XML parsing, thread management, network support and a unified cross-platform API for file handling.");
    label->setWordWrap(true);
    label->setFixedWidth(200);

    QVBoxLayout *layout = new QVBoxLayout;
    layout->addWidget(label);
    layout->addSpacerItem(new QSpacerItem(1, 1, QSizePolicy::Fixed, QSizePolicy::Expanding));

    QWidget widget;
    widget.setWindowTitle(QTest::currentTestFunction());
    widget.setLayout(layout);
    widget.show();
    QVERIFY(QTest::qWaitForWindowExposed(&widget));

    int height = label->height();

    QRect g = widget.geometry();
    g.setWidth(600);
    widget.setGeometry(g);

    QTest::qWait(50);

    QCOMPARE(label->height(), height);
}

void tst_QBoxLayout::taskQTBUG_27420_takeAtShouldUnparentLayout()
{
    QSharedPointer<QHBoxLayout> outer(new QHBoxLayout);
    QPointer<QVBoxLayout> inner = new QVBoxLayout;

    outer->addLayout(inner);
    QCOMPARE(outer->count(), 1);
    QCOMPARE(inner->parent(), outer.data());

    QLayoutItem *item = outer->takeAt(0);
    QCOMPARE(item->layout(), inner.data());
    QVERIFY(!item->layout()->parent());

    outer.reset();

    if (inner)
        delete item; // success: a taken item/layout should not be deleted when the old parent is deleted
    else
        QVERIFY(!inner.isNull());
}

void tst_QBoxLayout::taskQTBUG_40609_addingWidgetToItsOwnLayout(){
    QWidget widget;
    widget.setWindowTitle(QTest::currentTestFunction());
    widget.setObjectName("347b469225a24a0ef05150a");
    QVBoxLayout layout(&widget);
    layout.setObjectName("ef9e2b42298e0e6420105bb");

    QTest::ignoreMessage(QtWarningMsg, "QLayout: Cannot add a null widget to QVBoxLayout/ef9e2b42298e0e6420105bb");
    layout.addWidget(nullptr);
    QCOMPARE(layout.count(), 0);

    QTest::ignoreMessage(QtWarningMsg, "QLayout: Cannot add parent widget QWidget/347b469225a24a0ef05150a to its child layout QVBoxLayout/ef9e2b42298e0e6420105bb");
    layout.addWidget(&widget);
    QCOMPARE(layout.count(), 0);
}

void tst_QBoxLayout::taskQTBUG_40609_addingLayoutToItself(){
    QWidget widget;
    widget.setWindowTitle(QTest::currentTestFunction());
    widget.setObjectName("fe44e5cb6c08006597126a");
    QVBoxLayout layout(&widget);
    layout.setObjectName("cc751dd0f50f62b05a62da");

    QTest::ignoreMessage(QtWarningMsg, "QLayout: Cannot add a null layout to QVBoxLayout/cc751dd0f50f62b05a62da");
    layout.addLayout(nullptr);
    QCOMPARE(layout.count(), 0);

    QTest::ignoreMessage(QtWarningMsg, "QLayout: Cannot add layout QVBoxLayout/cc751dd0f50f62b05a62da to itself");
    layout.addLayout(&layout);
    QCOMPARE(layout.count(), 0);
}

struct Descr
{
    Descr(int min, int sh, int max = -1, bool exp= false, int _stretch = 0, bool _empty = false)
        :minimumSize(min), sizeHint(sh), maximumSize(max < 0 ? QLAYOUTSIZE_MAX : max),
         expanding(exp), stretch(_stretch), empty(_empty)
        {}

    int minimumSize;
    int sizeHint;
    int maximumSize;
    bool expanding;

    int stretch;

    bool empty;
};


typedef QList<Descr> DescrList;
Q_DECLARE_METATYPE(DescrList);

typedef QList<int> SizeList;
typedef QList<int> PosList;



class LayoutItem : public QLayoutItem
{
public:
    LayoutItem(const Descr &descr) :m_descr(descr) {}

    QSize sizeHint() const override { return QSize(m_descr.sizeHint, 100); }
    QSize minimumSize() const override { return QSize(m_descr.minimumSize, 0); }
    QSize maximumSize() const override { return QSize(m_descr.maximumSize, QLAYOUTSIZE_MAX); }
    Qt::Orientations expandingDirections() const override
    { return m_descr.expanding ? Qt::Horizontal :  Qt::Orientations{}; }
    void setGeometry(const QRect &r) override { m_pos = r.x(); m_size = r.width();}
    QRect geometry() const override { return QRect(m_pos, 0, m_size, 100); }
    bool isEmpty() const override { return m_descr.empty; }

private:
    Descr m_descr;
    int m_pos;
    int m_size;
};

void tst_QBoxLayout::testLayoutEngine_data()
{
    // (int min, int sh, int max = -1, bool exp= false, int _stretch = 0, bool _empty = false)
    QTest::addColumn<DescrList>("itemDescriptions");
    QTest::addColumn<int>("size");
    QTest::addColumn<int>("spacing");
    QTest::addColumn<PosList>("expectedPositions");
    QTest::addColumn<SizeList>("expectedSizes");

    QTest::newRow("Just one")
        << (DescrList() << Descr(0, 100))
        << 200
        << 0
        << (PosList() << 0)
        << (SizeList() << 200);

    QTest::newRow("Two non-exp")
        << (DescrList() << Descr(0, 100) << Descr(0,100))
        << 400
        << 0
        << (PosList() << 0 << 200)
        << (SizeList() << 200 << 200);

    QTest::newRow("Exp + non-exp")
        << (DescrList() << Descr(0, 100, -1, true) << Descr(0,100))
        << 400
        << 0
        << (PosList() << 0 << 300)
        << (SizeList() << 300 << 100);


    QTest::newRow("Stretch")
        << (DescrList() << Descr(0, 100, -1, false, 1) << Descr(0,100, -1, false, 2))
        << 300
        << 0
        << (PosList() << 0 << 100)
        << (SizeList() << 100 << 200);


    QTest::newRow("Spacing")
        << (DescrList() << Descr(0, 100) << Descr(0,100))
        << 400
        << 10
        << (PosList() << 0 << 205)
        << (SizeList() << 195 << 195);


    QTest::newRow("Less than minimum")
        << (DescrList() << Descr(100, 100, 100, false) << Descr(50, 100, 100, false))
        << 100
        << 0
        << (PosList() << 0 << 50)
        << (SizeList() << 50 << 50);


    QTest::newRow("Less than sizehint")
        << (DescrList() << Descr(100, 200, 100, false) << Descr(50, 200, 100, false))
        << 200
        << 0
        << (PosList() << 0 << 100)
        << (SizeList() << 100 << 100);

    QTest::newRow("Too much space")
        << (DescrList() << Descr(0, 100, 100, false) << Descr(0, 100, 100, false))
        << 500
        << 0
        << (PosList() << 100 << 300)
        << (SizeList() << 100 << 100);

    QTest::newRow("Empty")
        << (DescrList() << Descr(0, 100, 100) << Descr(0,0,-1, false, 0, true) << Descr(0, 100, 100) )
        << 500
        << 0
        << (PosList() << 100 << 300 << 300)
        << (SizeList() << 100 << 0 << 100);

    QTest::newRow("QTBUG-33104")
        << (DescrList() << Descr(11, 75, 75, true) << Descr(75, 75))
        << 200
        << 0
        << (PosList() << 0 << 75)
        << (SizeList() << 75 << 125);

    QTest::newRow("Expanding with maximumSize")
        << (DescrList() << Descr(11, 75, 100, true) << Descr(75, 75))
        << 200
        << 0
        << (PosList() << 0 << 100)
        << (SizeList() << 100 << 100);

    QTest::newRow("Stretch with maximumSize")
        << (DescrList() << Descr(11, 75, 100, false, 1) << Descr(75, 75))
        << 200
        << 0
        << (PosList() << 0 << 100)
        << (SizeList() << 100 << 100);

    QTest::newRow("Stretch with maximumSize last")
        << (DescrList()  << Descr(75, 75) << Descr(11, 75, 100, false, 1))
        << 200
        << 0
        << (PosList() << 0 << 100)
        << (SizeList() << 100 << 100);
}

void tst_QBoxLayout::testLayoutEngine()
{
    QFETCH(DescrList, itemDescriptions);
    QFETCH(int, size);
    QFETCH(int, spacing);
    QFETCH(PosList, expectedPositions);
    QFETCH(SizeList, expectedSizes);

    QHBoxLayout box;
    box.setSpacing(spacing);
    int i;
    for (i = 0; i < itemDescriptions.size(); ++i) {
         Descr descr = itemDescriptions.at(i);
         LayoutItem *li = new LayoutItem(descr);
         box.addItem(li);
         box.setStretch(i, descr.stretch);
    }
    box.setGeometry(QRect(0,0,size,100));
    for (i = 0; i < expectedSizes.size(); ++i) {
        int xSize = expectedSizes.at(i);
        int xPos = expectedPositions.at(i);
        QLayoutItem *item = box.itemAt(i);
        QCOMPARE(item->geometry().width(), xSize);
        QCOMPARE(item->geometry().x(), xPos);
    }
}

void tst_QBoxLayout::replaceWidget()
{
    QWidget w;
    QBoxLayout *boxLayout = new QVBoxLayout(&w);

    QLineEdit *replaceFrom = new QLineEdit;
    QLineEdit *replaceTo = new QLineEdit;
    boxLayout->addWidget(new QLineEdit());
    boxLayout->addWidget(replaceFrom);
    boxLayout->addWidget(new QLineEdit());

    QCOMPARE(boxLayout->indexOf(replaceFrom), 1);
    QCOMPARE(boxLayout->indexOf(replaceTo), -1);
    QCOMPARE(boxLayout->count(), 3);
    boxLayout->replaceWidget(replaceFrom, replaceFrom);
    QCOMPARE(boxLayout->count(), 3);

    delete boxLayout->replaceWidget(replaceFrom, replaceTo);

    QCOMPARE(boxLayout->indexOf(replaceFrom), -1);
    QCOMPARE(boxLayout->indexOf(replaceTo), 1);
}

void tst_QBoxLayout::indexOf()
{
    QWidget w;
    auto outer = new QVBoxLayout(&w);
    auto inner = new QHBoxLayout();
    outer->addLayout(inner);
    auto widget1 = new QWidget();
    QWidget widget2;
    inner->addWidget(widget1);

    QCOMPARE(inner->indexOf(widget1), 0);
    QCOMPARE(inner->indexOf(&widget2), -1);
    QCOMPARE(outer->indexOf(widget1), -1);
    QCOMPARE(outer->indexOf(&widget2), -1);
    QCOMPARE(outer->indexOf(outer), -1);
    QCOMPARE(outer->indexOf(inner), 0);
    QCOMPARE(inner->indexOf(inner->itemAt(0)), 0);
}

QTEST_MAIN(tst_QBoxLayout)
#include "tst_qboxlayout.moc"
