# -*- coding: utf-8 -*-
"""
novelWriter – Config Class Tester
=================================

This file is a part of novelWriter
Copyright 2018–2021, Veronica Berglyd Olsen

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/>.
"""

import pytest
import sys
import os
import configparser

from shutil import copyfile

from dummy import causeOSError
from tools import cmpFiles

from nw.config import Config
from nw.constants import nwConst, nwFiles

@pytest.mark.base
def testBaseConfig_Constructor(monkeypatch):
    """Test config contructor.
    """
    # Linux
    monkeypatch.setattr("sys.platform", "linux")
    tstConf = Config()
    assert tstConf.osLinux is True
    assert tstConf.osDarwin is False
    assert tstConf.osWindows is False
    assert tstConf.osUnknown is False
    monkeypatch.undo()

    # macOS
    monkeypatch.setattr("sys.platform", "darwin")
    tstConf = Config()
    assert tstConf.osLinux is False
    assert tstConf.osDarwin is True
    assert tstConf.osWindows is False
    assert tstConf.osUnknown is False
    monkeypatch.undo()

    # Windows
    monkeypatch.setattr("sys.platform", "win32")
    tstConf = Config()
    assert tstConf.osLinux is False
    assert tstConf.osDarwin is False
    assert tstConf.osWindows is True
    assert tstConf.osUnknown is False
    monkeypatch.undo()

    # Cygwin
    monkeypatch.setattr("sys.platform", "cygwin")
    tstConf = Config()
    assert tstConf.osLinux is False
    assert tstConf.osDarwin is False
    assert tstConf.osWindows is True
    assert tstConf.osUnknown is False
    monkeypatch.undo()

    # Other
    monkeypatch.setattr("sys.platform", "some_other_os")
    tstConf = Config()
    assert tstConf.osLinux is False
    assert tstConf.osDarwin is False
    assert tstConf.osWindows is False
    assert tstConf.osUnknown is True
    monkeypatch.undo()

# END Test testBaseConfig_Constructor

@pytest.mark.base
def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir):
    """Test config intialisation.
    """
    tstConf = Config()

    confFile = os.path.join(tmpDir, "novelwriter.conf")
    testFile = os.path.join(outDir, "baseConfig_novelwriter.conf")
    compFile = os.path.join(refDir, "baseConfig_novelwriter.conf")

    # Make sure we don't have any old conf file
    if os.path.isfile(confFile):
        os.unlink(confFile)

    # Let the config class figure out the path
    monkeypatch.setattr("PyQt5.QtCore.QStandardPaths.writableLocation", lambda *args: fncDir)
    tstConf.verQtValue = 50600
    tstConf.initConfig()
    assert tstConf.confPath == os.path.join(fncDir, tstConf.appHandle)
    assert tstConf.dataPath == os.path.join(fncDir, tstConf.appHandle)
    assert not os.path.isfile(confFile)
    tstConf.verQtValue = 50000
    tstConf.initConfig()
    assert tstConf.confPath == os.path.join(fncDir, tstConf.appHandle)
    assert tstConf.dataPath == os.path.join(fncDir, tstConf.appHandle)
    assert not os.path.isfile(confFile)
    monkeypatch.undo()

    # Fail to make folders
    monkeypatch.setattr("os.mkdir", causeOSError)

    tstConfDir = os.path.join(fncDir, "test_conf")
    tstConf.initConfig(confPath=tstConfDir, dataPath=tmpDir)
    assert tstConf.confPath is None
    assert tstConf.dataPath == tmpDir
    assert not os.path.isfile(confFile)

    tstDataDir = os.path.join(fncDir, "test_data")
    tstConf.initConfig(confPath=tmpDir, dataPath=tstDataDir)
    assert tstConf.confPath == tmpDir
    assert tstConf.dataPath is None
    assert os.path.isfile(confFile)
    os.unlink(confFile)

    monkeypatch.undo()

    # Test load/save with no path
    tstConf.confPath = None
    assert not tstConf.loadConfig()
    assert not tstConf.saveConfig()

    # Run again and set the paths directly and correctly
    # This should create a config file as well
    monkeypatch.setattr("os.path.expanduser", lambda *args: "")
    tstConf.spellTool = nwConst.SP_INTERNAL
    tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir)
    assert tstConf.confPath == tmpDir
    assert tstConf.dataPath == tmpDir
    assert os.path.isfile(confFile)

    copyfile(confFile, testFile)
    assert cmpFiles(testFile, compFile, [2, 9, 10])
    monkeypatch.undo()

    # Load and save with OSError
    monkeypatch.setattr("builtins.open", causeOSError)

    assert not tstConf.loadConfig()
    assert tstConf.hasError is True
    assert tstConf.errData != []
    assert tstConf.getErrData().startswith("Could not")
    assert tstConf.hasError is False
    assert tstConf.errData == []

    assert not tstConf.saveConfig()
    assert tstConf.hasError is True
    assert tstConf.errData != []
    assert tstConf.getErrData().startswith("Could not")
    assert tstConf.hasError is False
    assert tstConf.errData == []

    monkeypatch.undo()

    assert tstConf.loadConfig()
    assert tstConf.saveConfig()

    # Test Correcting Quote Settings
    origDbl = tstConf.fmtDoubleQuotes
    origSng = tstConf.fmtSingleQuotes
    orDoDbl = tstConf.doReplaceDQuote
    orDoSng = tstConf.doReplaceSQuote

    tstConf.fmtDoubleQuotes = ["\"", "\""]
    tstConf.fmtSingleQuotes = ["'", "'"]
    tstConf.doReplaceDQuote = True
    tstConf.doReplaceSQuote = True
    assert tstConf.saveConfig()

    assert tstConf.loadConfig()
    assert not tstConf.doReplaceDQuote
    assert not tstConf.doReplaceSQuote

    tstConf.fmtDoubleQuotes = origDbl
    tstConf.fmtSingleQuotes = origSng
    tstConf.doReplaceDQuote = orDoDbl
    tstConf.doReplaceSQuote = orDoSng
    assert tstConf.saveConfig()

    copyfile(confFile, testFile)
    assert cmpFiles(testFile, compFile, [2, 9, 10])

# END Test testBaseConfig_Init

@pytest.mark.base
def testBaseConfig_RecentCache(monkeypatch, tmpConf, tmpDir, fncDir):
    """Test recent cache file.
    """
    # Check failing
    tmpConf.dataPath = None
    assert not tmpConf.loadRecentCache()
    assert not tmpConf.saveRecentCache()
    tmpConf.dataPath = tmpDir

    # Add a couple of values
    pathOne = os.path.join(fncDir, "projPathOne", nwFiles.PROJ_FILE)
    pathTwo = os.path.join(fncDir, "projPathTwo", nwFiles.PROJ_FILE)
    assert tmpConf.updateRecentCache(pathOne, "Proj One", 100, 1600002000)
    assert tmpConf.updateRecentCache(pathTwo, "Proj Two", 200, 1600005600)
    assert tmpConf.recentProj == {
        pathOne: {"time": 1600002000, "title": "Proj One", "words": 100},
        pathTwo: {"time": 1600005600, "title": "Proj Two", "words": 200},
    }

    # Fail to Save
    monkeypatch.setattr("builtins.open", causeOSError)
    assert not tmpConf.saveRecentCache()
    monkeypatch.undo()

    # Save Proper
    cacheFile = os.path.join(tmpDir, nwFiles.RECENT_FILE)
    assert tmpConf.saveRecentCache()
    assert tmpConf.saveRecentCache()
    assert os.path.isfile(cacheFile)

    # Fail to Load
    monkeypatch.setattr("builtins.open", causeOSError)
    tmpConf.recentProj = {}
    assert not tmpConf.loadRecentCache()
    assert tmpConf.recentProj == {}
    monkeypatch.undo()

    # Load Proper
    tmpConf.recentProj = {}
    assert tmpConf.loadRecentCache()
    assert tmpConf.recentProj == {
        pathOne: {"time": 1600002000, "title": "Proj One", "words": 100},
        pathTwo: {"time": 1600005600, "title": "Proj Two", "words": 200},
    }

    # Remove Non-Existent Entry
    assert not tmpConf.removeFromRecentCache("dummy")
    assert tmpConf.recentProj == {
        pathOne: {"time": 1600002000, "title": "Proj One", "words": 100},
        pathTwo: {"time": 1600005600, "title": "Proj Two", "words": 200},
    }

    # Remove Second Entry
    assert tmpConf.removeFromRecentCache(pathTwo)
    assert tmpConf.recentProj == {
        pathOne: {"time": 1600002000, "title": "Proj One", "words": 100},
    }

# END Test testBaseConfig_RecentCache

@pytest.mark.base
def testBaseConfig_SetPath(tmpConf, tmpDir):
    """Test path setters.
    """
    # Conf Path
    assert tmpConf.setConfPath(None)
    assert not tmpConf.setConfPath(os.path.join("somewhere", "over", "the", "rainbow"))
    assert tmpConf.setConfPath(os.path.join(tmpDir, "novelwriter.conf"))
    assert tmpConf.confPath == tmpDir
    assert tmpConf.confFile == "novelwriter.conf"
    assert not tmpConf.confChanged

    # Data Path
    assert tmpConf.setDataPath(None)
    assert not tmpConf.setDataPath(os.path.join("somewhere", "over", "the", "rainbow"))
    assert tmpConf.setDataPath(tmpDir)
    assert tmpConf.dataPath == tmpDir
    assert not tmpConf.confChanged

    # Last Path
    assert tmpConf.setLastPath(None)
    assert tmpConf.lastPath == ""

    assert tmpConf.setLastPath(os.path.join(tmpDir, "file.tmp"))
    assert tmpConf.lastPath == tmpDir

    assert tmpConf.setLastPath("")
    assert tmpConf.lastPath == ""

# END Test testBaseConfig_SetPath

@pytest.mark.base
def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir):
    """Set various sizes and positions
    """
    confFile = os.path.join(tmpDir, "novelwriter.conf")
    testFile = os.path.join(outDir, "baseConfig_novelwriter.conf")
    compFile = os.path.join(refDir, "baseConfig_novelwriter.conf")

    # GUI Scaling
    # ===========
    tmpConf.guiScale = 1.0
    assert tmpConf.pxInt(10) == 10
    assert tmpConf.pxInt(13) == 13
    assert tmpConf.rpxInt(10) == 10
    assert tmpConf.rpxInt(13) == 13

    tmpConf.guiScale = 2.0
    assert tmpConf.pxInt(10) == 20
    assert tmpConf.pxInt(13) == 26
    assert tmpConf.rpxInt(10) == 5
    assert tmpConf.rpxInt(13) == 6

    # Setter + Getter Combos
    # ======================

    # Window Size
    tmpConf.guiScale = 1.0
    assert tmpConf.setWinSize(1205, 655)
    assert not tmpConf.confChanged

    tmpConf.guiScale = 2.0
    assert tmpConf.setWinSize(70, 70)
    assert tmpConf.getWinSize() == [70, 70]
    assert tmpConf.winGeometry == [35, 35]

    tmpConf.guiScale = 1.0
    assert tmpConf.setWinSize(70, 70)
    assert tmpConf.getWinSize() == [70, 70]
    assert tmpConf.winGeometry == [70, 70]

    assert tmpConf.setWinSize(1200, 650)

    # Project Tree Columns
    tmpConf.guiScale = 2.0
    assert tmpConf.setTreeColWidths([10, 20, 25])
    assert tmpConf.getTreeColWidths() == [10, 20, 24]
    assert tmpConf.treeColWidth == [5, 10, 12]

    tmpConf.guiScale = 1.0
    assert tmpConf.setTreeColWidths([10, 20, 25])
    assert tmpConf.getTreeColWidths() == [10, 20, 25]
    assert tmpConf.treeColWidth == [10, 20, 25]

    assert tmpConf.setTreeColWidths([200, 50, 30])

    # Novel Tree Columns
    tmpConf.guiScale = 2.0
    assert tmpConf.setNovelColWidths([10, 20])
    assert tmpConf.getNovelColWidths() == [10, 20]
    assert tmpConf.novelColWidth == [5, 10]

    tmpConf.guiScale = 1.0
    assert tmpConf.setNovelColWidths([10, 20])
    assert tmpConf.getNovelColWidths() == [10, 20]
    assert tmpConf.novelColWidth == [10, 20]

    assert tmpConf.setNovelColWidths([200, 50])

    # Project Settings Tree Columns
    tmpConf.guiScale = 2.0
    assert tmpConf.setProjColWidths([10, 20, 30])
    assert tmpConf.getProjColWidths() == [10, 20, 30]
    assert tmpConf.projColWidth == [5, 10, 15]

    tmpConf.guiScale = 1.0
    assert tmpConf.setProjColWidths([10, 20, 30])
    assert tmpConf.getProjColWidths() == [10, 20, 30]
    assert tmpConf.projColWidth == [10, 20, 30]

    assert tmpConf.setProjColWidths([200, 60, 140])

    # Main Pane Splitter
    tmpConf.guiScale = 2.0
    assert tmpConf.setMainPanePos([200, 700])
    assert tmpConf.getMainPanePos() == [200, 700]
    assert tmpConf.mainPanePos == [100, 350]

    tmpConf.guiScale = 1.0
    assert tmpConf.setMainPanePos([200, 700])
    assert tmpConf.getMainPanePos() == [200, 700]
    assert tmpConf.mainPanePos == [200, 700]

    assert tmpConf.setMainPanePos([300, 800])

    # Doc Pane Splitter
    tmpConf.guiScale = 2.0
    assert tmpConf.setDocPanePos([300, 300])
    assert tmpConf.getDocPanePos() == [300, 300]
    assert tmpConf.docPanePos == [150, 150]

    tmpConf.guiScale = 1.0
    assert tmpConf.setDocPanePos([300, 300])
    assert tmpConf.getDocPanePos() == [300, 300]
    assert tmpConf.docPanePos == [300, 300]

    assert tmpConf.setDocPanePos([400, 400])

    # View Pane Splitter
    tmpConf.guiScale = 2.0
    assert tmpConf.setViewPanePos([400, 250])
    assert tmpConf.getViewPanePos() == [400, 250]
    assert tmpConf.viewPanePos == [200, 125]

    tmpConf.guiScale = 1.0
    assert tmpConf.setViewPanePos([400, 250])
    assert tmpConf.getViewPanePos() == [400, 250]
    assert tmpConf.viewPanePos == [400, 250]

    assert tmpConf.setViewPanePos([500, 150])

    # Outline Pane Splitter
    tmpConf.guiScale = 2.0
    assert tmpConf.setOutlinePanePos([400, 250])
    assert tmpConf.getOutlinePanePos() == [400, 250]
    assert tmpConf.outlnPanePos == [200, 125]

    tmpConf.guiScale = 1.0
    assert tmpConf.setOutlinePanePos([400, 250])
    assert tmpConf.getOutlinePanePos() == [400, 250]
    assert tmpConf.outlnPanePos == [400, 250]

    assert tmpConf.setOutlinePanePos([500, 150])

    # Getters Only
    # ============
    tmpConf.guiScale = 1.0
    assert tmpConf.getTextWidth() == 600
    assert tmpConf.getTextMargin() == 40
    assert tmpConf.getTabWidth() == 40
    assert tmpConf.getFocusWidth() == 800

    tmpConf.guiScale = 2.0
    assert tmpConf.getTextWidth() == 1200
    assert tmpConf.getTextMargin() == 80
    assert tmpConf.getTabWidth() == 80
    assert tmpConf.getFocusWidth() == 1600

    # Flag Setters
    # ============
    assert not tmpConf.setShowRefPanel(False)
    assert not tmpConf.showRefPanel
    assert tmpConf.setShowRefPanel(True)

    assert not tmpConf.setViewComments(False)
    assert not tmpConf.viewComments
    assert tmpConf.setViewComments(True)

    assert not tmpConf.setViewSynopsis(False)
    assert not tmpConf.viewSynopsis
    assert tmpConf.setViewSynopsis(True)

    # Check Final File
    # ================

    assert tmpConf.confChanged
    assert tmpConf.saveConfig()
    assert not tmpConf.confChanged

    copyfile(confFile, testFile)
    assert cmpFiles(testFile, compFile, [2, 9, 10])

# END Test testBaseConfig_SettersGetters

@pytest.mark.base
def testBaseConfig_Internal(monkeypatch, tmpConf):
    """Check internal functions.
    """
    # Function _packList
    assert tmpConf._packList(["A", 1, 2.0, None, False]) == "A, 1, 2.0, None, False"

    # Function _unpackList
    assert tmpConf._unpackList("1, 2, 3", [0, 0, 0], tmpConf.CNF_I_LST) == [1, 2, 3]
    assert tmpConf._unpackList("1, 2   ", [0, 0, 0], tmpConf.CNF_I_LST) == [1, 2, 0]
    assert tmpConf._unpackList("A, B, C", [0, 0, 0], tmpConf.CNF_I_LST) == [0, 0, 0]
    assert tmpConf._unpackList("1, 2, 3", ["X", "Y", "Z"], tmpConf.CNF_S_LST) == ["1", "2", "3"]
    assert tmpConf._unpackList("A, B   ", ["X", "Y", "Z"], tmpConf.CNF_S_LST) == ["A", "B", "Z"]
    assert tmpConf._unpackList("A, B, C", ["X", "Y", "Z"], tmpConf.CNF_S_LST) == ["A", "B", "C"]
    assert tmpConf._unpackList("A, B, C", ["X", "Y", "Z"], tmpConf.CNF_STR) == ["X", "Y", "Z"]

    # Function _parseLine
    cnfParse = configparser.ConfigParser()
    cnfParse.read_string(
        "[Main]\n"
        "val_string = dummy\n"
        "val_int = 123\n"
        "val_bool = True\n"
        "val_list_string = A, B, C\n"
        "val_list_int = 1, 2, 3\n"
    )

    assert tmpConf._parseLine(
        cnfParse, "Main", "val_string", tmpConf.CNF_STR, "default"
    ) == "dummy"
    assert tmpConf._parseLine(
        cnfParse, "Main", "nope", tmpConf.CNF_STR, "default"
    ) == "default"

    assert tmpConf._parseLine(
        cnfParse, "Main", "val_int", tmpConf.CNF_INT, "0"
    ) == 123
    assert tmpConf._parseLine(
        cnfParse, "Main", "nope", tmpConf.CNF_INT, 0
    ) == 0
    assert tmpConf._parseLine(
        cnfParse, "Main", "val_string", tmpConf.CNF_INT, 0
    ) == 0

    assert tmpConf._parseLine(
        cnfParse, "Main", "val_bool", tmpConf.CNF_BOOL, False
    ) is True
    assert tmpConf._parseLine(
        cnfParse, "Main", "nope", tmpConf.CNF_BOOL, False
    ) is False
    assert tmpConf._parseLine(
        cnfParse, "Main", "val_string", tmpConf.CNF_BOOL, False
    ) is False

    assert tmpConf._parseLine(
        cnfParse, "Main", "val_list_string", tmpConf.CNF_S_LST, ["W", "X", "Y", "Z"]
    ) == ["A", "B", "C", "Z"]
    assert tmpConf._parseLine(
        cnfParse, "Main", "nope", tmpConf.CNF_S_LST, ["W", "X", "Y", "Z"]
    ) == ["W", "X", "Y", "Z"]

    assert tmpConf._parseLine(
        cnfParse, "Main", "val_list_int", tmpConf.CNF_I_LST, [6, 7, 8, 9]
    ) == [1, 2, 3, 9]
    assert tmpConf._parseLine(
        cnfParse, "Main", "nope", tmpConf.CNF_S_LST, [6, 7, 8, 9]
    ) == [6, 7, 8, 9]

    # Function _checkNone
    assert tmpConf._checkNone(None) is None
    assert tmpConf._checkNone("None") is None
    assert tmpConf._checkNone("stuff") == "stuff"

    # Function _checkOptionalPackages
    # (Assumes enchant package exists ans is importable)
    tmpConf._checkOptionalPackages()
    assert tmpConf.hasEnchant is True

    monkeypatch.setitem(sys.modules, "enchant", None)
    tmpConf._checkOptionalPackages()
    assert tmpConf.hasEnchant is False
    monkeypatch.undo()

    monkeypatch.setattr("shutil.which", lambda *args: "dummy")
    tmpConf._checkOptionalPackages()
    assert tmpConf.hasAssistant is True
    monkeypatch.undo()

    monkeypatch.setattr("shutil.which", lambda *args: None)
    tmpConf._checkOptionalPackages()
    assert tmpConf.hasAssistant is False
    monkeypatch.undo()

# END Test testBaseConfig_Internal
