# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsProject.

.. note:: 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 2 of the License, or
(at your option) any later version.
"""
from builtins import chr
from builtins import range
__author__ = 'Sebastian Dietrich'
__date__ = '19/11/2015'
__copyright__ = 'Copyright 2015, The QGIS Project'

import os
import re
from osgeo import ogr
import codecs
from io import BytesIO
from zipfile import ZipFile
from tempfile import TemporaryDirectory

import qgis  # NOQA

from qgis.core import (QgsProject,
                       QgsCoordinateTransformContext,
                       QgsProjectDirtyBlocker,
                       QgsApplication,
                       QgsUnitTypes,
                       QgsCoordinateReferenceSystem,
                       QgsLabelingEngineSettings,
                       QgsVectorLayer,
                       QgsRasterLayer,
                       QgsMapLayer,
                       QgsExpressionContextUtils,
                       QgsProjectColorScheme,
                       QgsSettings)
from qgis.gui import (QgsLayerTreeMapCanvasBridge,
                      QgsMapCanvas)

from qgis.PyQt.QtTest import QSignalSpy
from qgis.PyQt.QtCore import QT_VERSION_STR, QTemporaryDir, QTemporaryFile
from qgis.PyQt.QtGui import QColor
from qgis.PyQt import sip

from qgis.testing import start_app, unittest
from utilities import (unitTestDataPath)
from shutil import copyfile

app = start_app()
TEST_DATA_DIR = unitTestDataPath()


def createLayer(name):
    return QgsVectorLayer("Point?field=x:string", name, "memory")


class TestQgsProject(unittest.TestCase):

    def __init__(self, methodName):
        """Run once on class initialization."""
        unittest.TestCase.__init__(self, methodName)
        self.messageCaught = False

    def test_makeKeyTokens_(self):
        # see http://www.w3.org/TR/REC-xml/#d0e804 for a list of valid characters

        invalidTokens = []
        validTokens = []

        # all test tokens will be generated by prepending or inserting characters to this token
        validBase = "valid"

        # some invalid characters, not allowed anywhere in a token
        # note that '/' must not be added here because it is taken as a separator by makeKeyTokens_()
        invalidChars = "+*,;<>|!$%()=?#\x01"

        # generate the characters that are allowed at the start of a token (and at every other position)
        validStartChars = ":_"
        charRanges = [
            (ord('a'), ord('z')),
            (ord('A'), ord('Z')),
            (0x00F8, 0x02FF),
            (0x0370, 0x037D),
            (0x037F, 0x1FFF),
            (0x200C, 0x200D),
            (0x2070, 0x218F),
            (0x2C00, 0x2FEF),
            (0x3001, 0xD7FF),
            (0xF900, 0xFDCF),
            (0xFDF0, 0xFFFD),
            # (0x10000, 0xEFFFF),   while actually valid, these are not yet accepted by makeKeyTokens_()
        ]
        for r in charRanges:
            for c in range(r[0], r[1]):
                validStartChars += chr(c)

        # generate the characters that are only allowed inside a token, not at the start
        validInlineChars = "-.\xB7"
        charRanges = [
            (ord('0'), ord('9')),
            (0x0300, 0x036F),
            (0x203F, 0x2040),
        ]
        for r in charRanges:
            for c in range(r[0], r[1]):
                validInlineChars += chr(c)

        # test forbidden start characters
        for c in invalidChars + validInlineChars:
            invalidTokens.append(c + validBase)

        # test forbidden inline characters
        for c in invalidChars:
            invalidTokens.append(validBase[:4] + c + validBase[4:])

        # test each allowed start character
        for c in validStartChars:
            validTokens.append(c + validBase)

        # test each allowed inline character
        for c in validInlineChars:
            validTokens.append(validBase[:4] + c + validBase[4:])

        logger = QgsApplication.messageLog()
        logger.messageReceived.connect(self.catchMessage)
        prj = QgsProject.instance()

        for token in validTokens:
            self.messageCaught = False
            prj.readEntry("test", token)
            myMessage = "valid token '%s' not accepted" % (token)
            assert not self.messageCaught, myMessage

        for token in invalidTokens:
            self.messageCaught = False
            prj.readEntry("test", token)
            myMessage = "invalid token '%s' accepted" % (token)
            assert self.messageCaught, myMessage

        logger.messageReceived.disconnect(self.catchMessage)

    def catchMessage(self):
        self.messageCaught = True

    def testClear(self):
        prj = QgsProject.instance()
        prj.setTitle('xxx')
        spy = QSignalSpy(prj.cleared)
        prj.clear()
        self.assertEqual(len(spy), 1)
        self.assertFalse(prj.title())

    def testCrs(self):
        prj = QgsProject.instance()
        prj.clear()

        self.assertFalse(prj.crs().isValid())
        prj.setCrs(QgsCoordinateReferenceSystem.fromOgcWmsCrs('EPSG:3111'))
        self.assertEqual(prj.crs().authid(), 'EPSG:3111')

    def testEllipsoid(self):
        prj = QgsProject.instance()
        prj.clear()

        prj.setCrs(QgsCoordinateReferenceSystem.fromOgcWmsCrs('EPSG:3111'))
        prj.setEllipsoid('WGS84')
        self.assertEqual(prj.ellipsoid(), 'WGS84')

        # if project has NO crs, then ellipsoid should always be none
        prj.setCrs(QgsCoordinateReferenceSystem())
        self.assertEqual(prj.ellipsoid(), 'NONE')

    def testDistanceUnits(self):
        prj = QgsProject.instance()
        prj.clear()

        prj.setDistanceUnits(QgsUnitTypes.DistanceFeet)
        self.assertEqual(prj.distanceUnits(), QgsUnitTypes.DistanceFeet)

    def testAreaUnits(self):
        prj = QgsProject.instance()
        prj.clear()

        prj.setAreaUnits(QgsUnitTypes.AreaSquareFeet)
        self.assertEqual(prj.areaUnits(), QgsUnitTypes.AreaSquareFeet)

    def testReadEntry(self):
        prj = QgsProject.instance()
        prj.read(os.path.join(TEST_DATA_DIR, 'labeling/test-labeling.qgs'))

        # add a test entry list
        prj.writeEntry("TestScope", "/TestListProperty", ["Entry1", "Entry2"])

        # valid key, valid value
        self.assertEqual(prj.readNumEntry("SpatialRefSys", "/ProjectionsEnabled", -1), (0, True))
        self.assertEqual(prj.readEntry("SpatialRefSys", "/ProjectCrs"), ("EPSG:32613", True))
        self.assertEqual(prj.readBoolEntry("PAL", "/ShowingCandidates"), (False, True))
        self.assertEqual(prj.readNumEntry("PAL", "/CandidatesPolygon"), (8., True))
        self.assertEqual(prj.readListEntry("TestScope", "/TestListProperty"), (["Entry1", "Entry2"], True))

        # invalid key
        self.assertEqual(prj.readNumEntry("SpatialRefSys", "/InvalidKey", -1), (-1, False))
        self.assertEqual(prj.readEntry("SpatialRefSys", "/InvalidKey", "wrong"), ("wrong", False))
        self.assertEqual(prj.readBoolEntry("PAL", "/InvalidKey", True), (True, False))
        self.assertEqual(prj.readDoubleEntry("PAL", "/InvalidKey", 42.), (42., False))
        self.assertEqual(prj.readListEntry("TestScope", "/InvalidKey", ["Default1", "Default2"]), (["Default1", "Default2"], False))

    def testEmbeddedGroup(self):
        testdata_path = unitTestDataPath('embedded_groups') + '/'

        prj_path = os.path.join(testdata_path, "project2.qgs")
        prj = QgsProject()
        prj.read(prj_path)

        layer_tree_group = prj.layerTreeRoot()
        self.assertEqual(len(layer_tree_group.findLayerIds()), 2)
        for layer_id in layer_tree_group.findLayerIds():
            name = prj.mapLayer(layer_id).name()
            self.assertTrue(name in ['polys', 'lines'])
            if name == 'polys':
                self.assertTrue(layer_tree_group.findLayer(layer_id).itemVisibilityChecked())
            elif name == 'lines':
                self.assertFalse(layer_tree_group.findLayer(layer_id).itemVisibilityChecked())

    def testInstance(self):
        """ test retrieving global instance """
        self.assertTrue(QgsProject.instance())

        # register a layer to the singleton
        QgsProject.instance().addMapLayer(createLayer('test'))

        # check that the same instance is returned
        self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1)

        QgsProject.instance().removeAllMapLayers()

    def test_addMapLayer(self):
        """ test adding individual map layers to registry """
        QgsProject.instance().removeAllMapLayers()

        l1 = createLayer('test')
        self.assertEqual(QgsProject.instance().addMapLayer(l1), l1)
        self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1)
        self.assertEqual(QgsProject.instance().count(), 1)

        # adding a second layer should leave existing layers intact
        l2 = createLayer('test2')
        self.assertEqual(QgsProject.instance().addMapLayer(l2), l2)
        self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1)
        self.assertEqual(len(QgsProject.instance().mapLayersByName('test2')), 1)
        self.assertEqual(QgsProject.instance().count(), 2)

        QgsProject.instance().removeAllMapLayers()

    def test_addMapLayerAlreadyAdded(self):
        """ test that already added layers can't be readded to registry """
        QgsProject.instance().removeAllMapLayers()

        l1 = createLayer('test')
        QgsProject.instance().addMapLayer(l1)
        self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1)
        self.assertEqual(QgsProject.instance().count(), 1)
        self.assertEqual(QgsProject.instance().addMapLayer(l1), None)
        self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1)
        self.assertEqual(QgsProject.instance().count(), 1)

        QgsProject.instance().removeAllMapLayers()

    def test_addMapLayerInvalid(self):
        """ test that invalid map layers can be added to registry """
        QgsProject.instance().removeAllMapLayers()

        vl = QgsVectorLayer("Point?field=x:string", 'test', "xxx")
        self.assertEqual(QgsProject.instance().addMapLayer(vl), vl)
        self.assertFalse(vl in QgsProject.instance().mapLayers(True).values())
        self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1)
        self.assertEqual(QgsProject.instance().count(), 1)
        self.assertEqual(QgsProject.instance().validCount(), 0)

        self.assertEqual(len(QgsProject.instance().mapLayers(True)), 0)

        QgsProject.instance().removeAllMapLayers()

    def test_addMapLayerSignals(self):
        """ test that signals are correctly emitted when adding map layer"""

        QgsProject.instance().removeAllMapLayers()

        layer_was_added_spy = QSignalSpy(QgsProject.instance().layerWasAdded)
        layers_added_spy = QSignalSpy(QgsProject.instance().layersAdded)
        legend_layers_added_spy = QSignalSpy(QgsProject.instance().legendLayersAdded)

        l1 = createLayer('test')
        QgsProject.instance().addMapLayer(l1)

        # can't seem to actually test the data which was emitted, so best we can do is test
        # the signal count
        self.assertEqual(len(layer_was_added_spy), 1)
        self.assertEqual(len(layers_added_spy), 1)
        self.assertEqual(len(legend_layers_added_spy), 1)

        # layer not added to legend
        QgsProject.instance().addMapLayer(createLayer('test2'), False)
        self.assertEqual(len(layer_was_added_spy), 2)
        self.assertEqual(len(layers_added_spy), 2)
        self.assertEqual(len(legend_layers_added_spy), 1)

        # try readding a layer already in the registry
        QgsProject.instance().addMapLayer(l1)
        # should be no extra signals emitted
        self.assertEqual(len(layer_was_added_spy), 2)
        self.assertEqual(len(layers_added_spy), 2)
        self.assertEqual(len(legend_layers_added_spy), 1)

    def test_addMapLayers(self):
        """ test adding multiple map layers to registry """
        QgsProject.instance().removeAllMapLayers()

        l1 = createLayer('test')
        l2 = createLayer('test2')
        self.assertEqual(set(QgsProject.instance().addMapLayers([l1, l2])), set([l1, l2]))
        self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1)
        self.assertEqual(len(QgsProject.instance().mapLayersByName('test2')), 1)
        self.assertEqual(QgsProject.instance().count(), 2)

        # adding more layers should leave existing layers intact
        l3 = createLayer('test3')
        l4 = createLayer('test4')
        self.assertEqual(set(QgsProject.instance().addMapLayers([l3, l4])), set([l3, l4]))
        self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1)
        self.assertEqual(len(QgsProject.instance().mapLayersByName('test2')), 1)
        self.assertEqual(len(QgsProject.instance().mapLayersByName('test3')), 1)
        self.assertEqual(len(QgsProject.instance().mapLayersByName('test4')), 1)
        self.assertEqual(QgsProject.instance().count(), 4)

        QgsProject.instance().removeAllMapLayers()

    def test_addMapLayersInvalid(self):
        """ test that invalid map layers can be added to registry """
        QgsProject.instance().removeAllMapLayers()

        vl = QgsVectorLayer("Point?field=x:string", 'test', "xxx")
        self.assertEqual(QgsProject.instance().addMapLayers([vl]), [vl])
        self.assertFalse(vl in QgsProject.instance().mapLayers(True).values())
        self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1)
        self.assertEqual(QgsProject.instance().count(), 1)
        self.assertEqual(QgsProject.instance().validCount(), 0)

        QgsProject.instance().removeAllMapLayers()

    def test_addMapLayersAlreadyAdded(self):
        """ test that already added layers can't be readded to registry """
        QgsProject.instance().removeAllMapLayers()

        l1 = createLayer('test')
        self.assertEqual(QgsProject.instance().addMapLayers([l1]), [l1])
        self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1)
        self.assertEqual(QgsProject.instance().count(), 1)
        self.assertEqual(QgsProject.instance().addMapLayers([l1]), [])
        self.assertEqual(len(QgsProject.instance().mapLayersByName('test')), 1)
        self.assertEqual(QgsProject.instance().count(), 1)

        QgsProject.instance().removeAllMapLayers()

    def test_addMapLayersSignals(self):
        """ test that signals are correctly emitted when adding map layers"""
        QgsProject.instance().removeAllMapLayers()

        layer_was_added_spy = QSignalSpy(QgsProject.instance().layerWasAdded)
        layers_added_spy = QSignalSpy(QgsProject.instance().layersAdded)
        legend_layers_added_spy = QSignalSpy(QgsProject.instance().legendLayersAdded)

        l1 = createLayer('test')
        l2 = createLayer('test2')
        QgsProject.instance().addMapLayers([l1, l2])

        # can't seem to actually test the data which was emitted, so best we can do is test
        # the signal count
        self.assertEqual(len(layer_was_added_spy), 2)
        self.assertEqual(len(layers_added_spy), 1)
        self.assertEqual(len(legend_layers_added_spy), 1)

        # layer not added to legend
        QgsProject.instance().addMapLayers([createLayer('test3'), createLayer('test4')], False)
        self.assertEqual(len(layer_was_added_spy), 4)
        self.assertEqual(len(layers_added_spy), 2)
        self.assertEqual(len(legend_layers_added_spy), 1)

        # try readding a layer already in the registry
        QgsProject.instance().addMapLayers([l1, l2])
        # should be no extra signals emitted
        self.assertEqual(len(layer_was_added_spy), 4)
        self.assertEqual(len(layers_added_spy), 2)
        self.assertEqual(len(legend_layers_added_spy), 1)

    def test_mapLayerById(self):
        """ test retrieving map layer by ID """
        QgsProject.instance().removeAllMapLayers()

        # test no crash with empty registry
        self.assertEqual(QgsProject.instance().mapLayer('bad'), None)
        self.assertEqual(QgsProject.instance().mapLayer(None), None)

        l1 = createLayer('test')
        l2 = createLayer('test2')

        QgsProject.instance().addMapLayers([l1, l2])

        self.assertEqual(QgsProject.instance().mapLayer('bad'), None)
        self.assertEqual(QgsProject.instance().mapLayer(None), None)
        self.assertEqual(QgsProject.instance().mapLayer(l1.id()), l1)
        self.assertEqual(QgsProject.instance().mapLayer(l2.id()), l2)

    def test_mapLayersByName(self):
        """ test retrieving map layer by name """
        p = QgsProject()

        # test no crash with empty registry
        self.assertEqual(p.mapLayersByName('bad'), [])
        self.assertEqual(p.mapLayersByName(None), [])

        l1 = createLayer('test')
        l2 = createLayer('test2')

        p.addMapLayers([l1, l2])

        self.assertEqual(p.mapLayersByName('bad'), [])
        self.assertEqual(p.mapLayersByName(None), [])
        self.assertEqual(p.mapLayersByName('test'), [l1])
        self.assertEqual(p.mapLayersByName('test2'), [l2])

        #duplicate name
        l3 = createLayer('test')
        p.addMapLayer(l3)
        self.assertEqual(set(p.mapLayersByName('test')), set([l1, l3]))

    def test_mapLayers(self):
        """ test retrieving map layers list """
        QgsProject.instance().removeAllMapLayers()

        # test no crash with empty registry
        self.assertEqual(QgsProject.instance().mapLayers(), {})

        l1 = createLayer('test')
        l2 = createLayer('test2')

        QgsProject.instance().addMapLayers([l1, l2])

        self.assertEqual(QgsProject.instance().mapLayers(), {l1.id(): l1, l2.id(): l2})

    def test_removeMapLayersById(self):
        """ test removing map layers by ID """
        QgsProject.instance().removeAllMapLayers()

        # test no crash with empty registry
        QgsProject.instance().removeMapLayers(['bad'])
        QgsProject.instance().removeMapLayers([None])

        l1 = createLayer('test')
        l2 = createLayer('test2')
        l3 = createLayer('test3')

        QgsProject.instance().addMapLayers([l1, l2, l3])
        self.assertEqual(QgsProject.instance().count(), 3)

        #remove bad layers
        QgsProject.instance().removeMapLayers(['bad'])
        self.assertEqual(QgsProject.instance().count(), 3)
        QgsProject.instance().removeMapLayers([None])
        self.assertEqual(QgsProject.instance().count(), 3)

        # remove valid layers
        l1_id = l1.id()
        QgsProject.instance().removeMapLayers([l1_id])
        self.assertEqual(QgsProject.instance().count(), 2)
        # double remove
        QgsProject.instance().removeMapLayers([l1_id])
        self.assertEqual(QgsProject.instance().count(), 2)

        # test that layer has been deleted
        self.assertTrue(sip.isdeleted(l1))

        # remove multiple
        QgsProject.instance().removeMapLayers([l2.id(), l3.id()])
        self.assertEqual(QgsProject.instance().count(), 0)
        self.assertTrue(sip.isdeleted(l2))

        # try removing a layer not in the registry
        l4 = createLayer('test4')
        QgsProject.instance().removeMapLayers([l4.id()])
        self.assertFalse(sip.isdeleted(l4))

    # fails on qt5 due to removeMapLayers list type conversion - needs a PyName alias
    # added to removeMapLayers for QGIS 3.0
    @unittest.expectedFailure(QT_VERSION_STR[0] == '5')
    def test_removeMapLayersByLayer(self):
        """ test removing map layers by layer"""
        QgsProject.instance().removeAllMapLayers()

        # test no crash with empty registry
        QgsProject.instance().removeMapLayers([None])

        l1 = createLayer('test')
        l2 = createLayer('test2')
        l3 = createLayer('test3')

        QgsProject.instance().addMapLayers([l1, l2, l3])
        self.assertEqual(QgsProject.instance().count(), 3)

        #remove bad layers
        QgsProject.instance().removeMapLayers([None])
        self.assertEqual(QgsProject.instance().count(), 3)

        # remove valid layers
        QgsProject.instance().removeMapLayers([l1])
        self.assertEqual(QgsProject.instance().count(), 2)

        # test that layer has been deleted
        self.assertTrue(sip.isdeleted(l1))

        # remove multiple
        QgsProject.instance().removeMapLayers([l2, l3])
        self.assertEqual(QgsProject.instance().count(), 0)
        self.assertTrue(sip.isdeleted(l2))
        self.assertTrue(sip.isdeleted(l3))

    def test_removeMapLayerById(self):
        """ test removing a map layer by ID """
        QgsProject.instance().removeAllMapLayers()

        # test no crash with empty registry
        QgsProject.instance().removeMapLayer('bad')
        QgsProject.instance().removeMapLayer(None)

        l1 = createLayer('test')
        l2 = createLayer('test2')

        QgsProject.instance().addMapLayers([l1, l2])
        self.assertEqual(QgsProject.instance().count(), 2)

        #remove bad layers
        QgsProject.instance().removeMapLayer('bad')
        self.assertEqual(QgsProject.instance().count(), 2)
        QgsProject.instance().removeMapLayer(None)
        self.assertEqual(QgsProject.instance().count(), 2)

        # remove valid layers
        l1_id = l1.id()
        QgsProject.instance().removeMapLayer(l1_id)
        self.assertEqual(QgsProject.instance().count(), 1)
        # double remove
        QgsProject.instance().removeMapLayer(l1_id)
        self.assertEqual(QgsProject.instance().count(), 1)

        # test that layer has been deleted
        self.assertTrue(sip.isdeleted(l1))

        # remove second layer
        QgsProject.instance().removeMapLayer(l2.id())
        self.assertEqual(QgsProject.instance().count(), 0)
        self.assertTrue(sip.isdeleted(l2))

        # try removing a layer not in the registry
        l3 = createLayer('test3')
        QgsProject.instance().removeMapLayer(l3.id())
        self.assertFalse(sip.isdeleted(l3))

    def test_removeMapLayerByLayer(self):
        """ test removing a map layer by layer """
        QgsProject.instance().removeAllMapLayers()

        # test no crash with empty registry
        QgsProject.instance().removeMapLayer('bad')
        QgsProject.instance().removeMapLayer(None)

        l1 = createLayer('test')
        l2 = createLayer('test2')

        QgsProject.instance().addMapLayers([l1, l2])
        self.assertEqual(QgsProject.instance().count(), 2)

        #remove bad layers
        QgsProject.instance().removeMapLayer(None)
        self.assertEqual(QgsProject.instance().count(), 2)
        l3 = createLayer('test3')
        QgsProject.instance().removeMapLayer(l3)
        self.assertEqual(QgsProject.instance().count(), 2)

        # remove valid layers
        QgsProject.instance().removeMapLayer(l1)
        self.assertEqual(QgsProject.instance().count(), 1)

        # test that layer has been deleted
        self.assertTrue(sip.isdeleted(l1))

        # remove second layer
        QgsProject.instance().removeMapLayer(l2)
        self.assertEqual(QgsProject.instance().count(), 0)
        self.assertTrue(sip.isdeleted(l2))

        # try removing a layer not in the registry
        l3 = createLayer('test3')
        QgsProject.instance().removeMapLayer(l3)
        self.assertFalse(sip.isdeleted(l3))

    def test_removeAllMapLayers(self):
        """ test removing all map layers from registry """
        QgsProject.instance().removeAllMapLayers()
        l1 = createLayer('test')
        l2 = createLayer('test2')

        QgsProject.instance().addMapLayers([l1, l2])
        self.assertEqual(QgsProject.instance().count(), 2)
        QgsProject.instance().removeAllMapLayers()
        self.assertEqual(QgsProject.instance().count(), 0)
        self.assertEqual(QgsProject.instance().mapLayersByName('test'), [])
        self.assertEqual(QgsProject.instance().mapLayersByName('test2'), [])

    def test_addRemoveLayersSignals(self):
        """ test that signals are correctly emitted when removing map layers"""
        QgsProject.instance().removeAllMapLayers()

        layers_will_be_removed_spy = QSignalSpy(QgsProject.instance().layersWillBeRemoved)
        layer_will_be_removed_spy_str = QSignalSpy(QgsProject.instance().layerWillBeRemoved[str])
        layer_will_be_removed_spy_layer = QSignalSpy(QgsProject.instance().layerWillBeRemoved[QgsMapLayer])
        layers_removed_spy = QSignalSpy(QgsProject.instance().layersRemoved)
        layer_removed_spy = QSignalSpy(QgsProject.instance().layerRemoved)
        remove_all_spy = QSignalSpy(QgsProject.instance().removeAll)

        l1 = createLayer('l1')
        l2 = createLayer('l2')
        l3 = createLayer('l3')
        l4 = createLayer('l4')
        QgsProject.instance().addMapLayers([l1, l2, l3, l4])

        # remove 1 layer
        QgsProject.instance().removeMapLayer(l1)
        # can't seem to actually test the data which was emitted, so best we can do is test
        # the signal count
        self.assertEqual(len(layers_will_be_removed_spy), 1)
        self.assertEqual(len(layer_will_be_removed_spy_str), 1)
        self.assertEqual(len(layer_will_be_removed_spy_layer), 1)
        self.assertEqual(len(layers_removed_spy), 1)
        self.assertEqual(len(layer_removed_spy), 1)
        self.assertEqual(len(remove_all_spy), 0)
        self.assertEqual(QgsProject.instance().count(), 3)

        # remove 2 layers at once
        QgsProject.instance().removeMapLayers([l2.id(), l3.id()])
        self.assertEqual(len(layers_will_be_removed_spy), 2)
        self.assertEqual(len(layer_will_be_removed_spy_str), 3)
        self.assertEqual(len(layer_will_be_removed_spy_layer), 3)
        self.assertEqual(len(layers_removed_spy), 2)
        self.assertEqual(len(layer_removed_spy), 3)
        self.assertEqual(len(remove_all_spy), 0)
        self.assertEqual(QgsProject.instance().count(), 1)

        # remove all
        QgsProject.instance().removeAllMapLayers()
        self.assertEqual(len(layers_will_be_removed_spy), 3)
        self.assertEqual(len(layer_will_be_removed_spy_str), 4)
        self.assertEqual(len(layer_will_be_removed_spy_layer), 4)
        self.assertEqual(len(layers_removed_spy), 3)
        self.assertEqual(len(layer_removed_spy), 4)
        self.assertEqual(len(remove_all_spy), 1)

        #remove some layers which aren't in the registry
        QgsProject.instance().removeMapLayers(['asdasd'])
        self.assertEqual(len(layers_will_be_removed_spy), 3)
        self.assertEqual(len(layer_will_be_removed_spy_str), 4)
        self.assertEqual(len(layer_will_be_removed_spy_layer), 4)
        self.assertEqual(len(layers_removed_spy), 3)
        self.assertEqual(len(layer_removed_spy), 4)
        self.assertEqual(len(remove_all_spy), 1)

        l5 = createLayer('test5')
        QgsProject.instance().removeMapLayer(l5)
        self.assertEqual(len(layers_will_be_removed_spy), 3)
        self.assertEqual(len(layer_will_be_removed_spy_str), 4)
        self.assertEqual(len(layer_will_be_removed_spy_layer), 4)
        self.assertEqual(len(layers_removed_spy), 3)
        self.assertEqual(len(layer_removed_spy), 4)
        self.assertEqual(len(remove_all_spy), 1)

    def test_RemoveLayerShouldNotSegFault(self):
        QgsProject.instance().removeAllMapLayers()

        reg = QgsProject.instance()
        # Should not segfault
        reg.removeMapLayers(['not_exists'])
        reg.removeMapLayer('not_exists2')

        # check also that the removal of an unexistent layer does not insert a null layer
        for k, layer in list(reg.mapLayers().items()):
            assert(layer is not None)

    def testTakeLayer(self):
        # test taking ownership of a layer from the project
        l1 = createLayer('l1')
        l2 = createLayer('l2')
        p = QgsProject()

        # add one layer to project
        p.addMapLayer(l1)
        self.assertEqual(p.mapLayers(), {l1.id(): l1})
        self.assertEqual(l1.parent().parent(), p)

        # try taking some layers which don't exist in project
        self.assertFalse(p.takeMapLayer(None))
        self.assertFalse(p.takeMapLayer(l2))
        # but l2 should still exist..
        self.assertTrue(l2.isValid())

        # take layer from project
        self.assertEqual(p.takeMapLayer(l1), l1)
        self.assertFalse(p.mapLayers()) # no layers left
        # but l1 should still exist
        self.assertTrue(l1.isValid())
        # layer should have no parent now
        self.assertFalse(l1.parent())

        # destroy project
        p = None
        self.assertTrue(l1.isValid())

    def test_transactionsGroup(self):
        # Undefined transaction group (wrong provider key).
        QgsProject.instance().setAutoTransaction(True)
        noTg = QgsProject.instance().transactionGroup("provider-key", "database-connection-string")
        self.assertIsNone(noTg)

    def test_zip_new_project(self):
        tmpDir = QTemporaryDir()
        tmpFile = "{}/project.qgz".format(tmpDir.path())

        # zip with existing file
        open(tmpFile, 'a').close()

        project = QgsProject()
        self.assertTrue(project.write(tmpFile))

        # zip with non existing file
        os.remove(tmpFile)

        project = QgsProject()
        self.assertTrue(project.write(tmpFile))
        self.assertTrue(os.path.isfile(tmpFile))

    def test_zip_invalid_path(self):
        project = QgsProject()
        self.assertFalse(project.write())
        self.assertFalse(project.write(""))
        self.assertFalse(project.write("/fake/test.zip"))

    def test_zip_filename(self):
        tmpDir = QTemporaryDir()
        tmpFile = "{}/project.qgz".format(tmpDir.path())

        project = QgsProject()
        self.assertFalse(project.write())

        project.setFileName(tmpFile)
        self.assertTrue(project.write())
        self.assertTrue(os.path.isfile(tmpFile))

    def test_unzip_invalid_path(self):
        project = QgsProject()
        self.assertFalse(project.read())
        self.assertFalse(project.read(""))
        self.assertFalse(project.read("/fake/test.zip"))

    def test_zip_unzip(self):
        tmpDir = QTemporaryDir()
        tmpFile = "{}/project.qgz".format(tmpDir.path())

        project = QgsProject()

        l0 = QgsVectorLayer(os.path.join(TEST_DATA_DIR, "points.shp"), "points", "ogr")
        l1 = QgsVectorLayer(os.path.join(TEST_DATA_DIR, "lines.shp"), "lines", "ogr")
        project.addMapLayers([l0, l1])

        self.assertTrue(project.write(tmpFile))

        project2 = QgsProject()
        self.assertFalse(project2.isZipped())
        self.assertTrue(project2.fileName() == "")
        self.assertTrue(project2.read(tmpFile))
        self.assertTrue(project2.isZipped())
        self.assertTrue(project2.fileName() == tmpFile)
        layers = project2.mapLayers()

        self.assertEqual(len(layers.keys()), 2)
        self.assertTrue(layers[l0.id()].isValid(), True)
        self.assertTrue(layers[l1.id()].isValid(), True)

        project2.clear()
        self.assertFalse(project2.isZipped())

    def testUpgradeOtfFrom2x(self):
        """
        Test that upgrading a 2.x project correctly brings across project CRS and OTF transformation settings
        """
        prj = QgsProject.instance()
        prj.read(os.path.join(TEST_DATA_DIR, 'projects', 'test_memory_layer_proj.qgs'))
        self.assertTrue(prj.crs().isValid())
        self.assertEqual(prj.crs().authid(), 'EPSG:2056')

    def testRelativePaths(self):
        """
        Test whether paths to layer sources are stored as relative to the project path
        """
        tmpDir = QTemporaryDir()
        tmpFile = "{}/project.qgs".format(tmpDir.path())
        copyfile(os.path.join(TEST_DATA_DIR, "points.shp"), os.path.join(tmpDir.path(), "points.shp"))
        copyfile(os.path.join(TEST_DATA_DIR, "points.dbf"), os.path.join(tmpDir.path(), "points.dbf"))
        copyfile(os.path.join(TEST_DATA_DIR, "points.shx"), os.path.join(tmpDir.path(), "points.shx"))
        copyfile(os.path.join(TEST_DATA_DIR, "lines.shp"), os.path.join(tmpDir.path(), "lines.shp"))
        copyfile(os.path.join(TEST_DATA_DIR, "lines.dbf"), os.path.join(tmpDir.path(), "lines.dbf"))
        copyfile(os.path.join(TEST_DATA_DIR, "lines.shx"), os.path.join(tmpDir.path(), "lines.shx"))
        copyfile(os.path.join(TEST_DATA_DIR, "landsat_4326.tif"), os.path.join(tmpDir.path(), "landsat_4326.tif"))

        project = QgsProject()

        l0 = QgsVectorLayer(os.path.join(tmpDir.path(), "points.shp"), "points", "ogr")
        l1 = QgsVectorLayer(os.path.join(tmpDir.path(), "lines.shp"), "lines", "ogr")
        l2 = QgsRasterLayer(os.path.join(tmpDir.path(), "landsat_4326.tif"), "landsat", "gdal")
        self.assertTrue(l0.isValid())
        self.assertTrue(l1.isValid())
        self.assertTrue(l2.isValid())
        self.assertTrue(project.addMapLayers([l0, l1, l2]))
        self.assertTrue(project.write(tmpFile))
        del project

        with open(tmpFile, 'r') as f:
            content = ''.join(f.readlines())
            self.assertTrue('source="./lines.shp"' in content)
            self.assertTrue('source="./points.shp"' in content)
            self.assertTrue('source="./landsat_4326.tif"' in content)

        # Re-read the project and store absolute
        project = QgsProject()
        self.assertTrue(project.read(tmpFile))
        store = project.layerStore()
        self.assertEqual(set([l.name() for l in store.mapLayers().values()]), set(['lines', 'landsat', 'points']))
        project.writeEntryBool('Paths', '/Absolute', True)
        tmpFile2 = "{}/project2.qgs".format(tmpDir.path())
        self.assertTrue(project.write(tmpFile2))

        with open(tmpFile2, 'r') as f:
            content = ''.join(f.readlines())
            self.assertTrue('source="{}/lines.shp"'.format(tmpDir.path()) in content)
            self.assertTrue('source="{}/points.shp"'.format(tmpDir.path()) in content)
            self.assertTrue('source="{}/landsat_4326.tif"'.format(tmpDir.path()) in content)

        del project

    def testRelativePathsGpkg(self):
        """
        Test whether paths to layer sources are stored as relative to the project path with GPKG storage
        """

        def _check_datasource(_path):
            # Verify datasource path stored in the project

            ds = ogr.GetDriverByName('GPKG').Open(_path)
            l = ds.GetLayer(1)
            self.assertEqual(l.GetName(), 'qgis_projects')
            self.assertEqual(l.GetFeatureCount(), 1)
            f = l.GetFeature(1)
            zip_content = BytesIO(codecs.decode(f.GetFieldAsBinary(2), 'hex'))
            z = ZipFile(zip_content)
            qgs = z.read(z.filelist[0])
            self.assertEqual(re.findall(b'<datasource>(.*)?</datasource>', qgs)[0], b'./relative_paths_gh30387.gpkg|layername=some_data')

        with TemporaryDirectory() as d:
            path = os.path.join(d, 'relative_paths_gh30387.gpkg')
            copyfile(os.path.join(TEST_DATA_DIR, 'projects', 'relative_paths_gh30387.gpkg'), path)
            project = QgsProject()
            l = QgsVectorLayer(path + '|layername=some_data', 'mylayer', 'ogr')
            self.assertTrue(l.isValid())
            self.assertTrue(project.addMapLayers([l]))
            self.assertEqual(project.count(), 1)
            # Project URI
            uri = 'geopackage://{}?projectName=relative_project'.format(path)
            project.setFileName(uri)
            self.assertTrue(project.write())
            # Verify
            project = QgsProject()
            self.assertTrue(project.read(uri))
            self.assertEqual(project.writePath(path), './relative_paths_gh30387.gpkg')

            _check_datasource(path)

            for _, l in project.mapLayers().items():
                self.assertTrue(l.isValid())

            with TemporaryDirectory() as d2:
                # Move it!
                path2 = os.path.join(d2, 'relative_paths_gh30387.gpkg')
                copyfile(path, path2)
                # Delete old temporary dir
                del d
                # Verify moved
                project = QgsProject()
                uri2 = 'geopackage://{}?projectName=relative_project'.format(path2)
                self.assertTrue(project.read(uri2))

                _check_datasource(path2)

                self.assertEqual(project.count(), 1)
                for _, l in project.mapLayers().items():
                    self.assertTrue(l.isValid())

    def testSymbolicLinkInProjectPath(self):
        """
        Test whether paths to layer sources relative to the project are stored correctly
        when project'name contains a symbolic link.
        In other words, test if project's and layers' names are correctly resolved.
        """
        tmpDir = QTemporaryDir()
        tmpFile = "{}/project.qgs".format(tmpDir.path())
        copyfile(os.path.join(TEST_DATA_DIR, "points.shp"), os.path.join(tmpDir.path(), "points.shp"))
        copyfile(os.path.join(TEST_DATA_DIR, "points.dbf"), os.path.join(tmpDir.path(), "points.dbf"))
        copyfile(os.path.join(TEST_DATA_DIR, "points.shx"), os.path.join(tmpDir.path(), "points.shx"))
        copyfile(os.path.join(TEST_DATA_DIR, "lines.shp"), os.path.join(tmpDir.path(), "lines.shp"))
        copyfile(os.path.join(TEST_DATA_DIR, "lines.dbf"), os.path.join(tmpDir.path(), "lines.dbf"))
        copyfile(os.path.join(TEST_DATA_DIR, "lines.shx"), os.path.join(tmpDir.path(), "lines.shx"))
        copyfile(os.path.join(TEST_DATA_DIR, "landsat_4326.tif"), os.path.join(tmpDir.path(), "landsat_4326.tif"))

        project = QgsProject()

        l0 = QgsVectorLayer(os.path.join(tmpDir.path(), "points.shp"), "points", "ogr")
        l1 = QgsVectorLayer(os.path.join(tmpDir.path(), "lines.shp"), "lines", "ogr")
        l2 = QgsRasterLayer(os.path.join(tmpDir.path(), "landsat_4326.tif"), "landsat", "gdal")
        self.assertTrue(l0.isValid())
        self.assertTrue(l1.isValid())
        self.assertTrue(l2.isValid())
        self.assertTrue(project.addMapLayers([l0, l1, l2]))
        self.assertTrue(project.write(tmpFile))
        del project

        # Create symbolic link to previous project
        tmpDir2 = QTemporaryDir()
        symlinkDir = os.path.join(tmpDir2.path(), "dir")
        os.symlink(tmpDir.path(), symlinkDir)
        tmpFile = "{}/project.qgs".format(symlinkDir)

        # Open project from symmlink and force re-save.
        project = QgsProject()
        self.assertTrue(project.read(tmpFile))
        self.assertTrue(project.write(tmpFile))
        del project

        with open(tmpFile, 'r') as f:
            content = ''.join(f.readlines())
            self.assertTrue('source="./lines.shp"' in content)
            self.assertTrue('source="./points.shp"' in content)
            self.assertTrue('source="./landsat_4326.tif"' in content)

    def testHomePath(self):
        p = QgsProject()
        path_changed_spy = QSignalSpy(p.homePathChanged)
        self.assertFalse(p.homePath())
        self.assertFalse(p.presetHomePath())

        # simulate save file
        tmp_dir = QTemporaryDir()
        tmp_file = "{}/project.qgs".format(tmp_dir.path())
        with open(tmp_file, 'w') as f:
            pass
        p.setFileName(tmp_file)

        # home path should be file path
        self.assertEqual(p.homePath(), tmp_dir.path())
        self.assertFalse(p.presetHomePath())
        self.assertEqual(len(path_changed_spy), 1)

        # manually override home path
        p.setPresetHomePath('/tmp/my_path')
        self.assertEqual(p.homePath(), '/tmp/my_path')
        self.assertEqual(p.presetHomePath(), '/tmp/my_path')
        self.assertEqual(len(path_changed_spy), 2)
        # check project scope
        scope = QgsExpressionContextUtils.projectScope(p)
        self.assertEqual(scope.variable('project_home'), '/tmp/my_path')

        # no extra signal if path is unchanged
        p.setPresetHomePath('/tmp/my_path')
        self.assertEqual(p.homePath(), '/tmp/my_path')
        self.assertEqual(p.presetHomePath(), '/tmp/my_path')
        self.assertEqual(len(path_changed_spy), 2)

        # setting file name should not affect home path is manually set
        tmp_file_2 = "{}/project/project2.qgs".format(tmp_dir.path())
        os.mkdir(tmp_dir.path() + '/project')
        with open(tmp_file_2, 'w') as f:
            pass
        p.setFileName(tmp_file_2)
        self.assertEqual(p.homePath(), '/tmp/my_path')
        self.assertEqual(p.presetHomePath(), '/tmp/my_path')
        self.assertEqual(len(path_changed_spy), 2)

        scope = QgsExpressionContextUtils.projectScope(p)
        self.assertEqual(scope.variable('project_home'), '/tmp/my_path')

        # clear manual path
        p.setPresetHomePath('')
        self.assertEqual(p.homePath(), tmp_dir.path() + '/project')
        self.assertFalse(p.presetHomePath())
        self.assertEqual(len(path_changed_spy), 3)

        scope = QgsExpressionContextUtils.projectScope(p)
        self.assertEqual(scope.variable('project_home'), tmp_dir.path() + '/project')

        # relative path
        p.setPresetHomePath('../home')
        self.assertEqual(p.homePath(), tmp_dir.path() + '/home')
        self.assertEqual(p.presetHomePath(), '../home')
        self.assertEqual(len(path_changed_spy), 4)

        scope = QgsExpressionContextUtils.projectScope(p)
        self.assertEqual(scope.variable('project_home'), tmp_dir.path() + '/home')

        # relative path, no filename
        p.setFileName('')
        self.assertEqual(p.homePath(), '../home')
        self.assertEqual(p.presetHomePath(), '../home')

        scope = QgsExpressionContextUtils.projectScope(p)
        self.assertEqual(scope.variable('project_home'), '../home')

        p = QgsProject()
        path_changed_spy = QSignalSpy(p.homePathChanged)
        p.setFileName('/tmp/not/existing/here/path.qgz')
        self.assertFalse(p.presetHomePath())
        self.assertEqual(p.homePath(), '/tmp/not/existing/here')
        self.assertEqual(len(path_changed_spy), 1)

    def testDirtyBlocker(self):
        # first test manual QgsProjectDirtyBlocker construction
        p = QgsProject()

        dirty_spy = QSignalSpy(p.isDirtyChanged)
        # ^ will do *whatever* it takes to discover the enemy's secret plans!

        # simple checks
        p.setDirty(True)
        self.assertTrue(p.isDirty())
        self.assertEqual(len(dirty_spy), 1)
        self.assertEqual(dirty_spy[-1], [True])
        p.setDirty(True) # already dirty
        self.assertTrue(p.isDirty())
        self.assertEqual(len(dirty_spy), 1)
        p.setDirty(False)
        self.assertFalse(p.isDirty())
        self.assertEqual(len(dirty_spy), 2)
        self.assertEqual(dirty_spy[-1], [False])
        p.setDirty(True)
        self.assertTrue(p.isDirty())
        self.assertEqual(len(dirty_spy), 3)
        self.assertEqual(dirty_spy[-1], [True])

        # with a blocker
        blocker = QgsProjectDirtyBlocker(p)
        # blockers will allow cleaning projects
        p.setDirty(False)
        self.assertFalse(p.isDirty())
        self.assertEqual(len(dirty_spy), 4)
        self.assertEqual(dirty_spy[-1], [False])
        # but not dirtying!
        p.setDirty(True)
        self.assertFalse(p.isDirty())
        self.assertEqual(len(dirty_spy), 4)
        self.assertEqual(dirty_spy[-1], [False])
        # nested block
        blocker2 = QgsProjectDirtyBlocker(p)
        p.setDirty(True)
        self.assertFalse(p.isDirty())
        self.assertEqual(len(dirty_spy), 4)
        self.assertEqual(dirty_spy[-1], [False])
        del blocker2
        p.setDirty(True)
        self.assertFalse(p.isDirty())
        self.assertEqual(len(dirty_spy), 4)
        self.assertEqual(dirty_spy[-1], [False])
        del blocker
        p.setDirty(True)
        self.assertTrue(p.isDirty())
        self.assertEqual(len(dirty_spy), 5)
        self.assertEqual(dirty_spy[-1], [True])

        # using python context manager
        with QgsProject.blockDirtying(p):
            # cleaning allowed
            p.setDirty(False)
            self.assertFalse(p.isDirty())
            self.assertEqual(len(dirty_spy), 6)
            self.assertEqual(dirty_spy[-1], [False])
            # but not dirtying!
            p.setDirty(True)
            self.assertFalse(p.isDirty())
            self.assertEqual(len(dirty_spy), 6)
            self.assertEqual(dirty_spy[-1], [False])

        # unblocked
        p.setDirty(True)
        self.assertTrue(p.isDirty())
        self.assertEqual(len(dirty_spy), 7)
        self.assertEqual(dirty_spy[-1], [True])

    def testCustomLayerOrderFrom2xProject(self):
        prj = QgsProject.instance()
        prj.read(os.path.join(TEST_DATA_DIR, 'layer_rendering_order_issue_qgis3.qgs'))

        layer_x = prj.mapLayers()['x20180406151213536']
        layer_y = prj.mapLayers()['y20180406151217017']

        # check layer order
        tree = prj.layerTreeRoot()
        self.assertEqual(tree.children()[0].layer(), layer_x)
        self.assertEqual(tree.children()[1].layer(), layer_y)
        self.assertTrue(tree.hasCustomLayerOrder())
        self.assertEqual(tree.customLayerOrder(), [layer_y, layer_x])
        self.assertEqual(tree.layerOrder(), [layer_y, layer_x])

    def testCustomLayerOrderFrom3xProject(self):
        prj = QgsProject.instance()
        prj.read(os.path.join(TEST_DATA_DIR, 'layer_rendering_order_qgis3_project.qgs'))

        layer_x = prj.mapLayers()['x20180406151213536']
        layer_y = prj.mapLayers()['y20180406151217017']

        # check layer order
        tree = prj.layerTreeRoot()
        self.assertEqual(tree.children()[0].layer(), layer_x)
        self.assertEqual(tree.children()[1].layer(), layer_y)
        self.assertTrue(tree.hasCustomLayerOrder())
        self.assertEqual(tree.customLayerOrder(), [layer_y, layer_x])
        self.assertEqual(tree.layerOrder(), [layer_y, layer_x])

    def testPalPropertiesReadWrite(self):
        tmpDir = QTemporaryDir()
        tmpFile = "{}/project.qgs".format(tmpDir.path())

        s0 = QgsLabelingEngineSettings()
        s0.setNumCandidatePositions(3, 33, 333)

        p0 = QgsProject()
        p0.setFileName(tmpFile)
        p0.setLabelingEngineSettings(s0)
        p0.write()

        p1 = QgsProject()
        p1.read(tmpFile)

        s1 = p1.labelingEngineSettings()
        candidates = s1.numCandidatePositions()

        self.assertEqual(candidates[0], 3)
        self.assertEqual(candidates[1], 33)
        self.assertEqual(candidates[2], 333)

    def testLayerChangeDirtiesProject(self):
        """
        Test that making changes to certain layer properties results in dirty projects
        """
        p = QgsProject()
        l = QgsVectorLayer(os.path.join(TEST_DATA_DIR, "points.shp"), "points", "ogr")
        self.assertTrue(l.isValid())
        self.assertTrue(p.addMapLayers([l]))
        p.setDirty(False)

        l.setCrs(QgsCoordinateReferenceSystem('EPSG:3111'))
        self.assertTrue(p.isDirty())
        p.setDirty(False)

        l.setName('test')
        self.assertTrue(p.isDirty())
        p.setDirty(False)

        self.assertTrue(l.setSubsetString('class=\'a\''))
        self.assertTrue(p.isDirty())

    def testProjectTitleWithPeriod(self):
        tmpDir = QTemporaryDir()
        tmpFile = "{}/2.18.21.qgs".format(tmpDir.path())
        tmpFile2 = "{}/qgis-3.2.0.qgs".format(tmpDir.path())

        p0 = QgsProject()
        p0.setFileName(tmpFile)

        p1 = QgsProject()
        p1.setFileName(tmpFile2)

        self.assertEqual(p0.baseName(), '2.18.21')
        self.assertEqual(p1.baseName(), 'qgis-3.2.0')

    def testWriteEntry(self):

        tmpDir = QTemporaryDir()
        tmpFile = "{}/project.qgs".format(tmpDir.path())

        # zip with existing file
        project = QgsProject()
        query = 'select * from "sample DH" where "sample DH"."Elev" > 130 and "sample DH"."Elev" < 140'
        self.assertTrue(project.writeEntry('myscope', 'myentry', query))
        self.assertTrue(project.write(tmpFile))

        self.assertTrue(project.read(tmpFile))
        q, ok = project.readEntry('myscope', 'myentry')
        self.assertTrue(ok)
        self.assertEqual(q, query)

    def testDirtying(self):

        project = QgsProject()

        # writing a new entry should dirty the project
        project.setDirty(False)
        self.assertTrue(project.writeEntry('myscope', 'myentry', True))
        self.assertTrue(project.isDirty())

        # over-writing a pre-existing entry with the same value should _not_ dirty the project
        project.setDirty(False)
        self.assertTrue(project.writeEntry('myscope', 'myentry', True))
        self.assertFalse(project.isDirty())

        # over-writing a pre-existing entry with a different value should dirty the project
        project.setDirty(False)
        self.assertTrue(project.writeEntry('myscope', 'myentry', False))
        self.assertTrue(project.isDirty())

        # removing an existing entry should dirty the project
        project.setDirty(False)
        self.assertTrue(project.removeEntry('myscope', 'myentry'))
        self.assertTrue(project.isDirty())

        # removing a non-existing entry should _not_ dirty the project
        project.setDirty(False)
        self.assertTrue(project.removeEntry('myscope', 'myentry'))
        self.assertFalse(project.isDirty())

        # setting a project CRS with a new value should dirty the project
        project.setCrs(QgsCoordinateReferenceSystem('EPSG:4326'))
        project.setDirty(False)
        project.setCrs(QgsCoordinateReferenceSystem('EPSG:3148'))
        self.assertTrue(project.isDirty())

        # setting a project CRS with the same project CRS should not dirty the project
        project.setDirty(False)
        project.setCrs(QgsCoordinateReferenceSystem('EPSG:3148'))
        self.assertFalse(project.isDirty())

    def testBackgroundColor(self):
        p = QgsProject()
        s = QgsSettings()

        red = int(s.value("qgis/default_canvas_color_red", 255))
        green = int(s.value("qgis/default_canvas_color_green", 255))
        blue = int(s.value("qgis/default_canvas_color_blue", 255))
        # test default canvas background color
        self.assertEqual(p.backgroundColor(), QColor(red, green, blue))
        spy = QSignalSpy(p.backgroundColorChanged)
        p.setBackgroundColor(QColor(0, 0, 0))
        self.assertEqual(len(spy), 1)
        # test customized canvas background color
        self.assertEqual(p.backgroundColor(), QColor(0, 0, 0))
        # test signal not emitted when color doesn't actually change
        p.setBackgroundColor(QColor(0, 0, 0))
        self.assertEqual(len(spy), 1)

    def testSelectionColor(self):
        p = QgsProject()
        s = QgsSettings()

        red = int(s.value("qgis/default_selection_color_red", 255))
        green = int(s.value("qgis/default_selection_color_green", 255))
        blue = int(s.value("qgis/default_selection_color_blue", 0))
        alpha = int(s.value("qgis/default_selection_color_alpha", 255))
        # test default feature selection color
        self.assertEqual(p.selectionColor(), QColor(red, green, blue, alpha))
        spy = QSignalSpy(p.selectionColorChanged)
        p.setSelectionColor(QColor(0, 0, 0, 50))
        self.assertEqual(len(spy), 1)
        # test customized feature selection color
        self.assertEqual(p.selectionColor(), QColor(0, 0, 0, 50))
        # test signal not emitted when color doesn't actually change
        p.setSelectionColor(QColor(0, 0, 0, 50))
        self.assertEqual(len(spy), 1)

    def testColorScheme(self):
        p = QgsProject.instance()
        spy = QSignalSpy(p.projectColorsChanged)
        p.setProjectColors([[QColor(255, 0, 0), 'red'], [QColor(0, 255, 0), 'green']])
        self.assertEqual(len(spy), 1)
        scheme = [s for s in QgsApplication.colorSchemeRegistry().schemes() if isinstance(s, QgsProjectColorScheme)][0]
        self.assertEqual([[c[0].name(), c[1]] for c in scheme.fetchColors()], [['#ff0000', 'red'], ['#00ff00', 'green']])
        # except color changed signal when clearing project
        p.clear()
        self.assertEqual(len(spy), 2)
        self.assertEqual([[c[0].name(), c[1]] for c in scheme.fetchColors()], [])

        # should be no signal on project destruction -- can cause a crash
        p = QgsProject()
        spy = QSignalSpy(p.projectColorsChanged)
        p.deleteLater()
        del p
        self.assertEqual(len(spy), 0)

    def testTransformContextSignalIsEmitted(self):
        """Test that when a project transform context changes a transformContextChanged signal is emitted"""

        p = QgsProject()
        spy = QSignalSpy(p.transformContextChanged)
        ctx = QgsCoordinateTransformContext()
        ctx.addSourceDestinationDatumTransform(QgsCoordinateReferenceSystem(4326), QgsCoordinateReferenceSystem(3857), 1234, 1235)
        ctx.addCoordinateOperation(QgsCoordinateReferenceSystem(4326), QgsCoordinateReferenceSystem(3857), 'x')
        p.setTransformContext(ctx)
        self.assertEqual(len(spy), 1)

    def testGpkgDirtyingWhenRemovedFromStorage(self):
        """Test that when a GPKG stored project is removed from the storage it is marked dirty"""

        with TemporaryDirectory() as d:
            path = os.path.join(d, 'relative_paths_gh30387.gpkg')
            copyfile(os.path.join(TEST_DATA_DIR, 'projects', 'relative_paths_gh30387.gpkg'), path)
            project = QgsProject.instance()
            # Project URI
            uri = 'geopackage://{}?projectName=relative_project'.format(path)
            project.setFileName(uri)
            self.assertTrue(project.write())
            # Verify
            self.assertTrue(project.read(uri))
            self.assertFalse(project.isDirty())
            # Remove from storage
            storage = QgsApplication.projectStorageRegistry().projectStorageFromUri(uri)
            self.assertTrue(storage.removeProject(uri))
            self.assertTrue(project.isDirty())
            # Save it back
            self.assertTrue(project.write())
            self.assertFalse(project.isDirty())
            # Reload
            self.assertTrue(project.read(uri))

    def testMapScales(self):
        p = QgsProject()
        self.assertFalse(p.mapScales())
        self.assertFalse(p.useProjectScales())

        spy = QSignalSpy(p.mapScalesChanged)
        p.setMapScales([])
        self.assertEqual(len(spy), 0)
        p.setUseProjectScales(False)
        self.assertEqual(len(spy), 0)

        p.setMapScales([5000, 6000, 3000, 4000])
        # scales must be sorted
        self.assertEqual(p.mapScales(), [6000.0, 5000.0, 4000.0, 3000.0])
        self.assertEqual(len(spy), 1)
        p.setMapScales([5000, 6000, 3000, 4000])
        self.assertEqual(len(spy), 1)
        self.assertEqual(p.mapScales(), [6000.0, 5000.0, 4000.0, 3000.0])
        p.setMapScales([5000, 6000, 3000, 4000, 1000])
        self.assertEqual(len(spy), 2)
        self.assertEqual(p.mapScales(), [6000.0, 5000.0, 4000.0, 3000.0, 1000.0])

        p.setUseProjectScales(True)
        self.assertEqual(len(spy), 3)
        p.setUseProjectScales(True)
        self.assertEqual(len(spy), 3)
        p.setUseProjectScales(False)
        self.assertEqual(len(spy), 4)

    def testSetInstance(self):
        """Test singleton API"""

        p = QgsProject()
        self.assertNotEqual(p, QgsProject.instance())
        QgsProject.setInstance(p)
        self.assertEqual(p, QgsProject.instance())


if __name__ == '__main__':
    unittest.main()
