"""
novelWriter – Main GUI Main Menu 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 PyQt6.QtGui import QAction, QDesktopServices, QTextBlock, QTextCursor
from PyQt6.QtWidgets import QFileDialog, QMessageBox

from novelwriter import CONFIG, SHARED
from novelwriter.constants import nwKeyWords, nwShortcode, nwStats, nwUnicode
from novelwriter.enum import nwDocAction, nwDocInsert
from novelwriter.gui.doceditor import GuiDocEditor
from novelwriter.types import QtKeepAnchor, QtMoveRight

from tests.tools import C, buildTestProject, writeFile


@pytest.mark.gui
def testGuiMainMenu_Slots(qtbot, monkeypatch, nwGUI, projPath):
    """Test the main menu slots."""
    buildTestProject(nwGUI, projPath)

    # Open Manual
    with monkeypatch.context() as mp:
        openUrl = MagicMock()
        mp.setattr(QDesktopServices, "openUrl", openUrl)
        CONFIG._manuals = {"manual": projPath / "manual.pdf"}
        CONFIG._manuals["manual"].touch()
        nwGUI.mainMenu._openUserManualFile()
        assert openUrl.called is True
        assert "manual.pdf" in openUrl.call_args[0][0].url()

    # Spell Checking
    assert SHARED.project.data.spellLang is None
    nwGUI.mainMenu._changeSpelling("en")
    assert SHARED.project.data.spellLang == "en"


@pytest.mark.gui
def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum):
    """Test the main menu Edit and Format entries."""
    monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True)
    mainMenu = nwGUI.mainMenu
    docEditor = nwGUI.docEditor

    # Test Document Action with No Project
    assert docEditor.docAction(nwDocAction.COPY) is False

    assert nwGUI.openProject(prjLipsum) is True
    x = 72

    # Split By Chapter
    assert nwGUI.openDocument("4c4f28287af27") is True
    docEditor.setCursorPosition(x+3)
    cleanText = docEditor.getText()[x:x+47]

    # Bold
    mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger)
    fmtStr = "**Pellentesque** nec erat ut nulla posuere commodo."
    assert docEditor.getText()[x:x+51] == fmtStr
    mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText()[x:x+47] == cleanText

    # Italic
    mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger)
    fmtStr = "_Pellentesque_ nec erat ut nulla posuere commodo."
    assert docEditor.getText()[x:x+49] == fmtStr
    mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText()[x:x+47] == cleanText

    # Strikethrough
    mainMenu.aFmtStrike.activate(QAction.ActionEvent.Trigger)
    fmtStr = "~~Pellentesque~~ nec erat ut nulla posuere commodo."
    assert docEditor.getText()[x:x+51] == fmtStr
    mainMenu.aFmtStrike.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText()[x:x+47] == cleanText

    # Should get us back to plain
    mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger)
    mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger)
    mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger)
    mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText()[x:x+47] == cleanText

    # Double Quotes
    mainMenu.aFmtDQuote.activate(QAction.ActionEvent.Trigger)
    fmtStr = "“Pellentesque” nec erat ut nulla posuere commodo."
    assert docEditor.getText()[x:x+49] == fmtStr
    mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText()[x:x+47] == cleanText

    # Single Quotes
    mainMenu.aFmtSQuote.activate(QAction.ActionEvent.Trigger)
    fmtStr = "‘Pellentesque’ nec erat ut nulla posuere commodo."
    assert docEditor.getText()[x:x+49] == fmtStr
    mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText()[x:x+47] == cleanText

    # Block Formats
    # =============
    # cSpell:ignore Pellentesque erat nulla posuere commodo
    docEditor.setCursorPosition(x+3)

    # Header 1
    mainMenu.aFmtHead1.activate(QAction.ActionEvent.Trigger)
    fmtStr = "# Pellentesque nec erat ut nulla posuere commodo."
    assert docEditor.getText()[x:x+49] == fmtStr

    # Header 2
    mainMenu.aFmtHead2.activate(QAction.ActionEvent.Trigger)
    fmtStr = "## Pellentesque nec erat ut nulla posuere commodo."
    assert docEditor.getText()[x:x+50] == fmtStr

    # Header 3
    mainMenu.aFmtHead3.activate(QAction.ActionEvent.Trigger)
    fmtStr = "### Pellentesque nec erat ut nulla posuere commodo."
    assert docEditor.getText()[x:x+51] == fmtStr

    # Header 4
    mainMenu.aFmtHead4.activate(QAction.ActionEvent.Trigger)
    fmtStr = "#### Pellentesque nec erat ut nulla posuere commodo."
    assert docEditor.getText()[x:x+52] == fmtStr

    # Title Format
    mainMenu.aFmtTitle.activate(QAction.ActionEvent.Trigger)
    fmtStr = "#! Pellentesque nec erat ut nulla posuere commodo."
    assert docEditor.getText()[x:x+50] == fmtStr

    # Unnumbered Chapter
    mainMenu.aFmtUnNum.activate(QAction.ActionEvent.Trigger)
    fmtStr = "##! Pellentesque nec erat ut nulla posuere commodo."
    assert docEditor.getText()[x:x+51] == fmtStr

    # Hard Scene
    mainMenu.aFmtHardSc.activate(QAction.ActionEvent.Trigger)
    fmtStr = "###! Pellentesque nec erat ut nulla posuere commodo."
    assert docEditor.getText()[x:x+52] == fmtStr

    # Clear Format
    mainMenu.aFmtNoFormat.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText()[x:x+47] == cleanText

    # Comment On
    mainMenu.aFmtComment.activate(QAction.ActionEvent.Trigger)
    fmtStr = "% Pellentesque nec erat ut nulla posuere commodo."
    assert docEditor.getText()[x:x+49] == fmtStr

    # Comment Off
    mainMenu.aFmtComment.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText()[x:x+47] == cleanText

    # Check comment with no space before text
    docEditor.setCursorPosition(x)
    docEditor.insertText("%")
    fmtStr = "%Pellentesque nec erat ut nulla posuere commodo."
    assert docEditor.getText()[x:x+48] == fmtStr

    mainMenu.aFmtNoFormat.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText()[x:x+47] == cleanText

    # Undo/Redo
    mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger)
    fmtStr = "%Pellentesque nec erat ut nulla posuere commodo."
    assert docEditor.getText()[x:x+48] == fmtStr
    mainMenu.aEditRedo.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText()[x:x+47] == cleanText

    # Cut, Copy and Paste
    docEditor.setCursorPosition(x)
    docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor)

    mainMenu.aEditCut.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText()[x:x+50] == (
        " nec erat ut nulla posuere commodo. Curabitur nisi"
    )

    mainMenu.aEditPaste.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText()[x:x+50] == (
        "Pellentesque nec erat ut nulla posuere commodo. Cu"
    )

    docEditor.setCursorPosition(x)
    docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor)

    mainMenu.aEditCopy.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText()[x:x+50] == (
        "Pellentesque nec erat ut nulla posuere commodo. Cu"
    )

    docEditor.setCursorPosition(x)
    mainMenu.aEditPaste.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText()[x:x+50] == (
        "PellentesquePellentesque nec erat ut nulla posuere"
    )
    mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger)

    # Select Paragraph/All
    docEditor.setCursorPosition(x+3)
    mainMenu.aSelectPar.activate(QAction.ActionEvent.Trigger)
    cursor = docEditor.textCursor()
    assert cursor.selectedText() == (
        "Pellentesque nec erat ut nulla posuere commodo. Curabitur nisi augue, imperdiet et porta "
        "imperdiet, efficitur id leo. Cras finibus arcu at nibh commodo congue. Proin suscipit "
        "placerat condimentum. Aenean ante enim, cursus id lorem a, blandit venenatis nibh. "
        "Maecenas suscipit porta elit, sit amet porta felis porttitor eu. Sed a dui nibh. "
        "Phasellus sed faucibus dui. Pellentesque felis nulla, ultrices non efficitur quis, "
        "rutrum id mi. Mauris tempus auctor nisl, in bibendum enim pellentesque sit amet. Proin "
        "nunc lacus, imperdiet nec posuere ac, interdum non lectus."
    )

    docEditor.setCursorPosition(x+3)
    mainMenu.aSelectAll.activate(QAction.ActionEvent.Trigger)
    cursor = docEditor.textCursor()
    assert len(cursor.selectedText()) == 1928

    # Clear the Text
    docEditor.clear()
    assert docEditor.isEmpty

    # Alignment & Indent
    # ==================

    cleanText = "A single, short paragraph.\n\n"
    docEditor.setPlainText(cleanText)
    docEditor.setCursorPosition(0)

    # Left Align
    mainMenu.aFmtAlignLeft.activate(QAction.ActionEvent.Trigger)
    fmtStr = "A single, short paragraph. <<"
    assert docEditor.getText()[:29] == fmtStr

    # Right Align
    mainMenu.aFmtAlignRight.activate(QAction.ActionEvent.Trigger)
    fmtStr = ">> A single, short paragraph."
    assert docEditor.getText()[:29] == fmtStr

    # Centre Align
    mainMenu.aFmtAlignCentre.activate(QAction.ActionEvent.Trigger)
    fmtStr = ">> A single, short paragraph. <<"
    assert docEditor.getText()[:32] == fmtStr

    # Left Indent
    mainMenu.aFmtIndentLeft.activate(QAction.ActionEvent.Trigger)
    fmtStr = "> A single, short paragraph."
    assert docEditor.getText()[:28] == fmtStr

    # Right Indent
    mainMenu.aFmtIndentRight.activate(QAction.ActionEvent.Trigger)
    fmtStr = "> A single, short paragraph. <"
    assert docEditor.getText()[:30] == fmtStr

    # No Format
    mainMenu.aFmtNoFormat.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText()[:30] == cleanText

    # Other Checks

    # Replace Quotes
    docEditor.setPlainText(
        "### New Text\n\n"
        "Text with 'single' quotes and 'tricky stuff's'.\n\n"
        'Also text with "double" quotes which are "less tricky".\n\n'
    )

    mainMenu.aSelectAll.activate(QAction.ActionEvent.Trigger)
    mainMenu.aFmtReplSng.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == (
        "### New Text\n\n"
        "Text with ‘single’ quotes and ‘tricky stuff’s’.\n\n"
        'Also text with "double" quotes which are "less tricky".\n\n'
    )

    mainMenu.aSelectAll.activate(QAction.ActionEvent.Trigger)
    mainMenu.aFmtReplDbl.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == (
        "### New Text\n\n"
        "Text with ‘single’ quotes and ‘tricky stuff’s’.\n\n"
        "Also text with “double” quotes which are “less tricky”.\n\n"
    )

    # Remove in-paragraph line breaks
    docEditor.setPlainText(
        "### New Text\n\n"
        "@char: Someone\n"
        "@location: Somewhere\n\n"
        "% Some comment ...\n\n"
        "Here is some text\non multiple\nlines.\n\n"
        "With another paragraph\nhere."
    )
    mainMenu.aFmtRmBreaks.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == (
        "### New Text\n\n"
        "@char: Someone\n"
        "@location: Somewhere\n\n"
        "% Some comment ...\n\n"
        "Here is some text on multiple lines.\n\n"
        "With another paragraph here.\n"
    )

    docEditor.setPlainText(
        "### New Text\n\n"
        "@char: Someone\n"
        "@location: Somewhere\n\n"
        "% Some comment ...\n\n"
        "Here is some text\non multiple\nlines.\n\n"
        "With another paragraph\nhere."
    )
    cursor = docEditor.textCursor()
    cursor.setPosition(74)
    cursor.movePosition(QtMoveRight, QtKeepAnchor, 29)
    docEditor.setTextCursor(cursor)
    mainMenu.aFmtRmBreaks.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == (
        "### New Text\n\n"
        "@char: Someone\n"
        "@location: Somewhere\n\n"
        "% Some comment ...\n\n"
        "Here is some text on multiple lines.\n\n"
        "With another paragraph\nhere."
    )

    # Test Invalid Document Action
    assert not docEditor.docAction(nwDocAction.NO_ACTION)

    # Test Invalid Formats
    docEditor.setPlainText(
        "### New Text\n\n"
        "@tag: Bod\n\n"
        "Text with 'single' quotes and 'tricky stuff's'.\n\n"
        'Also text with "double" quotes which are "less tricky".\n\n'
    )

    # Cannot Format Tag
    docEditor.setCursorPosition(17)
    assert not docEditor._formatBlock(nwDocAction.BLOCK_TXT)

    # Invalid Action
    docEditor.setCursorPosition(30)
    assert not docEditor._formatBlock(nwDocAction.NO_ACTION)

    # Ensure No Changes
    assert docEditor.getText() == (
        "### New Text\n\n"
        "@tag: Bod\n\n"
        "Text with 'single' quotes and 'tricky stuff's'.\n\n"
        'Also text with "double" quotes which are "less tricky".\n\n'
    )

    # qtbot.stop()


@pytest.mark.gui
def testGuiMainMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
    """Test the Insert menu."""
    buildTestProject(nwGUI, projPath)

    assert nwGUI.openDocument(C.hSceneDoc) is True
    mainMenu = nwGUI.mainMenu
    docEditor = nwGUI.docEditor
    docEditor.clear()

    # Test Faulty Inserts
    docEditor.insertText("hello world")
    assert docEditor.getText() == "hello world"
    docEditor.clear()

    docEditor.insertText(nwDocInsert.NO_INSERT)
    assert docEditor.isEmpty

    docEditor.insertText(None)
    assert docEditor.isEmpty

    # qtbot.stop()
    docEditor.clear()

    # Check Menu Entries
    mainMenu.aInsENDash.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == nwUnicode.U_ENDASH
    docEditor.clear()

    mainMenu.aInsEMDash.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == nwUnicode.U_EMDASH
    docEditor.clear()

    mainMenu.aInsHorBar.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == nwUnicode.U_HBAR
    docEditor.clear()

    mainMenu.aInsFigDash.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == nwUnicode.U_FGDASH
    docEditor.clear()

    mainMenu.aInsQuoteLS.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == CONFIG.fmtSQuoteOpen
    docEditor.clear()

    mainMenu.aInsQuoteRS.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == CONFIG.fmtSQuoteClose
    docEditor.clear()

    mainMenu.aInsQuoteLD.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == CONFIG.fmtDQuoteOpen
    docEditor.clear()

    mainMenu.aInsQuoteRD.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == CONFIG.fmtDQuoteClose
    docEditor.clear()

    mainMenu.aInsMSApos.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == nwUnicode.U_MAPOS
    docEditor.clear()

    mainMenu.aInsEllipsis.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == nwUnicode.U_HELLIP
    docEditor.clear()

    mainMenu.aInsPrime.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == nwUnicode.U_PRIME
    docEditor.clear()

    mainMenu.aInsDPrime.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == nwUnicode.U_DPRIME
    docEditor.clear()

    mainMenu.aInsBullet.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == nwUnicode.U_BULL
    docEditor.clear()

    mainMenu.aInsHyBull.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == nwUnicode.U_HYBULL
    docEditor.clear()

    mainMenu.aInsFlower.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == nwUnicode.U_FLOWER
    docEditor.clear()

    mainMenu.aInsPerMille.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == nwUnicode.U_PERMIL
    docEditor.clear()

    mainMenu.aInsDegree.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == nwUnicode.U_DEGREE
    docEditor.clear()

    mainMenu.aInsMinus.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == nwUnicode.U_MINUS
    docEditor.clear()

    mainMenu.aInsTimes.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == nwUnicode.U_TIMES
    docEditor.clear()

    mainMenu.aInsDivide.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == nwUnicode.U_DIVIDE
    docEditor.clear()

    mainMenu.aInsNBSpace.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == nwUnicode.U_NBSP
    docEditor.clear()

    mainMenu.aInsThinSpace.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == nwUnicode.U_THSP
    docEditor.clear()

    mainMenu.aInsThinNBSpace.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == nwUnicode.U_THNBSP
    docEditor.clear()

    # Insert Keywords
    # ===============

    for action, key in zip(mainMenu.mInsKeywords.actions(), nwKeyWords.ALL_KEYS, strict=False):
        docEditor.setPlainText("Stuff")
        action.activate(QAction.ActionEvent.Trigger)
        assert docEditor.getText() == f"Stuff\n{key}: "

    # Faulty Keyword Inserts
    assert not docEditor.insertKeyWord("blabla")
    with monkeypatch.context() as mp:
        mp.setattr(QTextBlock, "isValid", lambda *a, **k: False)
        assert not docEditor.insertKeyWord(nwKeyWords.TAG_KEY)

    # Insert Fields
    # =============

    for action, field in zip(mainMenu.mInsField.actions(), nwStats.ALL_FIELDS, strict=False):
        value = nwShortcode.FIELD_VALUE.format(field)
        docEditor.setPlainText("Stuff ")
        docEditor.setCursorPosition(6)
        action.activate(QAction.ActionEvent.Trigger)
        assert docEditor.getText() == f"Stuff {value}"

    docEditor.clear()

    # Insert Special Comments
    # =======================

    docEditor.setPlainText("Stuff\n")
    mainMenu.aInsSynopsis.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == "Stuff\n%Synopsis: \n"

    docEditor.setPlainText("Stuff\n")
    mainMenu.aInsShort.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == "Stuff\n%Short: \n"

    # Breaks and Vertical Space
    # =========================

    docEditor.setPlainText("### Stuff\n")
    mainMenu.aInsNewPage.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == "[newpage]\n### Stuff\n"

    docEditor.setPlainText("Line OneLine Two\n")
    docEditor.setCursorPosition(8)
    mainMenu.aInsLineBreak.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == "Line One[br]Line Two\n"

    docEditor.setPlainText("### Stuff\n")
    mainMenu.aInsVSpaceS.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == "[vspace]\n### Stuff\n"

    docEditor.setPlainText("### Stuff\n")
    mainMenu.aInsVSpaceM.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == "[vspace:2]\n### Stuff\n"

    docEditor.clear()

    # Insert Text from File
    # =====================

    nwGUI.closeDocument()

    # First, with no path
    monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: ("", ""))
    assert not nwGUI.importDocument()

    # Then with a path, but an invalid one
    monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (" ", ""))
    assert not nwGUI.importDocument()

    # Then a valid path, but bot a file that exists
    iFile = fncPath / "import.txt"
    monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(iFile), ""))
    assert not nwGUI.importDocument()

    # Create the file and try again, but with no target document open
    writeFile(iFile, "Foo")
    assert not nwGUI.importDocument()

    # Open the document from before, and add some text to it
    nwGUI.openDocument(C.hSceneDoc)
    docEditor.setPlainText("Bar")
    assert docEditor.getText() == "Bar"

    # The document isn't empty, so the message box should pop
    with monkeypatch.context() as mp:
        mp.setattr(QMessageBox, "result", lambda *a, **k: QMessageBox.StandardButton.No)
        assert not nwGUI.importDocument()
        assert docEditor.getText() == "Bar"

    # Finally, accept the replaced text, this time we use the menu entry to trigger it
    mainMenu.aImportFile.activate(QAction.ActionEvent.Trigger)
    assert docEditor.getText() == "Foo"

    # Reveal File Location
    # ====================

    mainMenu.aFileDetails.activate(QAction.ActionEvent.Trigger)
    path = str(projPath / "content" / "000000000000f.nwd")
    assert SHARED.lastAlert.endswith(f"File Location: {path}")

    # qtbot.stop()
