// 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 <QtTest/QtTest>

#include "qeventtransition.h"
#include "qhistorystate.h"
#include "qstate.h"
#include "qstatemachine.h"
#include "qsignaltransition.h"

class tst_QState : public QObject
{
    Q_OBJECT

private slots:
    void assignProperty();
    void assignPropertyTwice();
    void historyInitialState();
    void transitions();
    void privateSignals();
    void parallelStateAndInitialState();
};

class TestClass: public QObject
{
    Q_OBJECT
public:
    TestClass() : called(false) {}
    bool called;

public slots:
    void slot() { called = true; }


};

void tst_QState::assignProperty()
{
    QStateMachine machine;

    QObject object;
    object.setProperty("fooBar", 10);

    QState *s1 = new QState(&machine);
    s1->assignProperty(&object, "fooBar", 20);

    machine.setInitialState(s1);
    machine.start();

    QTRY_COMPARE(object.property("fooBar").toInt(), 20);
}

void tst_QState::assignPropertyTwice()
{
    QStateMachine machine;

    QObject object;
    object.setProperty("fooBar", 10);

    QState *s1 = new QState(&machine);
    s1->assignProperty(&object, "fooBar", 20);
    s1->assignProperty(&object, "fooBar", 30);

    machine.setInitialState(s1);
    machine.start();

    QTRY_COMPARE(object.property("fooBar").toInt(), 30);
}

class EventTestTransition: public QAbstractTransition
{
public:
    EventTestTransition(QEvent::Type type, QState *targetState)
        : QAbstractTransition(), m_type(type)
    {
        setTargetState(targetState);
    }

protected:
    bool eventTest(QEvent *e) override
    {
        return e->type() == m_type;
    }

    void onTransition(QEvent *) override {}

private:
    QEvent::Type m_type;

};

void tst_QState::historyInitialState()
{
    QStateMachine machine;

    QState *s1 = new QState(&machine);

    QState *s2 = new QState(&machine);
    QHistoryState *h1 = new QHistoryState(s2);

    s2->setInitialState(h1);

    QState *s3 = new QState(s2);
    h1->setDefaultState(s3);

    QState *s4 = new QState(s2);

    s1->addTransition(new EventTestTransition(QEvent::User, s2));
    s2->addTransition(new EventTestTransition(QEvent::User, s1));
    s3->addTransition(new EventTestTransition(QEvent::Type(QEvent::User+1), s4));

    machine.setInitialState(s1);
    machine.start();
    QCoreApplication::processEvents();

    QTRY_COMPARE(machine.configuration().size(), 1);
    QTRY_VERIFY(machine.configuration().contains(s1));

    machine.postEvent(new QEvent(QEvent::User));
    QCoreApplication::processEvents();

    QTRY_COMPARE(machine.configuration().size(), 2);
    QTRY_VERIFY(machine.configuration().contains(s2));
    QTRY_VERIFY(machine.configuration().contains(s3));

    machine.postEvent(new QEvent(QEvent::User));
    QCoreApplication::processEvents();

    QTRY_COMPARE(machine.configuration().size(), 1);
    QTRY_VERIFY(machine.configuration().contains(s1));

    machine.postEvent(new QEvent(QEvent::User));
    QCoreApplication::processEvents();

    QTRY_COMPARE(machine.configuration().size(), 2);
    QTRY_VERIFY(machine.configuration().contains(s2));
    QTRY_VERIFY(machine.configuration().contains(s3));

    machine.postEvent(new QEvent(QEvent::Type(QEvent::User+1)));
    QCoreApplication::processEvents();

    QTRY_COMPARE(machine.configuration().size(), 2);
    QTRY_VERIFY(machine.configuration().contains(s2));
    QTRY_VERIFY(machine.configuration().contains(s4));

    machine.postEvent(new QEvent(QEvent::User));
    QCoreApplication::processEvents();

    QTRY_COMPARE(machine.configuration().size(), 1);
    QTRY_VERIFY(machine.configuration().contains(s1));

    machine.postEvent(new QEvent(QEvent::User));
    QCoreApplication::processEvents();

    QTRY_COMPARE(machine.configuration().size(), 2);
    QTRY_VERIFY(machine.configuration().contains(s2));
    QTRY_VERIFY(machine.configuration().contains(s4));
}

void tst_QState::transitions()
{
    QState s1;
    QState s2;

    QVERIFY(s1.transitions().isEmpty());

    QAbstractTransition *t1 = s1.addTransition(this, SIGNAL(destroyed()), &s2);
    QAbstractTransition *t1_1 = s1.addTransition(this, &tst_QState::destroyed, &s2);
    QVERIFY(t1 != 0);
    QVERIFY(t1_1 != 0);
    QCOMPARE(s1.transitions().size(), 2);
    QCOMPARE(s1.transitions().first(), t1);
    QCOMPARE(s1.transitions().last(), t1_1);
    QVERIFY(s2.transitions().isEmpty());

    s1.removeTransition(t1);
    s1.removeTransition(t1_1);
    QVERIFY(s1.transitions().isEmpty());

    s1.addTransition(t1);
    QCOMPARE(s1.transitions().size(), 1);
    QCOMPARE(s1.transitions().first(), t1);

    QAbstractTransition *t2 = new QEventTransition(&s1);
    QCOMPARE(s1.transitions().size(), 2);
    QVERIFY(s1.transitions().contains(t1));
    QVERIFY(s1.transitions().contains(t2));

    // Transitions from child states should not be reported.
    QState *s21 = new QState(&s2);
    QAbstractTransition *t3 = s21->addTransition(this, SIGNAL(destroyed()), &s2);
    QVERIFY(s2.transitions().isEmpty());
    QCOMPARE(s21->transitions().size(), 1);
    QCOMPARE(s21->transitions().first(), t3);
}

class MyState : public QState
{
    Q_OBJECT
public:
    MyState(QState *parent = 0)
      : QState(parent)
    {

    }

    void emitPrivateSignals()
    {
        // These deliberately do not compile
//         emit entered();
//         emit exited();
//
//         emit entered(QPrivateSignal());
//         emit exited(QPrivateSignal());
//
//         emit entered(QAbstractState::QPrivateSignal());
//         emit exited(QAbstractState::QPrivateSignal());
    }

};

class MyTransition : public QSignalTransition
{
    Q_OBJECT
public:
    MyTransition(QObject * sender, const char * signal, QState *sourceState = 0)
      : QSignalTransition(sender, signal, sourceState)
    {

    }

    void emitPrivateSignals()
    {
        // These deliberately do not compile
//         emit triggered();
//
//         emit triggered(QPrivateSignal());
//
//         emit triggered(QAbstractTransition::QPrivateSignal());
    }
};

class SignalConnectionTester : public QObject
{
    Q_OBJECT
public:
    SignalConnectionTester(QObject *parent = 0)
      : QObject(parent), testPassed(false)
    {

    }

public Q_SLOTS:
    void testSlot()
    {
      testPassed = true;
    }

public:
    bool testPassed;
};

class TestTrigger : public QObject
{
  Q_OBJECT
public:
    TestTrigger(QObject *parent = 0)
      : QObject(parent)
    {

    }

    void emitTrigger()
    {
        emit trigger();
    }

signals:
    void trigger();
};

void tst_QState::privateSignals()
{
    QStateMachine machine;

    QState *s1 = new QState(&machine);
    MyState *s2 = new MyState(&machine);

    TestTrigger testTrigger;

    MyTransition *t1 = new MyTransition(&testTrigger, SIGNAL(trigger()), s1);
    s1->addTransition(t1);
    t1->setTargetState(s2);

    machine.setInitialState(s1);
    machine.start();
    QCoreApplication::processEvents();

    SignalConnectionTester s1Tester;
    SignalConnectionTester s2Tester;
    SignalConnectionTester t1Tester;

    QObject::connect(s1, &QState::exited, &s1Tester, &SignalConnectionTester::testSlot);
    QObject::connect(s2, &QState::entered, &s2Tester, &SignalConnectionTester::testSlot);
    QObject::connect(t1, &QSignalTransition::triggered, &t1Tester, &SignalConnectionTester::testSlot);

    testTrigger.emitTrigger();

    QCoreApplication::processEvents();

    QTRY_VERIFY(s1Tester.testPassed);
    QTRY_VERIFY(s2Tester.testPassed);
    QTRY_VERIFY(t1Tester.testPassed);

}

void tst_QState::parallelStateAndInitialState()
{
    QStateMachine machine;

    { // setting an initial state on a parallel state:
        QState a(QState::ParallelStates, &machine);
        QState b(&a);
        QVERIFY(!a.initialState());
        const QString warning
            = QString::asprintf("QState::setInitialState: ignoring attempt to set initial state of parallel state group %p", &a);
        QTest::ignoreMessage(QtWarningMsg, qPrintable(warning));
        a.setInitialState(&b); // should produce a warning and do nothing.
        QVERIFY(!a.initialState());
    }

    { // setting the child-mode from ExclusiveStates to ParallelStates should remove the initial state:
        QState a(QState::ExclusiveStates, &machine);
        QState b(&a);
        a.setInitialState(&b);
        QCOMPARE(a.initialState(), &b);
        const QString warning
            = QString::asprintf("QState::setChildMode: setting the child-mode of state %p to "
                                "parallel removes the initial state", &a);
        QTest::ignoreMessage(QtWarningMsg, qPrintable(warning));
        a.setChildMode(QState::ParallelStates); // should produce a warning and remove the initial state
        QVERIFY(!a.initialState());
        QCOMPARE(a.childMode(), QState::ParallelStates);
    }
}

QTEST_MAIN(tst_QState)
#include "tst_qstate.moc"
