"""
novelWriter – GUI Theme and Icons Classes 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 pathlib import Path

import pytest

from PyQt5.QtGui import QColor, QIcon, QPalette, QPixmap
from PyQt5.QtWidgets import QApplication

from novelwriter import CONFIG, SHARED
from novelwriter.common import NWConfigParser
from novelwriter.constants import nwLabels
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType

from tests.mocked import causeOSError
from tests.tools import writeFile


@pytest.mark.gui
def testGuiTheme_Main(qtbot, nwGUI, tstPaths):
    """Test the theme class init."""
    mainTheme = SHARED.theme

    # Methods
    # =======

    mSize = mainTheme.getTextWidth("m")
    assert mSize > 0
    assert mainTheme.getTextWidth("m", mainTheme.guiFont) == mSize

    # Scan for Themes
    # ===============

    assert mainTheme._listConf({}, Path("not_a_path")) is False

    themeOne = tstPaths.cnfDir / "themes" / "themeone.conf"
    themeTwo = tstPaths.cnfDir / "themes" / "themetwo.conf"
    writeFile(themeOne, "# Stuff")
    writeFile(themeTwo, "# Stuff")

    result = {}
    assert mainTheme._listConf(result, tstPaths.cnfDir / "themes") is True
    assert result["themeone"] == themeOne
    assert result["themetwo"] == themeTwo

    # Parse Colours
    # =============

    parser = NWConfigParser()
    parser["Palette"] = {
        "colour1": "100, 150, 200",            # Valid
        "colour2": "100, 150, 200, 250",       # With alpha
        "colour3": "100, 150, 200, 250, 300",  # Too many values
        "colour4": "250, 250",                 # Missing blue
        "colour5": "-10, 127, 300",            # Invalid red and blue
        "colour6": "bob, 127, 255",            # Invalid red
    }

    # Test the parser for several valid and invalid values
    assert mainTheme._parseColour(parser, "Palette", "colour1").getRgb() == (100, 150, 200, 255)
    assert mainTheme._parseColour(parser, "Palette", "colour2").getRgb() == (100, 150, 200, 250)
    assert mainTheme._parseColour(parser, "Palette", "colour3").getRgb() == (100, 150, 200, 250)
    assert mainTheme._parseColour(parser, "Palette", "colour4").getRgb() == (250, 250, 0, 255)
    assert mainTheme._parseColour(parser, "Palette", "colour5").getRgb() == (0, 0, 0, 0)
    assert mainTheme._parseColour(parser, "Palette", "colour6").getRgb() == (0, 127, 255, 255)

    # The palette should load with the parsed values
    mainTheme._setPalette(parser, "Palette", "colour1", QPalette.ColorRole.Window)
    assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 255)
    mainTheme._setPalette(parser, "Palette", "colour2", QPalette.ColorRole.Window)
    assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250)
    mainTheme._setPalette(parser, "Palette", "colour3", QPalette.ColorRole.Window)
    assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250)
    mainTheme._setPalette(parser, "Palette", "colour4", QPalette.ColorRole.Window)
    assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (250, 250, 0, 255)
    mainTheme._setPalette(parser, "Palette", "colour5", QPalette.ColorRole.Window)
    assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 0)
    mainTheme._setPalette(parser, "Palette", "colour6", QPalette.ColorRole.Window)
    assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 127, 255, 255)

    # Non-existing value should return default colour
    mainTheme._setPalette(parser, "Palette", "stuff", QPalette.ColorRole.Window)
    assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 255)

    # qtbot.stop()


@pytest.mark.gui
def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths):
    """Test the theme part of the class."""
    mainTheme = SHARED.theme

    # List Themes
    # ===========

    # Block the reading of the files
    with monkeypatch.context() as mp:
        mp.setattr("builtins.open", causeOSError)
        assert mainTheme.listThemes() == []

    # Load the theme info, default themes first
    themesList = mainTheme.listThemes()
    assert themesList[0] == ("default_dark", "Default Dark Theme")
    assert themesList[1] == ("default_light", "Default Light Theme")
    assert themesList[2] == ("cyberpunk_night", "Cyberpunk Night")
    assert themesList[3] == ("dracula", "Dracula")

    # A second call should returned the cached list
    assert mainTheme.listThemes() == mainTheme._themeList

    # Check handling of broken theme settings
    CONFIG.guiTheme = "not_a_theme"
    availThemes = mainTheme._availThemes
    mainTheme._availThemes = {}
    assert mainTheme.loadTheme() is False
    mainTheme._availThemes = availThemes

    # Check handling of unreadable file
    CONFIG.guiTheme = "default"
    with monkeypatch.context() as mp:
        mp.setattr("builtins.open", causeOSError)
        assert mainTheme.loadTheme() is False

    # Load Default Theme
    # ==================

    # Set a mock colour for the window background
    mainTheme._guiPalette.color(QPalette.ColorRole.Window).setRgb(0, 0, 0, 0)

    # Load the default theme
    CONFIG.guiTheme = "default"
    assert mainTheme.loadTheme() is True

    # This should load a standard palette
    wCol = QApplication.style().standardPalette().color(QPalette.ColorRole.Window).getRgb()
    assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == wCol

    # Mock Dark Theme
    # ===============

    mockTheme: Path = tstPaths.cnfDir / "themes" / "test.conf"
    mockTheme.write_text(
        "[Main]\n"
        "name = Test\n"
        "\n"
        "[Palette]\n"
        "window = 0, 0, 0\n"
        "windowtext = 255, 255, 255\n"
        "\n"
        "[GUI]\n"
        "helptext = 0, 0, 0\n"
    )
    mainTheme._availThemes["test"] = mockTheme

    CONFIG.guiTheme = "test"
    assert mainTheme.loadTheme() is True
    assert mainTheme.isLightTheme is False
    assert mainTheme.helpText.getRgb() == (165, 165, 165, 255)

    # Load Default Light Theme
    # ========================

    CONFIG.guiTheme = "default_light"
    assert mainTheme.loadTheme() is True

    # Check a few values
    assert mainTheme._guiPalette.color(
        QPalette.ColorRole.Window).getRgb() == (239, 239, 239, 255)
    assert mainTheme._guiPalette.color(
        QPalette.ColorRole.WindowText).getRgb() == (0, 0, 0, 255)
    assert mainTheme._guiPalette.color(
        QPalette.ColorRole.Base).getRgb() == (255, 255, 255, 255)
    assert mainTheme._guiPalette.color(
        QPalette.ColorRole.AlternateBase).getRgb() == (239, 239, 239, 255)

    # Load Default Dark Theme
    # =======================

    CONFIG.guiTheme = "default_dark"
    assert mainTheme.loadTheme() is True

    # Check a few values
    assert mainTheme._guiPalette.color(
        QPalette.ColorRole.Window).getRgb()        == (54, 54, 54, 255)
    assert mainTheme._guiPalette.color(
        QPalette.ColorRole.WindowText).getRgb()    == (204, 204, 204, 255)
    assert mainTheme._guiPalette.color(
        QPalette.ColorRole.Base).getRgb()          == (62, 62, 62, 255)
    assert mainTheme._guiPalette.color(
        QPalette.ColorRole.AlternateBase).getRgb() == (78, 78, 78, 255)

    # qtbot.stop()


@pytest.mark.gui
def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI):
    """Test the syntax part of the class."""
    mainTheme = SHARED.theme

    # List Themes
    # ===========

    # Block the reading of the files
    with monkeypatch.context() as mp:
        mp.setattr("builtins.open", causeOSError)
        assert mainTheme.listThemes() == []

    # Load the syntax info
    syntaxList = mainTheme.listSyntax()
    assert syntaxList[0] == ("default_dark", "Default Dark")
    assert syntaxList[1] == ("default_light", "Default Light")

    # A second call should returned the cached list
    assert mainTheme.listSyntax() == mainTheme._syntaxList

    # Check handling of broken theme settings
    availSyntax = mainTheme._availSyntax
    mainTheme._availSyntax = {}
    CONFIG.guiSyntax = "not_a_syntax"
    assert mainTheme.loadSyntax() is False
    mainTheme._availSyntax = availSyntax

    # Check handling of unreadable file
    CONFIG.guiSyntax = "default_light"
    with monkeypatch.context() as mp:
        mp.setattr("builtins.open", causeOSError)
        assert mainTheme.loadSyntax() is False

    # Load Default Light Syntax
    # =========================

    # Load the default syntax
    CONFIG.guiSyntax = "default_light"
    assert mainTheme.loadSyntax() is True

    # Check some values
    assert mainTheme.syntaxName == "Default Light"
    assert mainTheme.colBack == QColor(255, 255, 255)
    assert mainTheme.colText == QColor(0, 0, 0)
    assert mainTheme.colLink == QColor(0, 0, 200)

    # Load Default Dark Theme
    # =======================

    # Load the default syntax
    CONFIG.guiSyntax = "default_dark"
    assert mainTheme.loadSyntax() is True

    # Check some values
    assert mainTheme.syntaxName == "Default Dark"
    assert mainTheme.colBack == QColor(42, 42, 42)
    assert mainTheme.colText == QColor(204, 204, 204)
    assert mainTheme.colLink == QColor(102, 153, 204)

    # qtbot.stop()


@pytest.mark.gui
def testGuiTheme_IconThemes(qtbot, caplog, monkeypatch, nwGUI, tstPaths):
    """Test the icon cache class."""
    iconCache = SHARED.theme.iconCache

    # Load Theme
    # ==========

    # Invalid theme name
    assert iconCache.loadTheme("not_a_theme") is False

    # Check handling of unreadable file
    with monkeypatch.context() as mp:
        mp.setattr("builtins.open", causeOSError)
        assert iconCache.loadTheme("material_rounded_normal") is False

    # Load working theme file
    assert iconCache.loadTheme("material_rounded_normal") is True
    assert iconCache.themeName == "Material Symbols - Rounded Medium"

    # Load with project colour override
    purple = iconCache._svgColours["purple"]
    assert iconCache._svgColours["root"] != purple
    assert iconCache._svgColours["folder"] != purple
    assert iconCache._svgColours["file"] != purple
    assert iconCache._svgColours["title"] != purple
    assert iconCache._svgColours["chapter"] != purple
    assert iconCache._svgColours["scene"] != purple
    assert iconCache._svgColours["note"] != purple

    CONFIG.iconColTree = "purple"
    assert iconCache.loadTheme("material_rounded_normal") is True
    assert iconCache._svgColours["root"] == purple
    assert iconCache._svgColours["folder"] == purple
    assert iconCache._svgColours["file"] == purple
    assert iconCache._svgColours["title"] == purple
    assert iconCache._svgColours["chapter"] == purple
    assert iconCache._svgColours["scene"] == purple
    assert iconCache._svgColours["note"] == purple

    # Change some colours
    iconCache.setIconColor("root", QColor(255, 255, 255))
    assert iconCache._svgColours["root"] != purple
    assert iconCache._svgColours["root"] == b"#ffffff"

    # List Themes
    # ===========

    # Load error returns empty list
    with monkeypatch.context() as mp:
        mp.setattr("builtins.open", causeOSError)
        themes = iconCache.listThemes()
        assert themes == []

    # Successful read
    themes = iconCache.listThemes()
    assert len(themes) > 1
    assert "material_rounded_normal" in dict(themes)

    # Load error doesn't matter on second read since list is cached
    with monkeypatch.context() as mp:
        mp.setattr("builtins.open", causeOSError)
        assert iconCache.listThemes() == themes

    # qtbot.stop()


@pytest.mark.gui
def testGuiTheme_LoadIcons(qtbot, nwGUI):
    """Test the icon cache class."""
    iconCache = SHARED.theme.iconCache
    assert iconCache.loadTheme("material_rounded_normal") is True

    # Load Icons
    # ==========

    # Load an unknown icon
    qIcon = iconCache.getIcon("stuff")
    assert isinstance(qIcon, QIcon)
    assert qIcon == iconCache._noIcon

    # Load an icon, it is likely already cached
    qIcon = iconCache.getIcon("add")
    assert isinstance(qIcon, QIcon)
    assert qIcon.isNull() is False

    # Load it as a pixmap with a size
    qPix = iconCache.getPixmap("add", (50, 50))
    assert isinstance(qPix, QPixmap)
    assert qPix.isNull() is False
    assert qPix.width() == 50
    assert qPix.height() == 50

    # Load app icon
    qIcon = iconCache.getIcon("novelwriter")
    assert isinstance(qIcon, QIcon)
    assert qIcon != iconCache._noIcon

    # Load mime icon
    qIcon = iconCache.getIcon("proj_nwx")
    assert isinstance(qIcon, QIcon)
    assert qIcon != iconCache._noIcon

    # Toggle icon
    qIcon = iconCache.getToggleIcon("bullet", (24, 24))
    assert isinstance(qIcon, QIcon)
    assert qIcon != iconCache._noIcon
    pOn = qIcon.pixmap(24, 24, QIcon.Mode.Normal, QIcon.State.On)
    pOff = qIcon.pixmap(24, 24, QIcon.Mode.Normal, QIcon.State.Off)
    assert pOn != pOff

    # Unknown toggle icon
    qIcon = iconCache.getToggleIcon("stuff", (24, 24))
    assert isinstance(qIcon, QIcon)
    assert qIcon == iconCache._noIcon

    # Load Item Icons
    # ===============

    # Root -> Not Null
    assert iconCache.getItemIcon(
        nwItemType.ROOT, nwItemClass.NOVEL, nwItemLayout.NO_LAYOUT, hLevel="H0"
    ) == iconCache.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL], "root")

    # Folder -> Not Null
    assert iconCache.getItemIcon(
        nwItemType.FOLDER, nwItemClass.NOVEL, nwItemLayout.NO_LAYOUT, hLevel="H0"
    ) == iconCache.getIcon("prj_folder", "folder")

    # Document H0 -> Not Null
    assert iconCache.getItemIcon(
        nwItemType.FILE, nwItemClass.NOVEL, nwItemLayout.NO_LAYOUT, hLevel="H0"
    ) == iconCache._noIcon

    # Document H1 -> Not Null
    assert iconCache.getItemIcon(
        nwItemType.FILE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H1"
    ) == iconCache.getIcon("prj_title", "title")

    # Document H2 -> Not Null
    assert iconCache.getItemIcon(
        nwItemType.FILE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H2"
    ) == iconCache.getIcon("prj_chapter", "chapter")

    # Document H3 -> Not Null
    assert iconCache.getItemIcon(
        nwItemType.FILE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H3"
    ) == iconCache.getIcon("prj_scene", "scene")

    # Document H4 -> Not Null
    assert iconCache.getItemIcon(
        nwItemType.FILE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H4"
    ) == iconCache.getIcon("prj_document", "file")

    # Document H5 -> Not Null
    assert iconCache.getItemIcon(
        nwItemType.FILE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H5"
    ) == iconCache.getIcon("prj_document", "file")

    # Note -> Not Null
    assert iconCache.getItemIcon(
        nwItemType.FILE, nwItemClass.NOVEL, nwItemLayout.NOTE, hLevel="H5"
    ) == iconCache.getIcon("prj_note", "note")

    # No Type -> Null
    assert iconCache.getItemIcon(
        nwItemType.NO_TYPE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H0"
    ) == iconCache._noIcon

    # qtbot.stop()


@pytest.mark.gui
def testGuiTheme_LoadDecorations(qtbot, monkeypatch, nwGUI):
    """Test the icon cache class."""
    iconCache = SHARED.theme.iconCache
    assert iconCache.loadTheme("material_rounded_normal") is True

    # Load Decorations
    # ================

    # Invalid name should return empty pixmap
    qPix = iconCache.loadDecoration("stuff")
    assert qPix.isNull() is True

    # Load an image
    qPix = iconCache.loadDecoration("welcome")
    assert qPix.isNull() is False

    # Fail finding the file
    with monkeypatch.context() as mp:
        mp.setattr("pathlib.Path.is_file", lambda *a: False)
        qPix = iconCache.loadDecoration("welcome")
        assert qPix.isNull() is True

    # Test image sizes
    qPix = iconCache.loadDecoration("welcome", w=100, h=None)
    assert qPix.isNull() is False
    assert qPix.width() == 100
    assert qPix.height() > 50

    qPix = iconCache.loadDecoration("welcome", w=None, h=100)
    assert qPix.isNull() is False
    assert qPix.width() > 100
    assert qPix.height() == 100

    qPix = iconCache.loadDecoration("welcome", w=100, h=100)
    assert qPix.isNull() is False
    assert qPix.width() == 100
    assert qPix.height() == 100

    # Header Decorations
    # ==================

    assert iconCache.getHeaderDecoration(-1) == iconCache._headerDec[0]
    assert iconCache.getHeaderDecoration(0)  == iconCache._headerDec[0]
    assert iconCache.getHeaderDecoration(1)  == iconCache._headerDec[1]
    assert iconCache.getHeaderDecoration(2)  == iconCache._headerDec[2]
    assert iconCache.getHeaderDecoration(3)  == iconCache._headerDec[3]
    assert iconCache.getHeaderDecoration(4)  == iconCache._headerDec[4]
    assert iconCache.getHeaderDecoration(5)  == iconCache._headerDec[4]

    # Narrow Header Decorations
    # =========================

    assert iconCache.getHeaderDecorationNarrow(-1) == iconCache._headerDecNarrow[0]
    assert iconCache.getHeaderDecorationNarrow(0)  == iconCache._headerDecNarrow[0]
    assert iconCache.getHeaderDecorationNarrow(1)  == iconCache._headerDecNarrow[1]
    assert iconCache.getHeaderDecorationNarrow(2)  == iconCache._headerDecNarrow[2]
    assert iconCache.getHeaderDecorationNarrow(3)  == iconCache._headerDecNarrow[3]
    assert iconCache.getHeaderDecorationNarrow(4)  == iconCache._headerDecNarrow[4]
    assert iconCache.getHeaderDecorationNarrow(5)  == iconCache._headerDecNarrow[5]
    assert iconCache.getHeaderDecorationNarrow(6)  == iconCache._headerDecNarrow[5]

    # qtbot.stop()
