"""
novelWriter – Main GUI Project Tree Class Tester
================================================

This file is a part of novelWriter
Copyright (C) 2020 Veronica Berglyd Olsen and novelWriter contributors

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations

from unittest.mock import MagicMock

import pytest

from PyQt5.QtCore import QEvent, QItemSelectionModel, QModelIndex, QPoint
from PyQt5.QtGui import QMouseEvent
from PyQt5.QtWidgets import QMenu, QMessageBox

from novelwriter import CONFIG, SHARED
from novelwriter.dialogs.docmerge import GuiDocMerge
from novelwriter.dialogs.docsplit import GuiDocSplit
from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.enum import nwDocMode, nwItemClass, nwItemLayout, nwItemType
from novelwriter.gui.projtree import _TreeContextMenu
from novelwriter.types import (
    QtAccepted, QtModNone, QtMouseLeft, QtMouseMiddle, QtRejected,
    QtScrollAlwaysOff, QtScrollAsNeeded
)

from tests.mocked import causeOSError
from tests.tools import C, buildTestProject


@pytest.mark.gui
def testGuiProjTree_NewTreeItem(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd):
    """Test adding items to the project tree."""
    monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))

    projView = nwGUI.projView
    projTree = projView.projTree
    project = SHARED.project
    tree = project.tree

    # Try to add item with no project
    projTree.newTreeItem(nwItemType.FILE)
    assert len(tree) == 0

    # Create a project
    buildTestProject(nwGUI, projPath)
    trash = tree.trash
    assert trash is not None
    assert len(tree) == 9
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Plot", "Characters", "Locations", "Trash",
    ]

    # Root Items
    # ==========

    # No class set
    projTree.newTreeItem(nwItemType.ROOT)
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Plot", "Characters", "Locations", "Trash",
    ]

    # Create Objects root item after Locations
    hObjectRoot = "0000000000011"
    projView.setSelectedHandle(C.hWorldRoot)
    projTree.newTreeItem(nwItemType.ROOT, nwItemClass.OBJECT)
    assert hObjectRoot in tree
    item = tree[hObjectRoot]
    assert item is not None
    assert item.itemName == "Objects"
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Plot", "Characters", "Locations", "Objects", "Trash",
    ]

    # File/Folder Items
    # =================

    # No location selected for new item
    projTree._clearSelection()

    caplog.clear()
    projTree.newTreeItem(nwItemType.FILE)
    assert "Did not find anywhere" in caplog.text

    caplog.clear()
    projTree.newTreeItem(nwItemType.FOLDER)
    assert "Did not find anywhere" in caplog.text

    # Try to add them to Trash
    projView.setSelectedHandle(trash.item.itemHandle)

    caplog.clear()
    projTree.newTreeItem(nwItemType.FILE)
    assert "Cannot add new files or folders" in caplog.text

    caplog.clear()
    projTree.newTreeItem(nwItemType.FOLDER)
    assert "Cannot add new files or folders" in caplog.text

    # Create new folder as child of Novel folder
    hNewFolder = "0000000000012"
    projView.setSelectedHandle(C.hNovelRoot, doScroll=True)
    projTree.newTreeItem(nwItemType.FOLDER)
    assert hNewFolder in tree
    item = tree[hNewFolder]
    assert item is not None
    assert item.itemName == "New Folder"
    assert item.itemParent == C.hNovelRoot
    assert item.itemRoot == C.hNovelRoot
    assert item.itemClass == nwItemClass.NOVEL
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "New Folder", "Plot", "Characters", "Locations", "Objects", "Trash",
    ]

    # Add a new file in the new folder
    hNewFile = "0000000000013"
    projView.setSelectedHandle(hNewFolder, doScroll=True)
    projTree.newTreeItem(nwItemType.FILE)
    assert hNewFile in tree
    item = tree[hNewFile]
    assert item is not None
    assert item.itemName == "New Document"
    assert item.itemParent == hNewFolder
    assert item.itemRoot == C.hNovelRoot
    assert item.itemClass == nwItemClass.NOVEL
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "New Folder", "New Document", "Plot", "Characters", "Locations",
        "Objects", "Trash",
    ]

    # Add a new chapter next to the other new file
    hNewChapter = "0000000000014"
    projView.setSelectedHandle(hNewFile, doScroll=True)
    projTree.newTreeItem(nwItemType.FILE, hLevel=2)
    assert hNewChapter in tree
    item = tree[hNewChapter]
    assert item is not None
    assert item.itemName == "New Chapter"
    assert item.itemParent == hNewFolder
    assert item.itemRoot == C.hNovelRoot
    assert item.itemClass == nwItemClass.NOVEL
    assert nwGUI.openDocument(hNewChapter)
    assert nwGUI.docEditor.getText() == "## New Chapter\n\n"
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "New Folder", "New Document", "New Chapter", "Plot", "Characters",
        "Locations", "Objects", "Trash",
    ]

    # Add a new scene next to the other new file
    hNewScene = "0000000000015"
    projView.setSelectedHandle(hNewChapter, doScroll=True)
    projTree.newTreeItem(nwItemType.FILE, hLevel=3)
    assert hNewScene in tree
    item = tree[hNewScene]
    assert item is not None
    assert item.itemName == "New Scene"
    assert item.itemParent == hNewFolder
    assert item.itemRoot == C.hNovelRoot
    assert item.itemClass == nwItemClass.NOVEL
    assert nwGUI.openDocument(hNewScene)
    assert nwGUI.docEditor.getText() == "### New Scene\n\n"
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "New Folder", "New Document", "New Chapter", "New Scene", "Plot",
        "Characters", "Locations", "Objects", "Trash",
    ]

    # Add a new scene with the content copied from the previous
    nwGUI.openDocument(hNewScene)
    nwGUI.docEditor.setPlainText("### New Scene\n\nWith Stuff\n\n")
    nwGUI.saveDocument()

    hNewSceneCopy = "0000000000016"
    projView.setSelectedHandle(hNewScene, doScroll=True)
    projTree.newTreeItem(nwItemType.FILE, copyDoc=hNewScene)
    assert hNewSceneCopy in tree
    item = tree[hNewSceneCopy]
    assert item is not None
    assert item.itemName == "New Scene"
    assert item.itemParent == hNewFolder
    assert item.itemRoot == C.hNovelRoot
    assert item.itemClass == nwItemClass.NOVEL
    assert nwGUI.openDocument(hNewSceneCopy)
    assert nwGUI.docEditor.getText() == "### New Scene\n\nWith Stuff\n\n"
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "New Folder", "New Document", "New Chapter", "New Scene", "New Scene",
        "Plot", "Characters", "Locations", "Objects", "Trash",
    ]

    # Add a new file to the characters folder
    hNewCharacter = "0000000000017"
    projView.setSelectedHandle(C.hCharRoot, doScroll=True)
    projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True)
    assert hNewCharacter in tree
    item = tree[hNewCharacter]
    assert item is not None
    assert item.itemName == "New Note"
    assert item.itemParent == C.hCharRoot
    assert item.itemRoot == C.hCharRoot
    assert item.itemClass == nwItemClass.CHARACTER
    assert nwGUI.openDocument(hNewCharacter)
    assert nwGUI.docEditor.getText() == "# New Note\n\n"
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "New Folder", "New Document", "New Chapter", "New Scene", "New Scene",
        "Plot", "Characters", "New Note", "Locations", "Objects", "Trash",
    ]

    # Cancel during creation
    with monkeypatch.context() as mp:
        mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("", False))
        projView.setSelectedHandle(hNewFile, doScroll=True)
        projTree.newTreeItem(nwItemType.FILE)
        assert [n.item.itemName for n in tree.model.root.allChildren()] == [
            "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
            "New Folder", "New Document", "New Chapter", "New Scene", "New Scene",
            "Plot", "Characters", "New Note", "Locations", "Objects", "Trash",
        ]

    # From Template
    # =============

    # Create template folder
    hTemplateRoot = "0000000000018"
    projView.setSelectedHandle(hObjectRoot)
    projTree.newTreeItem(nwItemType.ROOT, nwItemClass.TEMPLATE)
    assert hTemplateRoot in tree
    item = tree[hTemplateRoot]
    assert item is not None
    assert item.itemName == "Templates"
    assert item.itemParent is None
    assert item.itemRoot == hTemplateRoot
    assert item.itemClass == nwItemClass.TEMPLATE
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "New Folder", "New Document", "New Chapter", "New Scene", "New Scene",
        "Plot", "Characters", "New Note", "Locations", "Objects", "Templates",
        "Trash",
    ]

    # Create scene template
    hSceneTemplate = "0000000000019"
    projView.setSelectedHandle(hTemplateRoot, doScroll=True)
    projTree.newTreeItem(nwItemType.FILE, hLevel=3)
    assert hSceneTemplate in tree
    item = tree[hSceneTemplate]
    assert item is not None
    assert item.itemName == "New Scene"
    assert item.itemParent == hTemplateRoot
    assert item.itemRoot == hTemplateRoot
    assert item.itemClass == nwItemClass.TEMPLATE

    item.setName("New Scene Template")
    nwGUI.openDocument(hSceneTemplate)
    nwGUI.docEditor.setPlainText("### New Scene Template\n\nWith Stuff\n\n")
    nwGUI.saveDocument()
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "New Folder", "New Document", "New Chapter", "New Scene", "New Scene",
        "Plot", "Characters", "New Note", "Locations", "Objects", "Templates",
        "New Scene Template", "Trash",
    ]

    # Create from template
    hNewFromTemplate = "000000000001a"
    projView.setSelectedHandle(hNewSceneCopy, doScroll=True)
    projView.createFileFromTemplate(hSceneTemplate)
    assert hNewFromTemplate in tree
    item = tree[hNewFromTemplate]
    assert item is not None
    assert item.itemName == "New Scene Template"
    assert item.itemParent == hNewFolder
    assert item.itemRoot == C.hNovelRoot
    assert item.itemClass == nwItemClass.NOVEL
    assert nwGUI.openDocument(hNewFromTemplate)
    assert nwGUI.docEditor.getText() == "### New Scene Template\n\nWith Stuff\n\n"
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "New Folder", "New Document", "New Chapter", "New Scene", "New Scene",
        "New Scene Template", "Plot", "Characters", "New Note", "Locations",
        "Objects", "Templates", "New Scene Template", "Trash",
    ]

    # Rename Item
    # ===========

    # Rename plot folder
    with monkeypatch.context() as mp:
        mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("Stuff", True))
        projView.setSelectedHandle(C.hPlotRoot, doScroll=True)
        projView.renameTreeItem()
        item = tree[C.hPlotRoot]
        assert item is not None
        assert item.itemName == "Stuff"

    # Clean up
    # qtbot.stop()
    nwGUI.closeProject()


@pytest.mark.gui
def testGuiProjTree_SimpleOperations(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
    """Test simple operations in the project tree like internal move,
    change selection and expand/collapse nodes.
    """
    monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))

    projView = nwGUI.projView
    projTree = projView.projTree
    project = SHARED.project
    tree = project.tree
    model = tree.model

    # The default model is empty
    assert projView.getSelectedHandle() is None
    assert projTree._getModel() is None
    assert projTree._selectedRows() == []
    assert projTree._getNode(QModelIndex()) is None

    # Create a project
    buildTestProject(nwGUI, projPath)
    trash = tree.trash
    assert trash is not None
    assert len(tree) == 9
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Plot", "Characters", "Locations", "Trash",
    ]

    # Add some scenes
    hFolder = "0000000000011"
    hScenes = ["0000000000012", "0000000000013", "0000000000014", "0000000000015"]
    projView.setSelectedHandle(C.hNovelRoot, doScroll=True)
    projTree.newTreeItem(nwItemType.FOLDER)
    assert hFolder in tree
    item = tree[hFolder]
    assert item is not None
    item.setName("Chapter Folder")

    projView.setSelectedHandle(hFolder, doScroll=True)
    for n, hScene in enumerate(hScenes):
        projTree.newTreeItem(nwItemType.FILE, hLevel=3)
        assert hScene in tree
        item = tree[hScene]
        assert item is not None
        item.setName(f"Scene {n+1}")

    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
        "Plot", "Characters", "Locations", "Trash",
    ]

    # Expansion
    # =========

    # Default State
    assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == [
        C.hNovelRoot, hFolder
    ]

    # Expand Novel
    projTree.expandFromIndex(model.indexFromHandle(C.hNovelRoot))
    assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == [
        C.hNovelRoot, C.hChapterDir, hFolder
    ]

    # Collapse Novel
    projTree.collapseFromIndex(model.indexFromHandle(C.hNovelRoot))
    assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == [
        C.hNovelRoot
    ]

    # Move Up
    # =======

    # Already first
    projView.setSelectedHandle(hScenes[0], doScroll=True)
    projTree.moveItemUp()
    assert [n.item.itemName for n in tree.model.root.allChildren()][5:10] == [
        "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
    ]

    # Proper move
    projView.setSelectedHandle(hScenes[1], doScroll=True)
    projTree.moveItemUp()
    assert [n.item.itemName for n in tree.model.root.allChildren()][5:10] == [
        "Chapter Folder", "Scene 2", "Scene 1", "Scene 3", "Scene 4",
    ]

    # Move Down
    # =========

    # Already last
    projView.setSelectedHandle(hScenes[3], doScroll=True)
    projTree.moveItemDown()
    assert [n.item.itemName for n in tree.model.root.allChildren()][5:10] == [
        "Chapter Folder", "Scene 2", "Scene 1", "Scene 3", "Scene 4",
    ]

    # Proper move
    projView.setSelectedHandle(hScenes[1], doScroll=True)
    projTree.moveItemDown()
    assert [n.item.itemName for n in tree.model.root.allChildren()][5:10] == [
        "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
    ]

    # Select Up
    # =========

    # Sibling
    projView.setSelectedHandle(hFolder, doScroll=True)
    assert projView.getSelectedHandle() == hFolder
    projTree.goToSiblingUp()
    assert projView.getSelectedHandle() == C.hChapterDir
    projTree.goToSiblingUp()
    assert projView.getSelectedHandle() == C.hTitlePage
    projTree.goToSiblingUp()
    assert projView.getSelectedHandle() == C.hTitlePage

    # Parent
    projTree.goToParent()
    assert projView.getSelectedHandle() == C.hNovelRoot
    projTree.goToParent()
    assert projView.getSelectedHandle() == C.hNovelRoot

    # Select Down
    # =========

    # Child
    projView.setSelectedHandle(C.hNovelRoot, doScroll=True)
    assert projView.getSelectedHandle() == C.hNovelRoot
    projTree.goToFirstChild()
    assert projView.getSelectedHandle() == C.hTitlePage

    # Sibling
    projTree.goToSiblingDown()
    assert projView.getSelectedHandle() == C.hChapterDir
    projTree.goToSiblingDown()
    assert projView.getSelectedHandle() == hFolder
    projTree.goToSiblingDown()
    assert projView.getSelectedHandle() == hFolder


@pytest.mark.gui
def testGuiProjTree_MouseClicks(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
    """Test mouse clicks in the project tree."""
    monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))

    projView = nwGUI.projView
    projTree = projView.projTree
    project = SHARED.project
    tree = project.tree
    model = tree.model

    # The default model is empty
    assert projView.getSelectedHandle() is None
    assert projTree._getModel() is None
    assert projTree._selectedRows() == []
    assert projTree._getNode(QModelIndex()) is None

    # Create a project
    buildTestProject(nwGUI, projPath)
    trash = tree.trash
    assert trash is not None
    assert len(tree) == 9

    # Single click emits a signal
    with qtbot.waitSignal(projView.selectedItemChanged) as signal:
        projTree._onSingleClick(model.indexFromHandle(C.hNovelRoot))
    assert signal.args[0] == C.hNovelRoot

    # Double click on folder expands/collapses it
    assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == []
    projTree._onDoubleClick(model.indexFromHandle(C.hNovelRoot))
    assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == [
        C.hNovelRoot
    ]
    projTree._onDoubleClick(model.indexFromHandle(C.hNovelRoot))
    assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == []

    # Double click on file opens it
    with qtbot.waitSignal(projView.openDocumentRequest) as signal:
        projTree._onDoubleClick(model.indexFromHandle(C.hChapterDoc))
    assert signal.args[0] == C.hChapterDoc
    assert signal.args[1] == nwDocMode.EDIT
    assert signal.args[2] == ""
    assert signal.args[3] is True

    # Mouse Button Clicks
    # ===================

    eType = QEvent.Type.MouseButtonPress
    modifier = QtModNone

    # Trigger the viewer
    pos = projTree.visualRect(model.indexFromHandle(C.hChapterDoc)).center()
    button = QtMouseMiddle
    event = QMouseEvent(eType, pos, button, button, modifier)
    projTree.mousePressEvent(event)
    assert nwGUI.docViewer.docHandle == C.hChapterDoc

    # Trigger the left click clear
    pos = QPoint(5000, 5000)
    button = QtMouseLeft
    event = QMouseEvent(eType, pos, button, button, modifier)

    projTree.setSelectedHandle(C.hChapterDoc)
    assert projView.getSelectedHandle() == C.hChapterDoc

    projTree.mousePressEvent(event)
    assert projView.getSelectedHandle() is None


@pytest.mark.gui
def testGuiProjTree_DeleteRequest(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd):
    """Test delete requests in the project tree."""
    monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))

    projView = nwGUI.projView
    projTree = projView.projTree
    project = SHARED.project
    tree = project.tree

    # Create a project
    buildTestProject(nwGUI, projPath)
    trash = tree.trash
    assert trash is not None
    assert len(tree) == 9
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Plot", "Characters", "Locations", "Trash",
    ]

    # Add some scenes
    hFolder = "0000000000011"
    hScenes = ["0000000000012", "0000000000013", "0000000000014", "0000000000015"]
    projView.setSelectedHandle(C.hNovelRoot, doScroll=True)
    projTree.newTreeItem(nwItemType.FOLDER)
    assert hFolder in tree
    item = tree[hFolder]
    assert item is not None
    item.setName("Chapter Folder")

    projView.setSelectedHandle(hFolder, doScroll=True)
    for n, hScene in enumerate(hScenes):
        projTree.newTreeItem(nwItemType.FILE, hLevel=3)
        assert hScene in tree
        item = tree[hScene]
        assert item is not None
        item.setName(f"Scene {n+1}")

    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
        "Plot", "Characters", "Locations", "Trash",
    ]

    # Calling it with no selection, does nothing
    projTree._clearSelection()
    projTree.processDeleteRequest()
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
        "Plot", "Characters", "Locations", "Trash",
    ]

    # Deleting a used root raises an error
    projTree.processDeleteRequest([C.hNovelRoot])
    assert SHARED.lastAlert == "Root folders can only be deleted when they are empty."
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
        "Plot", "Characters", "Locations", "Trash",
    ]

    # Deleting a unused root is fine
    projTree.processDeleteRequest([C.hWorldRoot])
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
        "Plot", "Characters", "Trash",
    ]

    # User can cancel move to trash
    with monkeypatch.context() as mp:
        mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No)
        projTree.processDeleteRequest(hScenes, askFirst=True)
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
        "Plot", "Characters", "Trash",
    ]

    # Items not already in trash can be moved there
    projTree.processDeleteRequest(hScenes, askFirst=True)
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Chapter Folder", "Plot", "Characters", "Trash",
        "Scene 1", "Scene 2", "Scene 3", "Scene 4",
    ]

    # User can block permanent deletion
    with monkeypatch.context() as mp:
        mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No)
        projTree.processDeleteRequest(hScenes[0:2], askFirst=True)
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Chapter Folder", "Plot", "Characters", "Trash",
        "Scene 1", "Scene 2", "Scene 3", "Scene 4",
    ]

    # Items in trash can be permanently deleted
    projTree.processDeleteRequest(hScenes[0:2], askFirst=True)
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Chapter Folder", "Plot", "Characters", "Trash",
        "Scene 3", "Scene 4",
    ]

    # Moving a parent item to trash, includes children
    projTree.processDeleteRequest([C.hChapterDir], askFirst=True)
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash",
        "Scene 3", "Scene 4", "New Folder", "New Chapter", "New Scene",
    ]

    # Permanently delete in trash is recursive
    projTree.processDeleteRequest([C.hChapterDir], askFirst=True)
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash",
        "Scene 3", "Scene 4",
    ]

    # Trash can be completely emptied, but user can block it
    with monkeypatch.context() as mp:
        mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No)
        projTree.emptyTrash()
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash",
        "Scene 3", "Scene 4",
    ]

    # Opening documents before delete, will close them
    nwGUI.openDocument(hScenes[2])
    nwGUI.viewDocument(hScenes[3])

    assert nwGUI.docEditor.docHandle == hScenes[2]
    assert nwGUI.docViewer.docHandle == hScenes[3]

    # Trash can be completely emptied
    projTree.emptyTrash()
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash",
    ]

    assert nwGUI.docEditor.docHandle is None
    assert nwGUI.docViewer.docHandle is None

    # Emptying empty trash pops an alert
    projTree.emptyTrash()
    assert SHARED.lastAlert == "The Trash folder is already empty."

    # Trash can be deleted if empty
    projTree.processDeleteRequest([trash.item.itemHandle])
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "Chapter Folder", "Plot", "Characters"
    ]

    # Emptying trash when it doesn't exist, recreates it
    projTree.emptyTrash()
    assert SHARED.lastAlert == "The Trash folder is already empty."
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash",
    ]


@pytest.mark.gui
def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd, ipsumText):
    """Test the merge document function."""
    mergeData = {}

    monkeypatch.setattr(GuiDocMerge, "__init__", lambda *a: None)
    monkeypatch.setattr(GuiDocMerge, "exec", lambda *a: None)
    monkeypatch.setattr(GuiDocMerge, "softDelete", lambda *a: None)
    monkeypatch.setattr(GuiDocMerge, "result", lambda *a: QtAccepted)
    monkeypatch.setattr(GuiDocMerge, "data", lambda *a: mergeData)

    projView = nwGUI.projView
    projTree = projView.projTree
    project = SHARED.project
    tree = project.tree

    # Create a project
    buildTestProject(nwGUI, projPath)
    trash = tree.trash
    assert trash is not None
    assert len(tree) == 9
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Plot", "Characters", "Locations", "Trash",
    ]

    # Create File to Merge
    hChapter1 = project.newFile("Chapter 1", C.hNovelRoot)
    assert hChapter1 == "0000000000011"

    hSceneOne11 = project.newFile("Scene 1.1", hChapter1)
    hSceneOne12 = project.newFile("Scene 1.2", hChapter1)
    hSceneOne13 = project.newFile("Scene 1.3", hChapter1)
    assert hSceneOne11 == "0000000000012"
    assert hSceneOne12 == "0000000000013"
    assert hSceneOne13 == "0000000000014"

    mergedDoc1 = "0000000000015"

    docText1 = "\n\n".join(ipsumText[0:2]) + "\n\n"
    docText2 = "\n\n".join(ipsumText[1:3]) + "\n\n"
    docText3 = "\n\n".join(ipsumText[2:4]) + "\n\n"
    docText4 = "\n\n".join(ipsumText[3:5]) + "\n\n"

    lenText1 = len(docText1)
    lenText2 = len(docText2)
    lenText3 = len(docText3)
    lenText4 = len(docText4)
    lenAll = lenText1 + lenText2 + lenText3 + lenText4

    project.writeNewFile(hChapter1, 2, True, docText1)
    project.writeNewFile(hSceneOne11, 3, True, docText2)
    project.writeNewFile(hSceneOne12, 3, True, docText3)
    project.writeNewFile(hSceneOne13, 3, True, docText4)

    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Chapter 1", "Scene 1.1", "Scene 1.2", "Scene 1.3",
        "Plot", "Characters", "Locations", "Trash",
    ]

    # Invalid file handle
    assert projTree.mergeDocuments(C.hInvalid, False) is False

    # Cannot merge root item
    assert projTree.mergeDocuments(C.hNovelRoot, False) is False

    # Merge to new file, but there is now merge data
    mergeData.clear()
    assert projTree.mergeDocuments(hChapter1, True) is False

    # Merge to New Doc
    # ================

    # Set merge job for new documents
    mergeData["finalItems"] = [hChapter1, hSceneOne11, hSceneOne12, hSceneOne13]
    mergeData["moveToTrash"] = False

    # User cancels merge
    with monkeypatch.context() as mp:
        mp.setattr(GuiDocMerge, "result", lambda *a: QtRejected)
        assert projTree.mergeDocuments(hChapter1, True) is False

    # The merge goes through
    assert projTree.mergeDocuments(hChapter1, True) is True
    mergedText = project.storage.getDocument(mergedDoc1).readDocument()
    assert mergedText is not None
    assert len(mergedText) > lenAll
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Chapter 1", "Scene 1.1", "Scene 1.2", "Scene 1.3", "[Merged] Chapter 1",
        "Plot", "Characters", "Locations", "Trash",
    ]

    # Merge to Existing Doc
    # =====================

    # Set merge job for parent document
    mergeData["finalItems"] = [hSceneOne11, hSceneOne12, hSceneOne13]
    mergeData["moveToTrash"] = False

    # Merging to a folder is not allowed
    assert projTree.mergeDocuments(C.hChapterDir, False) is False

    # Block writing and check error handling
    with monkeypatch.context() as mp:
        mp.setattr("builtins.open", causeOSError)
        assert projTree.mergeDocuments(hChapter1, False) is False

    # Successful merge, and move to trash
    mergeData["moveToTrash"] = True
    mergedText = project.storage.getDocument(hChapter1).readDocument()
    assert mergedText is not None
    assert len(mergedText) < lenAll
    assert projTree.mergeDocuments(hChapter1, False) is True
    mergedText = project.storage.getDocument(hChapter1).readDocument()
    assert mergedText is not None
    assert len(mergedText) > lenAll

    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Chapter 1", "[Merged] Chapter 1",
        "Plot", "Characters", "Locations", "Trash",
        "Scene 1.1", "Scene 1.2", "Scene 1.3",
    ]

    # qtbot.stop()


@pytest.mark.gui
def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, projPath, mockRnd, ipsumText):
    """Test the split document function."""
    splitData = {}
    splitText = []

    monkeypatch.setattr(GuiDocSplit, "__init__", lambda *a: None)
    monkeypatch.setattr(GuiDocSplit, "exec", lambda *a: None)
    monkeypatch.setattr(GuiDocSplit, "softDelete", lambda *a: None)
    monkeypatch.setattr(GuiDocSplit, "result", lambda *a: QtAccepted)
    monkeypatch.setattr(GuiDocSplit, "data", lambda *a: (splitData, splitText))

    projView = nwGUI.projView
    projTree = projView.projTree
    project = SHARED.project
    tree = project.tree

    # Create a project
    buildTestProject(nwGUI, projPath)
    trash = tree.trash
    assert trash is not None
    assert len(tree) == 9
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Plot", "Characters", "Locations", "Trash",
    ]

    docText = (
        "Text\n\n"
        "##! Prologue\n\nText\n\n"
        "## Chapter One\n\nText\n\n"
        "### Scene One\n\nText\n\n"
        "### Scene Two\n\nText\n\n"
        "## Chapter Two\n\nText\n\n"
        "### Scene Three\n\nText\n\n"
        "### Scene Four\n\nText\n\n"
        "#! New Title\n\nText\n\n"
        "## New Chapter\n\nText\n\n"
        "### New Scene\n\nText\n\n"
        "#### New Section\n\nText\n\n"
    )

    hSplitDoc = project.newFile("Split Doc", C.hNovelRoot)
    assert hSplitDoc is not None
    project.writeNewFile(hSplitDoc, 1, True, docText)

    docText = f"# Split Doc\n\n{docText}"
    splitData["headerList"] = [
        (0, 1, "Split Doc"),
        (4, 2, "Prologue"),
        (8, 2, "Chapter One"),
        (12, 3, "Scene One"),
        (16, 3, "Scene Two"),
        (20, 2, "Chapter Two"),
        (24, 3, "Scene Three"),
        (28, 3, "Scene Four"),
        (32, 1, "New Title"),
        (36, 2, "New Chapter"),
        (40, 3, "New Scene"),
        (44, 4, "New Section"),
    ]

    fstSet = [
        "0000000000012", "0000000000013", "0000000000014", "0000000000015",
        "0000000000016", "0000000000017", "0000000000018", "0000000000019",
        "000000000001a", "000000000001b", "000000000001c", "000000000001d",
    ]
    sndSet = [
        "000000000001e", "000000000001f", "0000000000020", "0000000000021",
        "0000000000022", "0000000000023", "0000000000024", "0000000000025",
        "0000000000026", "0000000000027", "0000000000028", "0000000000029",
    ]
    trdSet = [
        "000000000002b", "000000000002c", "000000000002d", "000000000002e",
        "000000000002f", "0000000000030", "0000000000031", "0000000000032",
        "0000000000033", "0000000000034", "0000000000035", "0000000000036",
    ]

    # Try to split an invalid document and a non-document
    assert projTree.splitDocument(C.hInvalid) is False
    assert projTree.splitDocument(C.hNovelRoot) is False

    # Split into same root folder
    splitData["intoFolder"] = False

    # Writing fails
    with monkeypatch.context() as mp:
        mp.setattr("builtins.open", causeOSError)
        assert projTree.splitDocument(hSplitDoc) is True
        for tHandle in fstSet:
            assert tHandle in project.tree
            assert not (projPath / "content" / f"{tHandle}.nwd").is_file()

    # Writing succeeds
    assert projTree.splitDocument(hSplitDoc) is True
    for tHandle in sndSet:
        assert tHandle in project.tree
        assert (projPath / "content" / f"{tHandle}.nwd").is_file()

    # Add to a folder and move source to trash
    splitData["intoFolder"] = True
    splitData["moveToTrash"] = True
    assert projTree.splitDocument(hSplitDoc) is True
    assert "0000000000029" in project.tree  # The folder
    for tHandle in trdSet:
        assert tHandle in project.tree
        assert (projPath / "content" / f"{tHandle}.nwd").is_file()

    assert trash.allChildren() == [tree.nodes[hSplitDoc]]

    # Cancelled by user
    with monkeypatch.context() as mp:
        mp.setattr(GuiDocSplit, "result", lambda *a: QtRejected)
        assert projTree.splitDocument(hSplitDoc) is False

    # qtbot.stop()


@pytest.mark.gui
def testGuiProjTree_Duplicate(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
    """Test the duplicate items function."""
    # Create a project
    projView = nwGUI.projView
    projTree = projView.projTree
    project = SHARED.project
    tree = project.tree

    # Create a project
    buildTestProject(nwGUI, projPath)
    trash = tree.trash
    assert trash is not None
    assert len(tree) == 9
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Plot", "Characters", "Locations", "Trash",
    ]

    projTree.expandAll()

    # Nothing to do
    projTree.duplicateFromHandle(C.hInvalid)
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Plot", "Characters", "Locations", "Trash",
    ]

    # Duplicate title page, but select no
    with monkeypatch.context() as mp:
        mp.setattr(QMessageBox, "result", lambda *a: QtRejected)
        projTree.duplicateFromHandle(C.hTitlePage)
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Plot", "Characters", "Locations", "Trash",
    ]

    # Duplicate title page
    projTree.duplicateFromHandle(C.hTitlePage)
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "Title Page", "New Folder", "New Chapter",
        "New Scene", "Plot", "Characters", "Locations", "Trash",
    ]

    # Duplicate folder
    projTree.duplicateFromHandle(C.hChapterDir)
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "Title Page", "New Folder", "New Chapter",
        "New Scene", "New Folder", "New Chapter", "New Scene",
        "Plot", "Characters", "Locations", "Trash",
    ]

    # Duplicate novel root
    projTree.duplicateFromHandle(C.hNovelRoot)
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "Title Page", "New Folder", "New Chapter",
        "New Scene", "New Folder", "New Chapter", "New Scene",
        "Novel", "Title Page", "Title Page", "New Folder", "New Chapter",
        "New Scene", "New Folder", "New Chapter", "New Scene",
        "Plot", "Characters", "Locations", "Trash",
    ]

    # qtbot.stop()


@pytest.mark.gui
def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
    """Test various parts of the project tree class not covered by
    other tests.
    """
    buildTestProject(nwGUI, projPath)
    projView = nwGUI.projView
    projTree = projView.projTree

    # Method: initSettings
    # ====================

    # Test that the scrollbar setting works
    CONFIG.hideVScroll = True
    CONFIG.hideHScroll = True
    projView.initSettings()
    assert projTree.verticalScrollBarPolicy() == QtScrollAlwaysOff
    assert projTree.horizontalScrollBarPolicy() == QtScrollAlwaysOff

    CONFIG.hideVScroll = False
    CONFIG.hideHScroll = False
    projView.initSettings()
    assert projTree.verticalScrollBarPolicy() == QtScrollAsNeeded
    assert projTree.horizontalScrollBarPolicy() == QtScrollAsNeeded

    # qtbot.stop()


@pytest.mark.gui
def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
    """Test the building of the project tree context menu. All this does
    is test that the menu builds. It doesn't open the actual menu.
    """
    monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
    QtSelect = QItemSelectionModel.SelectionFlag.Select

    # Create a project
    projView = nwGUI.projView
    projTree = projView.projTree
    project = SHARED.project
    tree = project.tree
    model = tree.model

    # Create a project
    buildTestProject(nwGUI, projPath)
    trash = tree.trash
    assert trash is not None
    assert len(tree) == 9
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Plot", "Characters", "Locations", "Trash",
    ]

    # Handles for new objects
    hCharNote     = "0000000000011"
    hNovelNote    = "0000000000012"
    hTrashDoc    = "0000000000013"
    hSubNote      = "0000000000014"
    hNewFolderOne = "0000000000015"
    hNewFolderTwo = "0000000000017"
    hTrashRoot    = trash.item.itemHandle

    projTree.expandAll()
    projTree.setSelectedHandle(C.hCharRoot)
    projTree.newTreeItem(nwItemType.FILE, isNote=True)
    projTree.setSelectedHandle(C.hNovelRoot)
    projTree.newTreeItem(nwItemType.FILE, isNote=True)
    projTree.setSelectedHandle(C.hNovelRoot)
    projTree.newTreeItem(nwItemType.FILE)
    projTree.setSelectedHandle(hTrashDoc)
    projTree.processDeleteRequest()

    project.newFile("SubNote", hNovelNote)
    item = tree[hSubNote]
    assert item is not None
    assert item.itemName == "SubNote"
    assert item.itemParent == hNovelNote

    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "New Note", "SubNote", "Plot", "Characters", "New Note", "Locations",
        "Trash", "New Document",
    ]

    # Pop the menu in various positions and check for success
    with monkeypatch.context() as mp:
        mockMenu = MagicMock()
        mp.setattr(QMenu, "exec", mockMenu)
        projTree._clearSelection()

        # No item under menu
        projTree.openContextMenu(projTree.viewport().rect().bottomRight())
        assert mockMenu.call_count == 0

        # Open Trash Menu
        projView.setSelectedHandle(hTrashRoot)
        projTree.openContextMenu()
        assert mockMenu.call_count == 1

        # Open Single Select Menu
        projView.setSelectedHandle(C.hNovelRoot)
        projTree.openContextMenu()
        assert mockMenu.call_count == 2

        # Open Multi-Select Menu
        projTree.selectionModel().select(model.indexFromHandle(hNovelNote), QtSelect)
        projTree.selectionModel().select(model.indexFromHandle(hSubNote), QtSelect)
        projTree.openContextMenu()
        assert mockMenu.call_count == 3
        projTree.clearSelection()

    # Menu Builders
    # =============

    # Context Menu on Root Item
    node = tree.nodes[C.hNovelRoot]
    indices = [model.indexFromHandle(C.hNovelRoot)]
    ctxMenu = _TreeContextMenu(projTree, model, node, indices)
    ctxMenu.buildSingleSelectMenu()
    actions = [x.text() for x in ctxMenu.actions() if x.text()]
    assert actions == [
        "Create New ...", "Rename", "Set Status to ...", "Expand All",
        "Collapse All", "Duplicate", "Delete Permanently",
    ]

    # Context Menu on Folder Item
    node = tree.nodes[C.hChapterDir]
    indices = [model.indexFromHandle(C.hChapterDir)]
    ctxMenu = _TreeContextMenu(projTree, model, node, indices)
    ctxMenu.buildSingleSelectMenu()
    actions = [x.text() for x in ctxMenu.actions() if x.text()]
    assert actions == [
        "Create New ...", "Rename", "Set Status to ...", "Transform ...", "Expand All",
        "Collapse All", "Duplicate", "Move to Trash",
    ]

    def getTransformSubMenu(menu: QMenu) -> list[str]:
        for action in menu.actions():
            if action.text() == "Transform ...":
                return [x.text() for x in action.menu().actions() if x.text()]
        return []

    # Context Menu on Document File Item
    node = tree.nodes[C.hChapterDoc]
    indices = [model.indexFromHandle(C.hChapterDoc)]
    ctxMenu = _TreeContextMenu(projTree, model, node, indices)
    ctxMenu.buildSingleSelectMenu()
    actions = [x.text() for x in ctxMenu.actions() if x.text()]
    assert actions == [
        "Open Document", "View Document", "Create New ...", "Rename", "Rename to Heading",
        "Toggle Active", "Set Status to ...", "Transform ...", "Duplicate", "Move to Trash",
    ]
    assert getTransformSubMenu(ctxMenu) == [
        "Convert to Project Note", "Split Document by Headings"
    ]

    # Context Menu on Note File Item in Character Folder
    node = tree.nodes[hCharNote]
    indices = [model.indexFromHandle(hCharNote)]
    ctxMenu = _TreeContextMenu(projTree, model, node, indices)
    ctxMenu.buildSingleSelectMenu()
    actions = [x.text() for x in ctxMenu.actions() if x.text()]
    assert actions == [
        "Open Document", "View Document", "Create New ...", "Rename", "Rename to Heading",
        "Toggle Active", "Set Importance to ...", "Transform ...", "Duplicate", "Move to Trash",
    ]
    assert getTransformSubMenu(ctxMenu) == [
        "Split Document by Headings",
    ]

    # Context Menu on Note File Item in Novel Tree
    node = tree.nodes[hNovelNote]
    indices = [model.indexFromHandle(hNovelNote)]
    ctxMenu = _TreeContextMenu(projTree, model, node, indices)
    ctxMenu.buildSingleSelectMenu()
    actions = [x.text() for x in ctxMenu.actions() if x.text()]
    assert actions == [
        "Open Document", "View Document", "Create New ...", "Rename", "Rename to Heading",
        "Toggle Active", "Set Status to ...", "Transform ...", "Expand All", "Collapse All",
        "Duplicate", "Move to Trash",
    ]
    assert getTransformSubMenu(ctxMenu) == [
        "Convert to Novel Document", "Merge Child Items into Self",
        "Merge Child Items into New", "Split Document by Headings",
    ]

    # Context Menu on Multiple Items, Clicked on Document
    node = tree.nodes[hNovelNote]
    indices = [
        model.indexFromHandle(hCharNote),
        model.indexFromHandle(hNovelNote),
        model.indexFromHandle(hSubNote),
    ]
    ctxMenu = _TreeContextMenu(projTree, model, node, indices)
    ctxMenu.buildMultiSelectMenu()
    actions = [x.text() for x in ctxMenu.actions() if x.text()]
    assert actions == [
        "Set Active to ...", "Set Status to ...", "Move to Trash",
    ]

    # Context Menu on Multiple Items, Clicked on Note
    node = tree.nodes[hCharNote]
    indices = [
        model.indexFromHandle(hCharNote),
        model.indexFromHandle(hNovelNote),
        model.indexFromHandle(hSubNote),
    ]
    ctxMenu = _TreeContextMenu(projTree, model, node, indices)
    ctxMenu.buildMultiSelectMenu()
    actions = [x.text() for x in ctxMenu.actions() if x.text()]
    assert actions == [
        "Set Active to ...", "Set Importance to ...", "Move to Trash",
    ]

    # Direct Edit Functions, Single
    # =============================

    node = tree.nodes[hNovelNote]
    indices = [model.indexFromHandle(hNovelNote)]

    # Toggle active flag
    ctxMenu = _TreeContextMenu(projTree, model, node, indices)
    ctxMenu.buildSingleSelectMenu()
    assert node.item.isActive is True
    ctxMenu._toggleItemActive()
    assert node.item.isActive is False

    # Change item status
    assert node.item.itemStatus == "s000000"
    ctxMenu._changeItemStatus("s000001")
    assert node.item.itemStatus == "s000001"

    # Change item importance
    assert node.item.itemImport == "i000004"
    ctxMenu._changeItemImport("i000005")
    assert node.item.itemImport == "i000005"

    # Change item layout
    assert node.item.itemLayout == nwItemLayout.NOTE
    ctxMenu._changeItemLayout(nwItemLayout.DOCUMENT)
    assert node.item.itemLayout == nwItemLayout.DOCUMENT
    ctxMenu._changeItemLayout(nwItemLayout.NOTE)
    assert node.item.itemLayout == nwItemLayout.NOTE

    # Convert Folders to Documents
    # ============================

    projView.setSelectedHandle(hNovelNote)
    projTree.newTreeItem(nwItemType.FOLDER)
    projView.setSelectedHandle(hNewFolderOne)
    projTree.newTreeItem(nwItemType.FILE)

    projView.setSelectedHandle(hNovelNote)
    projTree.newTreeItem(nwItemType.FOLDER)
    projView.setSelectedHandle(hNewFolderTwo)
    projTree.newTreeItem(nwItemType.FILE, isNote=True)

    assert hNewFolderOne in tree
    assert hNewFolderTwo in tree

    nodeOne = tree.nodes[hNewFolderOne]
    nodeTwo = tree.nodes[hNewFolderTwo]

    # Select Folder One
    indices = [model.indexFromHandle(hNewFolderOne)]
    ctxMenu = _TreeContextMenu(projTree, model, nodeOne, indices)
    ctxMenu.buildSingleSelectMenu()

    # Click no on the dialog
    with monkeypatch.context() as mp:
        mp.setattr(QMessageBox, "result", lambda *a: QtRejected)
        ctxMenu._convertFolderToFile(nwItemLayout.DOCUMENT)
        assert nodeOne.item.isFolderType()

    # Convert the first folder to a document
    assert nodeOne.item.isFolderType()
    ctxMenu._convertFolderToFile(nwItemLayout.DOCUMENT)
    assert nodeOne.item.isFileType()
    assert nodeOne.item.isDocumentLayout()

    # Select Folder Two
    indices = [model.indexFromHandle(hNewFolderTwo)]
    ctxMenu = _TreeContextMenu(projTree, model, nodeTwo, indices)
    ctxMenu.buildSingleSelectMenu()

    # Convert the second folder to a note
    ctxMenu._convertFolderToFile(nwItemLayout.NOTE)
    assert nodeTwo.item.isFileType()
    assert nodeTwo.item.isNoteLayout()

    # Direct Edit Functions, Multi
    # ============================

    node = tree.nodes[hCharNote]
    indices = [
        model.indexFromHandle(hCharNote),
        model.indexFromHandle(hNovelNote),
        model.indexFromHandle(hSubNote),
    ]
    ctxMenu = _TreeContextMenu(projTree, model, node, indices)
    ctxMenu.buildMultiSelectMenu()

    # projTree.clearSelection()
    # projTree._getTreeItem(hCharNote).setSelected(True)
    # projTree._getTreeItem(hNovelNote).setSelected(True)
    # projTree._getTreeItem(hSubNote).setSelected(True)

    nodeCNote = tree.nodes[hCharNote]
    nodeNNote = tree.nodes[hNovelNote]
    nodeSNote = tree.nodes[hSubNote]

    # Item Active
    assert nodeCNote.item.isActive is True
    assert nodeNNote.item.isActive is False
    assert nodeSNote.item.isActive is True
    ctxMenu._iterItemActive(False)
    assert nodeCNote.item.isActive is False
    assert nodeNNote.item.isActive is False
    assert nodeSNote.item.isActive is False
    ctxMenu._iterItemActive(True)
    assert nodeCNote.item.isActive is True
    assert nodeNNote.item.isActive is True
    assert nodeSNote.item.isActive is True

    # Item Status
    assert nodeCNote.item.itemStatus == "s000000"
    assert nodeNNote.item.itemStatus == "s000001"
    assert nodeSNote.item.itemStatus == "s000000"
    ctxMenu._iterSetItemStatus("s000003")
    assert nodeCNote.item.itemStatus == "s000000"
    assert nodeNNote.item.itemStatus == "s000003"
    assert nodeSNote.item.itemStatus == "s000003"

    # Item Importance
    assert nodeCNote.item.itemImport == "i000004"
    assert nodeNNote.item.itemImport == "i000005"
    assert nodeSNote.item.itemImport == "i000004"
    ctxMenu._iterSetItemImport("i000007")
    assert nodeCNote.item.itemImport == "i000007"
    assert nodeNNote.item.itemImport == "i000005"
    assert nodeSNote.item.itemImport == "i000004"

    # qtbot.stop()


@pytest.mark.gui
def testGuiProjTree_Templates(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
    """Test the templates feature of the project tree."""
    monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))

    # Create a project
    projView = nwGUI.projView
    projTree = projView.projTree
    projBar  = projView.projBar
    project = SHARED.project
    tree = project.tree

    # Create a project
    buildTestProject(nwGUI, projPath)
    trash = tree.trash
    assert trash is not None
    assert len(tree) == 9
    assert [n.item.itemName for n in tree.model.root.allChildren()] == [
        "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
        "Plot", "Characters", "Locations", "Trash",
    ]

    # Handles for new objects
    hTemplatesRoot = "0000000000011"
    hSceneTemplate = "0000000000012"
    hNoteTemplate  = "0000000000013"
    hNewScene      = "0000000000014"
    hNewCharacter  = "0000000000015"

    # Add template folder
    projTree.newTreeItem(nwItemType.ROOT, nwItemClass.TEMPLATE)
    nwTemplateRoot = tree[hTemplatesRoot]
    assert nwTemplateRoot is not None
    assert nwTemplateRoot.itemName == "Templates"

    # Add a scene template
    projTree.setSelectedHandle(hTemplatesRoot)
    projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False)
    nwSceneTemplate = tree[hSceneTemplate]
    assert nwSceneTemplate is not None
    assert nwSceneTemplate.itemName == "New Scene"
    assert projBar.mTemplates.actions()[0].text() == "New Scene"

    # Rename the scene template
    projView.renameTreeItem(hSceneTemplate, name="Scene")
    assert projBar.mTemplates.actions()[0].text() == "Scene"

    # Add a note template
    projTree.setSelectedHandle(hTemplatesRoot)
    projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True)
    nwNoteTemplate = project.tree[hNoteTemplate]
    assert nwNoteTemplate is not None
    assert nwNoteTemplate.itemName == "New Note"
    assert projBar.mTemplates.actions()[1].text() == "New Note"

    # Rename the note template
    projView.renameTreeItem(hNoteTemplate, name="Note")
    assert projBar.mTemplates.actions()[1].text() == "Note"

    # Add new content to template files
    (projPath / "content" / f"{hSceneTemplate}.nwd").write_text("### Scene\n\n@pov: Jane\n\n")
    (projPath / "content" / f"{hNoteTemplate}.nwd").write_text("# Jane\n\n@tag: Jane\n\n")

    # Add a new scene using the template
    projTree.setSelectedHandle(C.hSceneDoc, doScroll=True)
    projBar.mTemplates.actions()[0].trigger()
    nwNewScene = project.tree[hNewScene]
    assert nwNewScene is not None
    assert nwNewScene.itemName == "Scene"
    assert project.storage.getDocument(hNewScene).readDocument() == "### Scene\n\n@pov: Jane\n\n"

    # Add a new note using the template
    projTree.setSelectedHandle(C.hCharRoot, doScroll=True)
    projBar.mTemplates.actions()[1].trigger()
    nwNewCharacter = project.tree[hNewCharacter]
    assert nwNewCharacter is not None
    assert nwNewCharacter.itemName == "Note"
    assert project.storage.getDocument(hNewCharacter).readDocument() == "# Jane\n\n@tag: Jane\n\n"

    # Clearing the menu and rebuilding it should work
    projBar.mTemplates.clearMenu()
    assert len(projBar.mTemplates.actions()) == 0
    projBar.buildTemplatesMenu()
    assert len(projBar.mTemplates.actions()) == 2

    # Remove the note template
    assert projBar.mTemplates.menuAction().isVisible() is True
    assert len(projBar.mTemplates.actions()) == 2
    assert trash.childCount() == 0
    projTree.processDeleteRequest([hNoteTemplate])
    assert trash.childCount() == 1
    assert len(projBar.mTemplates.actions()) == 1
    projTree.processDeleteRequest([hSceneTemplate])
    assert len(projBar.mTemplates.actions()) == 0
    assert projBar.mTemplates.menuAction().isVisible() is False

    # qtbot.stop()
