/****************************************************************************
**
** Copyright (C) 2017 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** BSD License Usage
** Alternatively, you may use this file under the terms of the BSD license
** as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
**   * Redistributions of source code must retain the above copyright
**     notice, this list of conditions and the following disclaimer.
**   * Redistributions in binary form must reproduce the above copyright
**     notice, this list of conditions and the following disclaimer in
**     the documentation and/or other materials provided with the
**     distribution.
**   * Neither the name of The Qt Company Ltd nor the names of its
**     contributors may be used to endorse or promote products derived
**     from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/

import QtQuick 2.12
import QtTest 1.0
import QtQuick.Controls 2.12

TestCase {
    id: testCase
    width: 200
    height: 200
    visible: true
    when: windowShown
    name: "StackView"

    Item { id: item }
    Component { id: textField; TextField { } }
    Component { id: component; Item { } }

    Component {
        id: stackView
        StackView { }
    }

    Component {
        id: signalSpy
        SignalSpy { }
    }

    function test_initialItem() {
        var control1 = createTemporaryObject(stackView, testCase)
        verify(control1)
        compare(control1.currentItem, null)
        control1.destroy()

        var control2 = createTemporaryObject(stackView, testCase, {initialItem: item})
        verify(control2)
        compare(control2.currentItem, item)
        control2.destroy()

        var control3 = createTemporaryObject(stackView, testCase, {initialItem: component})
        verify(control3)
        verify(control3.currentItem)
        control3.destroy()
    }

    function test_currentItem() {
        var control = createTemporaryObject(stackView, testCase, {initialItem: item})
        verify(control)
        compare(control.currentItem, item)
        control.push(component)
        verify(control.currentItem !== item)
        control.pop(StackView.Immediate)
        compare(control.currentItem, item)
    }

    function test_busy() {
        var control = createTemporaryObject(stackView, testCase)
        verify(control)
        compare(control.busy, false)

        var busyCount = 0
        var busySpy = signalSpy.createObject(control, {target: control, signalName: "busyChanged"})
        verify(busySpy.valid)

        control.push(component)
        compare(control.busy, false)
        compare(busySpy.count, busyCount)

        control.push(component)
        compare(control.busy, true)
        compare(busySpy.count, ++busyCount)
        tryCompare(control, "busy", false)
        compare(busySpy.count, ++busyCount)

        control.replace(component)
        compare(control.busy, true)
        compare(busySpy.count, ++busyCount)
        tryCompare(control, "busy", false)
        compare(busySpy.count, ++busyCount)

        control.pop()
        compare(control.busy, true)
        compare(busySpy.count, ++busyCount)
        tryCompare(control, "busy", false)
        compare(busySpy.count, ++busyCount)

        control.pushEnter = null
        control.pushExit = null

        control.push(component)
        compare(control.busy, false)
        compare(busySpy.count, busyCount)

        control.replaceEnter = null
        control.replaceExit = null

        control.replace(component)
        compare(control.busy, false)
        compare(busySpy.count, busyCount)

        control.popEnter = null
        control.popExit = null

        control.pop()
        compare(control.busy, false)
        compare(busySpy.count, busyCount)
    }

    function test_status() {
        var control = createTemporaryObject(stackView, testCase)
        verify(control)

        var item1 = component.createObject(control)
        compare(item1.StackView.status, StackView.Inactive)
        control.push(item1)
        compare(item1.StackView.status, StackView.Active)

        var item2 = component.createObject(control)
        compare(item2.StackView.status, StackView.Inactive)
        control.push(item2)
        compare(item2.StackView.status, StackView.Activating)
        compare(item1.StackView.status, StackView.Deactivating)
        tryCompare(item2.StackView, "status", StackView.Active)
        tryCompare(item1.StackView, "status", StackView.Inactive)

        control.pop()
        compare(item2.StackView.status, StackView.Deactivating)
        compare(item1.StackView.status, StackView.Activating)
        tryCompare(item2.StackView, "status", StackView.Inactive)
        tryCompare(item1.StackView, "status", StackView.Active)
    }

    function test_index() {
        var control = createTemporaryObject(stackView, testCase)
        verify(control)

        var item1 = component.createObject(control)
        compare(item1.StackView.index, -1)
        control.push(item1, StackView.Immediate)
        compare(item1.StackView.index, 0)

        var item2 = component.createObject(control)
        compare(item2.StackView.index, -1)
        control.push(item2, StackView.Immediate)
        compare(item2.StackView.index, 1)
        compare(item1.StackView.index, 0)

        control.pop(StackView.Immediate)
        compare(item2.StackView.index, -1)
        compare(item1.StackView.index, 0)
    }

    function test_view() {
        var control = createTemporaryObject(stackView, testCase)
        verify(control)

        var item1 = component.createObject(control)
        compare(item1.StackView.view, null)
        control.push(item1, StackView.Immediate)
        compare(item1.StackView.view, control)

        var item2 = component.createObject(control)
        compare(item2.StackView.view, null)
        control.push(item2, StackView.Immediate)
        compare(item2.StackView.view, control)
        compare(item1.StackView.view, control)

        control.pop(StackView.Immediate)
        compare(item2.StackView.view, null)
        compare(item1.StackView.view, control)
    }

    function test_depth() {
        var control = createTemporaryObject(stackView, testCase)
        verify(control)

        var depthChanges = 0
        var emptyChanges = 0
        var depthSpy = signalSpy.createObject(control, {target: control, signalName: "depthChanged"})
        var emptySpy = signalSpy.createObject(control, {target: control, signalName: "emptyChanged"})
        verify(depthSpy.valid)
        verify(emptySpy.valid)
        compare(control.depth, 0)
        compare(control.empty, true)

        control.push(item, StackView.Immediate)
        compare(control.depth, 1)
        compare(depthSpy.count, ++depthChanges)
        compare(control.empty, false)
        compare(emptySpy.count, ++emptyChanges)

        control.clear()
        compare(control.depth, 0)
        compare(depthSpy.count, ++depthChanges)
        compare(control.empty, true)
        compare(emptySpy.count, ++emptyChanges)

        control.push(component, StackView.Immediate)
        compare(control.depth, 1)
        compare(depthSpy.count, ++depthChanges)
        compare(control.empty, false)
        compare(emptySpy.count, ++emptyChanges)

        control.push(component, StackView.Immediate)
        compare(control.depth, 2)
        compare(depthSpy.count, ++depthChanges)
        compare(control.empty, false)
        compare(emptySpy.count, emptyChanges)

        control.replace(component, StackView.Immediate)
        compare(control.depth, 2)
        compare(depthSpy.count, depthChanges)
        compare(control.empty, false)
        compare(emptySpy.count, emptyChanges)

        control.replace([component, component], StackView.Immediate)
        compare(control.depth, 3)
        compare(depthSpy.count, ++depthChanges)
        compare(control.empty, false)
        compare(emptySpy.count, emptyChanges)

        control.pop(null, StackView.Immediate)
        compare(control.depth, 1)
        compare(depthSpy.count, ++depthChanges)
        compare(control.empty, false)
        compare(emptySpy.count, emptyChanges)

        control.pop(StackView.Immediate) // ignored
        compare(control.depth, 1)
        compare(depthSpy.count, depthChanges)
        compare(control.empty, false)
        compare(emptySpy.count, emptyChanges)

        control.clear()
        compare(control.depth, 0)
        compare(depthSpy.count, ++depthChanges)
        compare(control.empty, true)
        compare(emptySpy.count, ++emptyChanges)

        control.clear()
        compare(control.depth, 0)
        compare(depthSpy.count, depthChanges)
        compare(control.empty, true)
        compare(emptySpy.count, emptyChanges)
    }

    function test_size() {
        var container = createTemporaryObject(component, testCase, {width: 200, height: 200})
        verify(container)
        var control = stackView.createObject(container, {width: 100, height: 100})
        verify(control)

        container.width += 10
        container.height += 20
        compare(control.width, 100)
        compare(control.height, 100)

        control.push(item, StackView.Immediate)
        compare(item.width, control.width)
        compare(item.height, control.height)

        control.width = 200
        control.height = 200
        compare(item.width, control.width)
        compare(item.height, control.height)

        control.clear()
        control.width += 10
        control.height += 20
        verify(item.width !== control.width)
        verify(item.height !== control.height)

        control.push(item, StackView.Immediate)
        compare(item.width, control.width)
        compare(item.height, control.height)
    }

    function test_focus_data() {
        return [
            { tag: "true", focus: true, forceActiveFocus: false },
            { tag: "false", focus: false, forceActiveFocus: false },
            { tag: "forceActiveFocus()", focus: false, forceActiveFocus: true },
        ]
    }

    function test_focus(data) {
        var control = createTemporaryObject(stackView, testCase, {initialItem: item, width: 200, height: 200})
        verify(control)

        if (data.focus)
            control.focus = true
        if (data.forceActiveFocus)
            control.forceActiveFocus()
        compare(control.activeFocus, data.focus || data.forceActiveFocus)

        var page = control.push(textField, StackView.Immediate)
        verify(page)
        compare(control.currentItem, page)
        compare(page.activeFocus, control.activeFocus)

        control.pop(StackView.Immediate)
        compare(control.currentItem, item)
        compare(item.activeFocus, data.focus || data.forceActiveFocus)
        verify(!page.activeFocus)
    }

    function test_find() {
        var control = createTemporaryObject(stackView, testCase)
        verify(control)

        var item1 = component.createObject(control, {objectName: "1"})
        var item2 = component.createObject(control, {objectName: "2"})
        var item3 = component.createObject(control, {objectName: "3"})

        control.push(item1, StackView.Immediate)
        control.push(item2, StackView.Immediate)
        control.push(item3, StackView.Immediate)

        compare(control.find(function(item, index) { return index === 0 }), item1)
        compare(control.find(function(item) { return item.objectName === "1" }), item1)

        compare(control.find(function(item, index) { return index === 1 }), item2)
        compare(control.find(function(item) { return item.objectName === "2" }), item2)

        compare(control.find(function(item, index) { return index === 2 }), item3)
        compare(control.find(function(item) { return item.objectName === "3" }), item3)

        compare(control.find(function() { return false }), null)
        compare(control.find(function() { return true }), item3)
    }

    function test_get() {
        var control = createTemporaryObject(stackView, testCase)
        verify(control)

        control.push([item, component, component], StackView.Immediate)

        verify(control.get(0, StackView.DontLoad))
        compare(control.get(0, StackView.ForceLoad), item)

        verify(!control.get(1, StackView.DontLoad))

        verify(control.get(2, StackView.DontLoad))
        verify(control.get(2, StackView.ForceLoad))
    }

    function test_push() {
        var control = createTemporaryObject(stackView, testCase)
        verify(control)

        // missing arguments
        ignoreWarning(Qt.resolvedUrl("tst_stackview.qml") + ":69:9: QML StackView: push: missing arguments")
        compare(control.push(), null)

        // nothing to push
        ignoreWarning(Qt.resolvedUrl("tst_stackview.qml") + ":69:9: QML StackView: push: nothing to push")
        compare(control.push(StackView.Immediate), null)

        // unsupported type
        ignoreWarning(Qt.resolvedUrl("tst_stackview.qml") + ":69:9: QML StackView: push: QtObject is not supported. Must be Item or Component.")
        control.push(Qt.createQmlObject('import QtQml 2.0; QtObject { }', control))

        // push(item)
        var item1 = component.createObject(control, {objectName:"1"})
        compare(control.push(item1, StackView.Immediate), item1)
        compare(control.depth, 1)
        compare(control.currentItem, item1)

        // push([item])
        var item2 = component.createObject(control, {objectName:"2"})
        compare(control.push([item2], StackView.Immediate), item2)
        compare(control.depth, 2)
        compare(control.currentItem, item2)

        // push(item, {properties})
        var item3 = component.createObject(control)
        compare(control.push(item3, {objectName:"3"}, StackView.Immediate), item3)
        compare(item3.objectName, "3")
        compare(control.depth, 3)
        compare(control.currentItem, item3)

        // push([item, {properties}])
        var item4 = component.createObject(control)
        compare(control.push([item4, {objectName:"4"}], StackView.Immediate), item4)
        compare(item4.objectName, "4")
        compare(control.depth, 4)
        compare(control.currentItem, item4)

        // push(component, {properties})
        var item5 = control.push(component, {objectName:"5"}, StackView.Immediate)
        compare(item5.objectName, "5")
        compare(control.depth, 5)
        compare(control.currentItem, item5)

        // push([component, {properties}])
        var item6 = control.push([component, {objectName:"6"}], StackView.Immediate)
        compare(item6.objectName, "6")
        compare(control.depth, 6)
        compare(control.currentItem, item6)
    }

    function test_pop() {
        var control = createTemporaryObject(stackView, testCase)
        verify(control)

        var items = []
        for (var i = 0; i < 7; ++i)
            items.push(component.createObject(control, {objectName:i}))

        control.push(items, StackView.Immediate)

        ignoreWarning(Qt.resolvedUrl("tst_stackview.qml") + ":69:9: QML StackView: pop: too many arguments")
        compare(control.pop(1, 2, 3), null)

        // pop the top most item
        compare(control.pop(StackView.Immediate), items[6])
        compare(control.depth, 6)
        compare(control.currentItem, items[5])

        // pop down to the current item
        compare(control.pop(control.currentItem, StackView.Immediate), null)
        compare(control.depth, 6)
        compare(control.currentItem, items[5])

        // pop down to (but not including) the Nth item
        compare(control.pop(items[3], StackView.Immediate), items[5])
        compare(control.depth, 4)
        compare(control.currentItem, items[3])

        // pop the top most item
        compare(control.pop(undefined, StackView.Immediate), items[3])
        compare(control.depth, 3)
        compare(control.currentItem, items[2])

        // don't pop non-existent item
        ignoreWarning(Qt.resolvedUrl("tst_stackview.qml") + ":69:9: QML StackView: pop: unknown argument: " + testCase)
        compare(control.pop(testCase, StackView.Immediate), null)
        compare(control.depth, 3)
        compare(control.currentItem, items[2])

        // pop all items down to (but not including) the 1st item
        control.pop(null, StackView.Immediate)
        compare(control.depth, 1)
        compare(control.currentItem, items[0])
    }

    function test_replace() {
        var control = createTemporaryObject(stackView, testCase)
        verify(control)

        // missing arguments
        ignoreWarning(Qt.resolvedUrl("tst_stackview.qml") + ":69:9: QML StackView: replace: missing arguments")
        compare(control.replace(), null)

        // nothing to push
        ignoreWarning(Qt.resolvedUrl("tst_stackview.qml") + ":69:9: QML StackView: replace: nothing to push")
        compare(control.replace(StackView.Immediate), null)

        // unsupported type
        ignoreWarning(Qt.resolvedUrl("tst_stackview.qml") + ":69:9: QML StackView: replace: QtObject is not supported. Must be Item or Component.")
        compare(control.replace(Qt.createQmlObject('import QtQml 2.0; QtObject { }', control)), null)

        // replace(item)
        var item1 = component.createObject(control, {objectName:"1"})
        compare(control.replace(item1, StackView.Immediate), item1)
        compare(control.depth, 1)
        compare(control.currentItem, item1)

        // replace([item])
        var item2 = component.createObject(control, {objectName:"2"})
        compare(control.replace([item2], StackView.Immediate), item2)
        compare(control.depth, 1)
        compare(control.currentItem, item2)

        // replace(item, {properties})
        var item3 = component.createObject(control)
        compare(control.replace(item3, {objectName:"3"}, StackView.Immediate), item3)
        compare(item3.objectName, "3")
        compare(control.depth, 1)
        compare(control.currentItem, item3)

        // replace([item, {properties}])
        var item4 = component.createObject(control)
        compare(control.replace([item4, {objectName:"4"}], StackView.Immediate), item4)
        compare(item4.objectName, "4")
        compare(control.depth, 1)
        compare(control.currentItem, item4)

        // replace(component, {properties})
        var item5 = control.replace(component, {objectName:"5"}, StackView.Immediate)
        compare(item5.objectName, "5")
        compare(control.depth, 1)
        compare(control.currentItem, item5)

        // replace([component, {properties}])
        var item6 = control.replace([component, {objectName:"6"}], StackView.Immediate)
        compare(item6.objectName, "6")
        compare(control.depth, 1)
        compare(control.currentItem, item6)

        // replace the topmost item
        control.push(component)
        compare(control.depth, 2)
        var item7 = control.replace(control.get(1), component, StackView.Immediate)
        compare(control.depth, 2)
        compare(control.currentItem, item7)

        // replace the item in the middle
        control.push(component)
        control.push(component)
        control.push(component)
        compare(control.depth, 5)
        var item8 = control.replace(control.get(2), component, StackView.Immediate)
        compare(control.depth, 3)
        compare(control.currentItem, item8)
    }

    function test_clear() {
        var control = createTemporaryObject(stackView, testCase)
        verify(control)

        control.push(component, StackView.Immediate)

        control.clear()
        compare(control.depth, 0)
        compare(control.busy, false)

        control.push(component, StackView.Immediate)

        control.clear(StackView.PopTransition)
        compare(control.depth, 0)
        compare(control.busy, true)
        tryCompare(control, "busy", false)
    }

    function test_visibility_data() {
        return [
            {tag:"default transitions", properties: {}},
            {tag:"null transitions", properties: {pushEnter: null, pushExit: null, popEnter: null, popExit: null}}
        ]
    }

    function test_visibility(data) {
        var control = createTemporaryObject(stackView, testCase, data.properties)
        verify(control)

        var item1 = component.createObject(control)
        control.push(item1, StackView.Immediate)
        verify(item1.visible)

        var item2 = component.createObject(control)
        control.push(item2)
        tryCompare(item1, "visible", false)
        verify(item2.visible)

        control.pop()
        verify(item1.visible)
        tryCompare(item2, "visible", false)
    }

    Component {
        id: transitionView
        StackView {
            property int popEnterRuns
            property int popExitRuns
            property int pushEnterRuns
            property int pushExitRuns
            property int replaceEnterRuns
            property int replaceExitRuns
            popEnter: Transition {
                PauseAnimation { duration: 1 }
                onRunningChanged: if (!running) ++popEnterRuns
            }
            popExit: Transition {
                PauseAnimation { duration: 1 }
                onRunningChanged: if (!running) ++popExitRuns
            }
            pushEnter: Transition {
                PauseAnimation { duration: 1 }
                onRunningChanged: if (!running) ++pushEnterRuns
            }
            pushExit: Transition {
                PauseAnimation { duration: 1 }
                onRunningChanged: if (!running) ++pushExitRuns
            }
            replaceEnter: Transition {
                PauseAnimation { duration: 1 }
                onRunningChanged: if (!running) ++replaceEnterRuns
            }
            replaceExit: Transition {
                PauseAnimation { duration: 1 }
                onRunningChanged: if (!running) ++replaceExitRuns
            }
        }
    }

    function test_transitions_data() {
        return [
            { tag: "undefined", operation: undefined,
              pushEnterRuns: [0,1,1,1], pushExitRuns: [0,1,1,1], replaceEnterRuns: [0,0,1,1], replaceExitRuns: [0,0,1,1], popEnterRuns: [0,0,0,1], popExitRuns: [0,0,0,1] },
            { tag: "immediate", operation: StackView.Immediate,
              pushEnterRuns: [0,0,0,0], pushExitRuns: [0,0,0,0], replaceEnterRuns: [0,0,0,0], replaceExitRuns: [0,0,0,0], popEnterRuns: [0,0,0,0], popExitRuns: [0,0,0,0] },
            { tag: "push", operation: StackView.PushTransition,
              pushEnterRuns: [1,2,3,4], pushExitRuns: [0,1,2,3], replaceEnterRuns: [0,0,0,0], replaceExitRuns: [0,0,0,0], popEnterRuns: [0,0,0,0], popExitRuns: [0,0,0,0] },
            { tag: "pop", operation: StackView.PopTransition,
              pushEnterRuns: [0,0,0,0], pushExitRuns: [0,0,0,0], replaceEnterRuns: [0,0,0,0], replaceExitRuns: [0,0,0,0], popEnterRuns: [1,2,3,4], popExitRuns: [0,1,2,3] },
            { tag: "replace", operation: StackView.ReplaceTransition,
              pushEnterRuns: [0,0,0,0], pushExitRuns: [0,0,0,0], replaceEnterRuns: [1,2,3,4], replaceExitRuns: [0,1,2,3], popEnterRuns: [0,0,0,0], popExitRuns: [0,0,0,0] },
        ]
    }

    function test_transitions(data) {
        var control = createTemporaryObject(transitionView, testCase)
        verify(control)

        control.push(component, data.operation)
        tryCompare(control, "busy", false)
        compare(control.pushEnterRuns, data.pushEnterRuns[0])
        compare(control.pushExitRuns, data.pushExitRuns[0])
        compare(control.replaceEnterRuns, data.replaceEnterRuns[0])
        compare(control.replaceExitRuns, data.replaceExitRuns[0])
        compare(control.popEnterRuns, data.popEnterRuns[0])
        compare(control.popExitRuns, data.popExitRuns[0])

        control.push(component, data.operation)
        tryCompare(control, "busy", false)
        compare(control.pushEnterRuns, data.pushEnterRuns[1])
        compare(control.pushExitRuns, data.pushExitRuns[1])
        compare(control.replaceEnterRuns, data.replaceEnterRuns[1])
        compare(control.replaceExitRuns, data.replaceExitRuns[1])
        compare(control.popEnterRuns, data.popEnterRuns[1])
        compare(control.popExitRuns, data.popExitRuns[1])

        control.replace(component, data.operation)
        tryCompare(control, "busy", false)
        compare(control.pushEnterRuns, data.pushEnterRuns[2])
        compare(control.pushExitRuns, data.pushExitRuns[2])
        compare(control.replaceEnterRuns, data.replaceEnterRuns[2])
        compare(control.replaceExitRuns, data.replaceExitRuns[2])
        compare(control.popEnterRuns, data.popEnterRuns[2])
        compare(control.popExitRuns, data.popExitRuns[2])

        control.pop(data.operation)
        tryCompare(control, "busy", false)
        compare(control.pushEnterRuns, data.pushEnterRuns[3])
        compare(control.pushExitRuns, data.pushExitRuns[3])
        compare(control.replaceEnterRuns, data.replaceEnterRuns[3])
        compare(control.replaceExitRuns, data.replaceExitRuns[3])
        compare(control.popEnterRuns, data.popEnterRuns[3])
        compare(control.popExitRuns, data.popExitRuns[3])
    }

    TestItem {
        id: indestructibleItem
    }

    Component {
        id: destructibleComponent
        TestItem { }
    }

    function test_ownership_data() {
        return [
            {tag:"item, transition", arg: indestructibleItem, operation: StackView.Transition, destroyed: false},
            {tag:"item, immediate", arg: indestructibleItem, operation: StackView.Immediate, destroyed: false},
            {tag:"component, transition", arg: destructibleComponent, operation: StackView.Transition, destroyed: true},
            {tag:"component, immediate", arg: destructibleComponent, operation: StackView.Immediate, destroyed: true},
            {tag:"url, transition", arg: Qt.resolvedUrl("TestItem.qml"), operation: StackView.Transition, destroyed: true},
            {tag:"url, immediate", arg: Qt.resolvedUrl("TestItem.qml"), operation: StackView.Immediate, destroyed: true}
        ]
    }

    function test_ownership(data) {
        var control = createTemporaryObject(transitionView, testCase, {initialItem: component})
        verify(control)

        // push-pop
        control.push(data.arg, StackView.Immediate)
        verify(control.currentItem)
        verify(control.currentItem.hasOwnProperty("destroyedCallback"))
        var destroyed = false
        control.currentItem.destroyedCallback = function() { destroyed = true }
        control.pop(data.operation)
        tryCompare(control, "busy", false)
        wait(0) // deferred delete
        compare(destroyed, data.destroyed)

        // push-replace
        control.push(data.arg, StackView.Immediate)
        verify(control.currentItem)
        verify(control.currentItem.hasOwnProperty("destroyedCallback"))
        destroyed = false
        control.currentItem.destroyedCallback = function() { destroyed = true }
        control.replace(component, data.operation)
        tryCompare(control, "busy", false)
        wait(0) // deferred delete
        compare(destroyed, data.destroyed)
    }

    Component {
        id: removeComponent

        Item {
            objectName: "removeItem"
            StackView.onRemoved: destroy()
        }
    }

    function test_destroyOnRemoved() {
        var control = createTemporaryObject(stackView, testCase, { initialItem: component })
        verify(control)

        var item = removeComponent.createObject(control)
        verify(item)

        var removedSpy = signalSpy.createObject(control, { target: item.StackView, signalName: "removed" })
        verify(removedSpy)
        verify(removedSpy.valid)

        var destructionSpy = signalSpy.createObject(control, { target: item.Component, signalName: "destruction" })
        verify(destructionSpy)
        verify(destructionSpy.valid)

        // push-pop
        control.push(item, StackView.Immediate)
        compare(control.currentItem, item)
        control.pop(StackView.Transition)
        item = null
        tryCompare(removedSpy, "count", 1)
        tryCompare(destructionSpy, "count", 1)
        compare(control.busy, false)

        item = removeComponent.createObject(control)
        verify(item)

        removedSpy.target = item.StackView
        verify(removedSpy.valid)

        destructionSpy.target = item.Component
        verify(destructionSpy.valid)

        // push-replace
        control.push(item, StackView.Immediate)
        compare(control.currentItem, item)
        control.replace(component, StackView.Transition)
        item = null
        tryCompare(removedSpy, "count", 2)
        tryCompare(destructionSpy, "count", 2)
        compare(control.busy, false)
    }

    function test_pushOnRemoved() {
        var control = createTemporaryObject(stackView, testCase, { initialItem: component })
        verify(control)

        var item = control.push(component, StackView.Immediate)
        verify(item)

        item.StackView.onRemoved.connect(function() {
            ignoreWarning(/.*QML StackView: cannot push while already in the process of completing a pop/)
            control.push(component, StackView.Immediate)
        })

        // don't crash (QTBUG-62153)
        control.pop(StackView.Immediate)
    }

    Component {
        id: attachedItem
        Item {
            property int index: StackView.index
            property StackView view: StackView.view
            property int status: StackView.status
        }
    }

    function test_attached() {
        var control = createTemporaryObject(stackView, testCase, {initialItem: attachedItem})

        compare(control.get(0).index, 0)
        compare(control.get(0).view, control)
        compare(control.get(0).status, StackView.Active)

        control.push(attachedItem, StackView.Immediate)

        compare(control.get(0).index, 0)
        compare(control.get(0).view, control)
        compare(control.get(0).status, StackView.Inactive)

        compare(control.get(1).index, 1)
        compare(control.get(1).view, control)
        compare(control.get(1).status, StackView.Active)

        control.pop(StackView.Immediate)

        compare(control.get(0).index, 0)
        compare(control.get(0).view, control)
        compare(control.get(0).status, StackView.Active)
    }

    Component {
        id: testButton
        Button {
            property int clicks: 0
            onClicked: ++clicks
        }
    }

    function test_interaction() {
        var control = createTemporaryObject(stackView, testCase, {initialItem: testButton, width: testCase.width, height: testCase.height})
        verify(control)

        var firstButton = control.currentItem
        verify(firstButton)

        var firstClicks = 0
        var secondClicks = 0
        var thirdClicks = 0

        // push - default transition
        var secondButton = control.push(testButton)
        compare(control.busy, true)
        mouseClick(firstButton) // filtered while busy
        mouseClick(secondButton) // filtered while busy
        compare(firstButton.clicks, firstClicks)
        compare(secondButton.clicks, secondClicks)
        tryCompare(control, "busy", false)
        mouseClick(secondButton)
        compare(secondButton.clicks, ++secondClicks)

        // replace - default transition
        var thirdButton = control.replace(testButton)
        compare(control.busy, true)
        mouseClick(secondButton) // filtered while busy
        mouseClick(thirdButton) // filtered while busy
        compare(secondButton.clicks, secondClicks)
        compare(thirdButton.clicks, thirdClicks)
        tryCompare(control, "busy", false)
        secondButton = null
        secondClicks = 0
        mouseClick(thirdButton)
        compare(thirdButton.clicks, ++thirdClicks)

        // pop - default transition
        control.pop()
        compare(control.busy, true)
        mouseClick(firstButton) // filtered while busy
        mouseClick(thirdButton) // filtered while busy
        compare(firstButton.clicks, firstClicks)
        compare(thirdButton.clicks, thirdClicks)
        tryCompare(control, "busy", false)
        thirdButton = null
        thirdClicks = 0
        mouseClick(firstButton)
        compare(firstButton.clicks, ++firstClicks)

        // push - immediate operation
        secondButton = control.push(testButton, StackView.Immediate)
        compare(control.busy, false)
        mouseClick(secondButton)
        compare(secondButton.clicks, ++secondClicks)

        // replace - immediate operation
        thirdButton = control.replace(testButton, StackView.Immediate)
        compare(control.busy, false)
        secondButton = null
        secondClicks = 0
        mouseClick(thirdButton)
        compare(thirdButton.clicks, ++thirdClicks)

        // pop - immediate operation
        control.pop(StackView.Immediate)
        compare(control.busy, false)
        thirdButton = null
        thirdClicks = 0
        mouseClick(firstButton)
        compare(firstButton.clicks, ++firstClicks)

        // push - null transition
        control.pushEnter = null
        control.pushExit = null
        secondButton = control.push(testButton)
        compare(control.busy, false)
        mouseClick(secondButton)
        compare(secondButton.clicks, ++secondClicks)

        // replace - null transition
        control.replaceEnter = null
        control.replaceExit = null
        thirdButton = control.replace(testButton)
        compare(control.busy, false)
        secondButton = null
        secondClicks = 0
        mouseClick(thirdButton)
        compare(thirdButton.clicks, ++thirdClicks)

        // pop - null transition
        control.popEnter = null
        control.popExit = null
        control.pop()
        compare(control.busy, false)
        thirdButton = null
        thirdClicks = 0
        mouseClick(firstButton)
        compare(firstButton.clicks, ++firstClicks)
    }

    Component {
        id: mouseArea
        MouseArea {
            property int presses: 0
            property int releases: 0
            property int clicks: 0
            property int doubleClicks: 0
            property int cancels: 0
            onPressed: ++presses
            onReleased: ++releases
            onClicked: ++clicks
            onDoubleClicked: ++doubleClicks
            onCanceled: ++cancels
        }
    }

    // QTBUG-50305
    function test_events() {
        var control = createTemporaryObject(stackView, testCase, {initialItem: mouseArea, width: testCase.width, height: testCase.height})
        verify(control)

        var testItem = control.currentItem
        verify(testItem)

        testItem.doubleClicked.connect(function() {
            control.push(mouseArea) // ungrab -> cancel
        })

        mouseDoubleClickSequence(testItem)
        compare(testItem.presses, 2)
        compare(testItem.releases, 2)
        compare(testItem.clicks, 1)
        compare(testItem.doubleClicks, 1)
        compare(testItem.pressed, false)
        compare(testItem.cancels, 0)
    }

    function test_ungrab() {
        var control = createTemporaryObject(stackView, testCase, {initialItem: mouseArea, width: testCase.width, height: testCase.height})
        verify(control)

        var testItem = control.currentItem
        verify(testItem)

        mousePress(testItem)
        control.push(mouseArea)
        tryCompare(control, "busy", false)
        mouseRelease(testItem)

        compare(testItem.presses, 1)
        compare(testItem.releases, 0)
        compare(testItem.clicks, 0)
        compare(testItem.doubleClicks, 0)
        compare(testItem.pressed, false)
        compare(testItem.cancels, 1)
    }

    function test_failures() {
        var control = createTemporaryObject(stackView, testCase, {initialItem: component})
        verify(control)

        var error = Qt.resolvedUrl("non-existent.qml") + ":-1 No such file or directory"

        ignoreWarning("QQmlComponent: Component is not ready")
        ignoreWarning(Qt.resolvedUrl("tst_stackview.qml") + ":69:9: QML StackView: push: " + error)
        control.push(Qt.resolvedUrl("non-existent.qml"))

        ignoreWarning("QQmlComponent: Component is not ready")
        ignoreWarning(Qt.resolvedUrl("tst_stackview.qml") + ":69:9: QML StackView: replace: " + error)
        control.replace(Qt.resolvedUrl("non-existent.qml"))

        ignoreWarning(Qt.resolvedUrl("tst_stackview.qml") + ":69:9: QML StackView: push: invalid url: x://[v]")
        control.push("x://[v]")

        ignoreWarning(Qt.resolvedUrl("tst_stackview.qml") + ":69:9: QML StackView: replace: invalid url: x://[v]")
        control.replace("x://[v]")

        control.pop()
    }

    Component {
        id: rectangle
        Rectangle {
            property color initialColor
            Component.onCompleted: initialColor = color
        }
    }

    function test_properties() {
        var control = createTemporaryObject(stackView, testCase)
        verify(control)

        var rect = control.push(rectangle, {color: "#ff0000"})
        compare(rect.color, "#ff0000")
        compare(rect.initialColor, "#ff0000")
    }

    Component {
        id: signalTest
        Control {
            id: ctrl
            property SignalSpy activatedSpy: SignalSpy { target: ctrl.StackView; signalName: "activated" }
            property SignalSpy activatingSpy: SignalSpy { target: ctrl.StackView; signalName: "activating" }
            property SignalSpy deactivatedSpy: SignalSpy { target: ctrl.StackView; signalName: "deactivated" }
            property SignalSpy deactivatingSpy: SignalSpy { target: ctrl.StackView; signalName: "deactivating" }
        }
    }

    function test_signals() {
        var control = createTemporaryObject(stackView, testCase)
        verify(control)

        var item1 = signalTest.createObject(control)
        compare(item1.StackView.status, StackView.Inactive)
        control.push(item1)
        compare(item1.StackView.status, StackView.Active)
        compare(item1.activatedSpy.count, 1)
        compare(item1.activatingSpy.count, 1)
        compare(item1.deactivatedSpy.count, 0)
        compare(item1.deactivatingSpy.count, 0)

        var item2 = signalTest.createObject(control)
        compare(item2.StackView.status, StackView.Inactive)
        control.push(item2)
        compare(item2.StackView.status, StackView.Activating)
        compare(item2.activatedSpy.count, 0)
        compare(item2.activatingSpy.count, 1)
        compare(item2.deactivatedSpy.count, 0)
        compare(item2.deactivatingSpy.count, 0)
        compare(item1.StackView.status, StackView.Deactivating)
        compare(item1.activatedSpy.count, 1)
        compare(item1.activatingSpy.count, 1)
        compare(item1.deactivatedSpy.count, 0)
        compare(item1.deactivatingSpy.count, 1)
        tryCompare(item2.activatedSpy, "count", 1)
        tryCompare(item1.deactivatedSpy, "count", 1)

        control.pop()
        compare(item2.StackView.status, StackView.Deactivating)
        compare(item2.activatedSpy.count, 1)
        compare(item2.activatingSpy.count, 1)
        compare(item2.deactivatedSpy.count, 0)
        compare(item2.deactivatingSpy.count, 1)
        compare(item1.StackView.status, StackView.Activating)
        compare(item1.activatedSpy.count, 1)
        compare(item1.activatingSpy.count, 2)
        compare(item1.deactivatedSpy.count, 1)
        compare(item1.deactivatingSpy.count, 1)
        tryCompare(item1.activatedSpy, "count", 2)
    }

    // QTBUG-56158
    function test_repeatedPop() {
        var control = createTemporaryObject(stackView, testCase, {initialItem: component, width: testCase.width, height: testCase.height})
        verify(control)

        for (var i = 0; i < 12; ++i)
            control.push(component)
        tryCompare(control, "busy", false)

        while (control.depth > 1) {
            control.pop()
            wait(50)
        }
        tryCompare(control, "busy", false)
    }

    function test_pushSameItem() {
        var control = createTemporaryObject(stackView, testCase)
        verify(control)

        control.push(item, StackView.Immediate)
        compare(control.currentItem, item)
        compare(control.depth, 1)

        // Pushing the same Item should do nothing.
        ignoreWarning(Qt.resolvedUrl("tst_stackview.qml") + ":69:9: QML StackView: push: nothing to push")
        control.push(item, StackView.Immediate)
        compare(control.currentItem, item)
        compare(control.depth, 1)

        // Push a component so that it becomes current.
        var current = control.push(component, StackView.Immediate)
        compare(control.currentItem, current)
        compare(control.depth, 2)

        // Push a bunch of stuff. "item" is already in the stack, so it should be ignored.
        current = control.push(component, item, StackView.Immediate)
        verify(current !== item)
        compare(control.currentItem, current)
        compare(control.depth, 3)
    }

    function test_visible() {
        var control = createTemporaryObject(stackView, testCase)
        verify(control)

        var item1 = component.createObject(control)
        control.push(item1, StackView.Immediate)
        compare(item1.visible, true)
        compare(item1.StackView.visible, item1.visible)

        var item2 = component.createObject(control)
        control.push(item2, StackView.Immediate)
        compare(item1.visible, false)
        compare(item2.visible, true)
        compare(item1.StackView.visible, false)
        compare(item2.StackView.visible, true)

        // keep explicitly visible
        item2.StackView.visible = true
        control.push(component, StackView.Immediate)
        compare(item2.visible, true)
        compare(item2.StackView.visible, true)

        // show underneath
        item1.StackView.visible = true
        compare(item1.visible, true)
        compare(item1.StackView.visible, true)

        control.pop(StackView.Immediate)
        compare(item2.visible, true)
        compare(item2.StackView.visible, true)

        // hide the top-most
        item2.StackView.visible = false
        compare(item2.visible, false)
        compare(item2.StackView.visible, false)

        // reset the top-most
        item2.StackView.visible = undefined
        compare(item2.visible, true)
        compare(item2.StackView.visible, true)

        // reset underneath
        item1.StackView.visible = undefined
        compare(item1.visible, false)
        compare(item1.StackView.visible, false)

        control.pop(StackView.Immediate)
        compare(item1.visible, true)
        compare(item1.StackView.visible, true)
    }

    function test_resolveInitialItem() {
        var control = createTemporaryObject(stackView, testCase, {initialItem: "TestItem.qml"})
        verify(control)
        verify(control.currentItem)
    }

    function test_resolve() {
        var control = createTemporaryObject(stackView, testCase)
        verify(control)

        var item = control.push("TestItem.qml")
        compare(control.depth, 1)
        verify(item)
    }

    // QTBUG-65084
    function test_mouseArea() {
        var ma = createTemporaryObject(mouseArea, testCase, {width: testCase.width, height: testCase.height})
        verify(ma)

        var control = stackView.createObject(ma, {width: testCase.width, height: testCase.height})
        verify(control)

        mousePress(control)
        verify(ma.pressed)

        mouseRelease(control)
        verify(!ma.pressed)

        var touch = touchEvent(control)
        touch.press(0, control).commit()
        verify(ma.pressed)

        touch.release(0, control).commit()
        verify(!ma.pressed)
    }

    // Separate function to ensure that the temporary value created to hold the return value of the Qt.createComponent()
    // call is out of scope when the caller calls gc().
    function stackViewFactory()
    {
        return createTemporaryObject(stackView, testCase, {initialItem: Qt.createComponent("TestItem.qml")})
    }

    function test_initalItemOwnership()
    {
        var control = stackViewFactory()
        verify(control)
        gc()
        verify(control.initialItem)
    }

    // Need to use this specific structure in order to reproduce the crash.
    Component {
        id: clearUponDestructionContainerComponent

        Item {
            id: container
            objectName: "container"

            property alias control: stackView
            property var onDestructionCallback

            property Component clearUponDestructionComponent: Component {
                id: clearUponDestructionComponent

                Item {
                    objectName: "clearUponDestructionItem"
                    Component.onDestruction: container.onDestructionCallback(stackView)
                }
            }

            StackView {
                id: stackView
                initialItem: Item {
                    objectName: "initialItem"
                }
            }
        }
    }

    // QTBUG-80353
    // Tests that calling clear() in Component.onDestruction in response to that
    // item being removed (e.g. via an earlier call to clear()) results in a warning and not a crash.
    function test_recursiveClearClear() {
        let container = createTemporaryObject(clearUponDestructionContainerComponent, testCase,
            { onDestructionCallback: function(stackView) { stackView.clear(StackView.Immediate) }})
        verify(container)

        let control = container.control
        control.push(container.clearUponDestructionComponent, StackView.Immediate)

        // Shouldn't crash.
        ignoreWarning(/.*cannot clear while already in the process of completing a clear/)
        control.clear(StackView.Immediate)
    }

    function test_recursivePopClear() {
        let container = createTemporaryObject(clearUponDestructionContainerComponent, testCase,
            { onDestructionCallback: function(stackView) { stackView.clear(StackView.Immediate) }})
        verify(container)

        let control = container.control
        control.push(container.clearUponDestructionComponent, StackView.Immediate)

        // Pop all items except the first, removing the second item we pushed in the process.
        // Shouldn't crash.
        ignoreWarning(/.*cannot clear while already in the process of completing a pop/)
        control.pop(null, StackView.Immediate)
    }

    function test_recursivePopPop() {
        let container = createTemporaryObject(clearUponDestructionContainerComponent, testCase,
            { onDestructionCallback: function(stackView) { stackView.pop(null, StackView.Immediate) }})
        verify(container)

        let control = container.control
        // Push an extra item so that we can call pop(null) and reproduce the conditions for the crash.
        control.push(component, StackView.Immediate)
        control.push(container.clearUponDestructionComponent, StackView.Immediate)

        // Pop the top item, then pop down to the first item in response.
        ignoreWarning(/.*cannot pop while already in the process of completing a pop/)
        control.pop(StackView.Immediate)
    }

    function test_recursiveReplaceClear() {
        let container = createTemporaryObject(clearUponDestructionContainerComponent, testCase,
            { onDestructionCallback: function(stackView) { stackView.clear(StackView.Immediate) }})
        verify(container)

        let control = container.control
        control.push(container.clearUponDestructionComponent, StackView.Immediate)

        // Replace the top item, then clear in response.
        ignoreWarning(/.*cannot clear while already in the process of completing a replace/)
        control.replace(component, StackView.Immediate)
    }

    function test_recursiveClearReplace() {
        let container = createTemporaryObject(clearUponDestructionContainerComponent, testCase,
            { onDestructionCallback: function(stackView) { stackView.replace(component, StackView.Immediate) }})
        verify(container)

        let control = container.control
        control.push(container.clearUponDestructionComponent, StackView.Immediate)

        // Replace the top item, then clear in response.
        ignoreWarning(/.*cannot replace while already in the process of completing a clear/)
        control.clear(StackView.Immediate)
    }

    Component {
        id: rectangleComponent
        Rectangle {}
    }

    Component {
        id: qtbug57267_StackViewComponent

        StackView {
            id: stackView

            popEnter: Transition {
                XAnimator { from: (stackView.mirrored ? -1 : 1) * -stackView.width; to: 0; duration: 400; easing.type: Easing.Linear }
            }
            popExit: Transition {
                XAnimator { from: 0; to: (stackView.mirrored ? -1 : 1) * stackView.width; duration: 400; easing.type: Easing.Linear }
            }
            pushEnter: Transition {
                XAnimator { from: (stackView.mirrored ? -1 : 1) * stackView.width; to: 0; duration: 400; easing.type: Easing.Linear }
            }
            pushExit: Transition {
                XAnimator { from: 0; to: (stackView.mirrored ? -1 : 1) * -stackView.width; duration: 400; easing.type: Easing.Linear }
            }
            replaceEnter: Transition {
                XAnimator { from: (stackView.mirrored ? -1 : 1) * stackView.width; to: 0; duration: 400; easing.type: Easing.Linear }
            }
            replaceExit: Transition {
                XAnimator { from: 0; to: (stackView.mirrored ? -1 : 1) * -stackView.width; duration: 400; easing.type: Easing.Linear }
            }
        }
    }

    function test_qtbug57267() {
        let redRect = createTemporaryObject(rectangleComponent, testCase, { color: "red" })
        verify(redRect)
        let blueRect = createTemporaryObject(rectangleComponent, testCase, { color: "blue" })
        verify(blueRect)
        let control = createTemporaryObject(qtbug57267_StackViewComponent, testCase,
            { "anchors.fill": testCase, initialItem: redRect })
        verify(control)

        control.replace(blueRect)
        compare(control.currentItem, blueRect)
        compare(control.depth, 1)

        // Wait until the animation has started and then interrupt it by pushing the redRect.
        tryCompare(control, "busy", true)
        control.replace(redRect)
        // The blue rect shouldn't be visible since we replaced it and therefore interrupted its animation.
        tryCompare(blueRect, "visible", false)
        // We did the replace very early on, so the transition for the redRect should still be happening.
        compare(control.busy, true)
        compare(redRect.visible, true)

        // After finishing the transition, the red rect should still be visible.
        tryCompare(control, "busy", false)
        compare(redRect.visible, true)
    }

    // QTBUG-84381
    function test_clearAndPushAfterDepthChange() {
        var control = createTemporaryObject(stackView, testCase, {
            popEnter: null, popExit: null, pushEnter: null,
            pushExit: null, replaceEnter: null, replaceExit: null
        })
        verify(control)

        control.depthChanged.connect(function() {
            if (control.depth === 2) {
                // Shouldn't assert.
                ignoreWarning(/.*QML StackView: cannot clear while already in the process of completing a push/)
                control.clear()
                // Shouldn't crash.
                ignoreWarning(/.*QML StackView: cannot push while already in the process of completing a push/)
                control.push(component)
            }
        })

        control.push(component)
        control.push(component)
    }
}
