"""
ARCHES - a program developed to inventory and manage immovable cultural heritage.
Copyright (C) 2013 J. Paul Getty Trust and World Monuments Fund

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.

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

import os, json, uuid
from django.core import management
from tests import test_settings
from tests.base_test import ArchesTestCase
from arches.app.models import models
from arches.app.models.graph import Graph, GraphValidationError
from arches.app.models.card import Card
from arches.app.utils.betterJSONSerializer import JSONSerializer, JSONDeserializer

# these tests can be run from the command line via
# python manage.py test tests/models/graph_tests.py --pattern="*.py" --settings="tests.test_settings"


class GraphTests(ArchesTestCase):
    @classmethod
    def setUpClass(cls):
        cls.loadOntology()

        for path in test_settings.RESOURCE_GRAPH_LOCATIONS:
            management.call_command("packages", operation="import_graphs", source=path)

        cls.NODE_NODETYPE_GRAPHID = "22000000-0000-0000-0000-000000000001"
        cls.SINGLE_NODE_GRAPHID = "22000000-0000-0000-0000-000000000000"

        # Node Branch
        graph_dict = {
            "author": "Arches",
            "color": None,
            "deploymentdate": None,
            "deploymentfile": None,
            "description": "Represents a single node in a graph",
            "graphid": cls.SINGLE_NODE_GRAPHID,
            "iconclass": "fa fa-circle",
            "isactive": True,
            "isresource": False,
            "name": "Node",
            "ontology_id": "e6e8db47-2ccf-11e6-927e-b8f6b115d7dd",
            "subtitle": "Represents a single node in a graph.",
            "version": "v1",
        }
        models.GraphModel.objects.create(**graph_dict).save()

        node_dict = {
            "config": None,
            "datatype": "semantic",
            "description": "Represents a single node in a graph",
            "graph_id": cls.SINGLE_NODE_GRAPHID,
            "isrequired": False,
            "issearchable": True,
            "istopnode": True,
            "name": "Node",
            "nodegroup_id": None,
            "nodeid": "20000000-0000-0000-0000-100000000000",
            "ontologyclass": "http://www.cidoc-crm.org/cidoc-crm/E1_CRM_Entity",
        }
        models.Node.objects.create(**node_dict).save()

        # Node/Node Type Branch
        graph_dict = {
            "author": "Arches",
            "color": None,
            "deploymentdate": None,
            "deploymentfile": None,
            "description": "Represents a node and node type pairing",
            "graphid": cls.NODE_NODETYPE_GRAPHID,
            "iconclass": "fa fa-angle-double-down",
            "isactive": True,
            "isresource": False,
            "name": "Node/Node Type",
            "ontology_id": "e6e8db47-2ccf-11e6-927e-b8f6b115d7dd",
            "subtitle": "Represents a node and node type pairing",
            "version": "v1",
        }
        models.GraphModel.objects.create(**graph_dict).save()

        nodegroup_dict = {
            "cardinality": "n",
            "legacygroupid": "",
            "nodegroupid": "20000000-0000-0000-0000-100000000001",
            "parentnodegroup_id": None,
        }
        models.NodeGroup.objects.create(**nodegroup_dict).save()

        card_dict = {
            "active": True,
            "cardid": "bf9ea150-3eaa-11e8-8b2b-c3a348661f61",
            "description": "Represents a node and node type pairing",
            "graph_id": cls.NODE_NODETYPE_GRAPHID,
            "helpenabled": False,
            "helptext": None,
            "helptitle": None,
            "instructions": "",
            "name": "Node/Node Type",
            "nodegroup_id": "20000000-0000-0000-0000-100000000001",
            "sortorder": None,
            "visible": True,
        }
        models.CardModel.objects.create(**card_dict).save()

        nodes = [
            {
                "config": None,
                "datatype": "string",
                "description": "",
                "graph_id": cls.NODE_NODETYPE_GRAPHID,
                "isrequired": False,
                "issearchable": True,
                "istopnode": True,
                "name": "Node",
                "nodegroup_id": "20000000-0000-0000-0000-100000000001",
                "nodeid": "20000000-0000-0000-0000-100000000001",
                "ontologyclass": "http://www.cidoc-crm.org/cidoc-crm/E1_CRM_Entity",
            },
            {
                "config": {"rdmCollection": None},
                "datatype": "concept",
                "description": "",
                "graph_id": cls.NODE_NODETYPE_GRAPHID,
                "isrequired": False,
                "issearchable": True,
                "istopnode": False,
                "name": "Node Type",
                "nodegroup_id": "20000000-0000-0000-0000-100000000001",
                "nodeid": "20000000-0000-0000-0000-100000000002",
                "ontologyclass": "http://www.cidoc-crm.org/cidoc-crm/E55_Type",
            },
        ]

        for node in nodes:
            models.Node.objects.create(**node).save()

        edges_dict = {
            "description": None,
            "domainnode_id": "20000000-0000-0000-0000-100000000001",
            "edgeid": "22200000-0000-0000-0000-000000000001",
            "graph_id": cls.NODE_NODETYPE_GRAPHID,
            "name": None,
            "ontologyproperty": "http://www.cidoc-crm.org/cidoc-crm/P2_has_type",
            "rangenode_id": "20000000-0000-0000-0000-100000000002",
        }
        models.Edge.objects.create(**edges_dict).save()

    @classmethod
    def tearDownClass(cls):
        cls.deleteGraph("2f7f8e40-adbc-11e6-ac7f-14109fd34195")

    def setUp(self):
        graph = Graph.new()
        graph.name = "TEST GRAPH"
        graph.subtitle = "ARCHES TEST GRAPH"
        graph.author = "Arches"
        graph.description = "ARCHES TEST GRAPH"
        graph.ontology_id = "e6e8db47-2ccf-11e6-927e-b8f6b115d7dd"
        graph.version = "v1.0.0"
        graph.isactive = False
        graph.iconclass = "fa fa-building"
        graph.nodegroups = []
        graph.save()

        graph.root.name = "ROOT NODE"
        graph.root.description = "Test Root Node"
        graph.root.ontologyclass = "http://www.cidoc-crm.org/cidoc-crm/E1_CRM_Entity"
        graph.root.datatype = "semantic"
        graph.root.save()

        self.rootNode = graph.root

    def tearDown(self):
        self.deleteGraph(self.rootNode.graph_id)

    def test_new_graph(self):
        name = "TEST NEW GRAPH"
        author = "ARCHES TEST"
        graph = Graph.new(name=name, is_resource=True, author=author)
        self.assertEqual(graph.name, name)
        self.assertEqual(graph.author, author)
        self.assertTrue(graph.isresource)
        self.assertFalse(graph.root.is_collector)
        self.assertEqual(len(graph.nodes), 1)
        self.assertEqual(len(graph.cards), 0)
        self.assertEqual(len(graph.get_nodegroups()), 0)

        graph = Graph.new(name=name, is_resource=False, author=author)
        self.assertEqual(graph.name, name)
        self.assertEqual(graph.author, author)
        self.assertFalse(graph.isresource)
        self.assertTrue(graph.root.is_collector)
        self.assertEqual(len(graph.nodes), 1)
        self.assertEqual(len(graph.cards), 1)
        self.assertEqual(len(graph.get_nodegroups()), 1)

    def test_graph_doesnt_pollute_db(self):
        """
        test that the mere act of creating a Graph instance doesn't save anything to the database

        """

        graph_obj = {
            "name": "TEST GRAPH",
            "subtitle": "ARCHES TEST GRAPH",
            "author": "Arches",
            "description": "ARCHES TEST GRAPH",
            "version": "v1.0.0",
            "isresource": True,
            "isactive": False,
            "iconclass": "fa fa-building",
            "nodegroups": [],
            "nodes": [
                {
                    "status": None,
                    "description": "",
                    "name": "ROOT_NODE",
                    "istopnode": True,
                    "ontologyclass": "",
                    "nodeid": "55555555-343e-4af3-8857-f7322dc9eb4b",
                    "nodegroup_id": "",
                    "datatype": "semantic",
                },
                {
                    "status": None,
                    "description": "",
                    "name": "NODE_NAME",
                    "istopnode": False,
                    "ontologyclass": "",
                    "nodeid": "66666666-24c9-4226-bde2-2c40ee60a26c",
                    "nodegroup_id": "66666666-24c9-4226-bde2-2c40ee60a26c",
                    "datatype": "string",
                },
            ],
            "edges": [
                {
                    "rangenode_id": "66666666-24c9-4226-bde2-2c40ee60a26c",
                    "name": "",
                    "edgeid": "11111111-d50f-11e5-8754-80e6500ee4e4",
                    "domainnode_id": "55555555-343e-4af3-8857-f7322dc9eb4b",
                    "ontologyproperty": "P2",
                    "description": "",
                }
            ],
            "cards": [
                {
                    "name": "NODE_NAME",
                    "description": "",
                    "instructions": "",
                    "helptext": "",
                    "cardinality": "n",
                    "nodegroup_id": "66666666-24c9-4226-bde2-2c40ee60a26c",
                }
            ],
            "functions": [],
        }

        nodes_count_before = models.Node.objects.count()
        edges_count_before = models.Edge.objects.count()
        cards_count_before = models.CardModel.objects.count()
        nodegroups_count_before = models.NodeGroup.objects.count()

        graph = Graph(graph_obj)

        self.assertEqual(models.Node.objects.count() - nodes_count_before, 0)
        self.assertEqual(models.Edge.objects.count() - edges_count_before, 0)
        self.assertEqual(models.CardModel.objects.count() - cards_count_before, 0)
        self.assertEqual(models.NodeGroup.objects.count() - nodegroups_count_before, 0)
        self.assertEqual(graph_obj["name"], graph.name)
        self.assertEqual(graph_obj["subtitle"], graph.subtitle)
        self.assertEqual(graph_obj["author"], graph.author)
        self.assertEqual(graph_obj["description"], graph.description)
        self.assertEqual(graph_obj["version"], graph.version)
        self.assertEqual(graph_obj["isresource"], graph.isresource)
        self.assertEqual(graph_obj["isactive"], graph.isactive)
        self.assertEqual(graph_obj["iconclass"], graph.iconclass)

    def test_nodes_are_byref(self):
        """
        test that the nodes referred to in the Graph.edges are exact references to
        the nodes as opposed to a node with the same attribute values

        """

        graph = Graph.objects.get(graphid=self.rootNode.graph_id)
        graph.append_branch("http://www.ics.forth.gr/isl/CRMdig/L54_is_same-as", graphid=self.NODE_NODETYPE_GRAPHID)
        graph.save()

        node_mapping = {nodeid: id(node) for nodeid, node in list(graph.nodes.items())}

        for key, edge in list(graph.edges.items()):
            self.assertEqual(node_mapping[edge.domainnode.pk], id(edge.domainnode))
            self.assertEqual(node_mapping[edge.rangenode.pk], id(edge.rangenode))

        for key, node in list(graph.nodes.items()):
            for key, edge in list(graph.edges.items()):
                newid = uuid.uuid4()
                if edge.domainnode.pk == node.pk:
                    node.pk = newid
                    self.assertEqual(edge.domainnode.pk, newid)
                elif edge.rangenode.pk == node.pk:
                    node.pk = newid
                    self.assertEqual(edge.rangenode.pk, newid)

    def test_copy_graph(self):
        """
        test that a copy of a graph has the same number of nodes and edges and that the primary keys have been changed
        and that the actual node references are different

        """

        graph = Graph.new(name="TEST RESOURCE")
        graph.append_branch("http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", graphid=self.NODE_NODETYPE_GRAPHID)
        graph_copy = graph.copy()["copy"]

        self.assertEqual(len(graph_copy.nodes), 3)
        self.assertEqual(len(graph_copy.edges), 2)
        self.assertEqual(len(graph_copy.cards), 2)
        self.assertEqual(len(graph_copy.get_nodegroups()), 2)
        self.assertEqual(len(graph.nodes), len(graph_copy.nodes))
        self.assertEqual(len(graph.edges), len(graph_copy.edges))
        self.assertEqual(len(graph.cards), len(graph_copy.cards))
        self.assertEqual(len(graph.get_nodegroups()), len(graph_copy.get_nodegroups()))

        # assert the copied nodegroup heirarchy is maintained
        for nodegroup in graph_copy.get_nodegroups():
            if graph_copy.nodes[nodegroup.pk] is graph_copy.root:
                parentnodegroup_copy = nodegroup
            else:
                childnodegroup_copy = nodegroup
        self.assertTrue(parentnodegroup_copy.parentnodegroup is None)
        self.assertEqual(childnodegroup_copy.parentnodegroup, parentnodegroup_copy)
        self.assertFalse(parentnodegroup_copy.parentnodegroup_id)
        self.assertEqual(childnodegroup_copy.parentnodegroup_id, parentnodegroup_copy.pk)

        # assert the copied node groups are not equal to the originals
        for nodegroup in graph.get_nodegroups():
            if graph.nodes[nodegroup.pk] is graph.root:
                parentnodegroup = nodegroup
            else:
                childnodegroup = nodegroup

        self.assertNotEqual(parentnodegroup, parentnodegroup_copy)
        self.assertNotEqual(parentnodegroup.pk, parentnodegroup_copy.pk)
        self.assertNotEqual(childnodegroup, childnodegroup_copy)
        self.assertNotEqual(childnodegroup.pk, childnodegroup_copy.pk)

        # assert the nodegroups attached to the cards are heirarchically correct
        for card in list(graph_copy.cards.values()):
            if str(card.nodegroup_id) == str(graph_copy.root.nodeid):
                parentcard_copy = card
            else:
                childcard_copy = card

        self.assertTrue(parentcard_copy.nodegroup is not None)
        self.assertTrue(childcard_copy.nodegroup is not None)
        self.assertTrue(parentcard_copy.nodegroup.parentnodegroup is None)
        self.assertTrue(childcard_copy.nodegroup.parentnodegroup is not None)
        self.assertEqual(parentcard_copy.nodegroup, childcard_copy.nodegroup.parentnodegroup)

        def findNodeByName(graph, name):
            for node in list(graph.nodes.values()):
                if node.name == name:
                    return node
            return None

        def findCardByName(graph, name):
            for card in list(graph.cards.values()):
                if card.name == name:
                    return card
            return None

        for node in list(graph.nodes.values()):
            node_copy = findNodeByName(graph_copy, node.name)
            self.assertIsNotNone(node_copy)
            self.assertNotEqual(node.pk, node_copy.pk)
            self.assertNotEqual(id(node), id(node_copy))
            self.assertEqual(node.is_collector, node_copy.is_collector)
            if node.nodegroup is not None:
                self.assertNotEqual(node.nodegroup, node_copy.nodegroup)

        for card in list(graph.cards.values()):
            card_copy = findCardByName(graph_copy, card.name)
            self.assertIsNotNone(card_copy)
            self.assertNotEqual(card.pk, card_copy.pk)
            self.assertNotEqual(id(card), id(card_copy))
            self.assertNotEqual(card.nodegroup, card_copy.nodegroup)

        for newedge in list(graph_copy.edges.values()):
            self.assertIsNotNone(graph_copy.nodes[newedge.domainnode_id])
            self.assertIsNotNone(graph_copy.nodes[newedge.rangenode_id])
            self.assertEqual(newedge.domainnode, graph_copy.nodes[newedge.domainnode.pk])
            self.assertEqual(newedge.rangenode, graph_copy.nodes[newedge.rangenode.pk])
            with self.assertRaises(KeyError):
                graph.edges[newedge.pk]

    def test_branch_append_with_ontology(self):
        """
        test if a branch is properly appended to a graph that defines an ontology

        """

        nodes_count_before = models.Node.objects.count()
        edges_count_before = models.Edge.objects.count()
        cards_count_before = models.CardModel.objects.count()
        nodegroups_count_before = models.NodeGroup.objects.count()

        graph = Graph.objects.get(pk=self.rootNode.graph.graphid)
        self.assertEqual(len(graph.nodes), 1)
        self.assertEqual(len(graph.edges), 0)
        self.assertEqual(len(graph.cards), 1)
        self.assertEqual(len(graph.get_nodegroups()), 1)

        appended_graph = graph.append_branch("http://www.ics.forth.gr/isl/CRMdig/L54_is_same-as", graphid=self.NODE_NODETYPE_GRAPHID)
        graph.save()

        self.assertEqual(len(graph.nodes), 3)
        self.assertEqual(len(graph.edges), 2)
        self.assertEqual(len(graph.cards), 2)
        self.assertEqual(len(graph.get_nodegroups()), 2)

        self.assertEqual(models.Node.objects.count() - nodes_count_before, 2)
        self.assertEqual(models.Edge.objects.count() - edges_count_before, 2)
        self.assertEqual(models.CardModel.objects.count() - cards_count_before, 1)
        self.assertEqual(models.NodeGroup.objects.count() - nodegroups_count_before, 1)

        for key, edge in list(graph.edges.items()):
            self.assertIsNotNone(graph.nodes[edge.domainnode_id])
            self.assertIsNotNone(graph.nodes[edge.rangenode_id])
            self.assertEqual(edge.domainnode, graph.nodes[edge.domainnode.pk])
            self.assertEqual(edge.rangenode, graph.nodes[edge.rangenode.pk])
            self.assertIsNotNone(edge.ontologyproperty)

        for key, node in list(graph.nodes.items()):
            self.assertIsNotNone(node.ontologyclass)
            if node.istopnode:
                self.assertEqual(node, self.rootNode)

        # confirm that a non-grouped node takes on the parent group when appended
        appended_branch = graph.append_branch("http://www.ics.forth.gr/isl/CRMdig/L54_is_same-as", graphid=self.SINGLE_NODE_GRAPHID)
        self.assertEqual(len(graph.nodes), 4)
        self.assertEqual(len(graph.edges), 3)
        self.assertEqual(len(graph.cards), 2)
        self.assertEqual(len(graph.get_nodegroups()), 2)
        self.assertEqual(appended_branch.root.nodegroup, self.rootNode.nodegroup)

        graph.save()
        self.assertEqual(models.Node.objects.count() - nodes_count_before, 3)
        self.assertEqual(models.Edge.objects.count() - edges_count_before, 3)
        self.assertEqual(models.CardModel.objects.count() - cards_count_before, 1)
        self.assertEqual(models.NodeGroup.objects.count() - nodegroups_count_before, 1)

    def test_rules_for_appending(self):
        """
        test the rules that control the appending of branches to graphs

        """

        graph = Graph.objects.get(node=self.rootNode)
        graph.isresource = True
        self.assertIsNotNone(graph.append_branch("http://www.ics.forth.gr/isl/CRMdig/L54_is_same-as", graphid=self.NODE_NODETYPE_GRAPHID))

        graph = Graph.new()
        graph.root.datatype = "string"
        graph.update_node(JSONSerializer().serializeToPython(graph.root))

        # create card collector graph to use for appending on to other graphs
        collector_graph = Graph.new()
        collector_graph.append_branch("http://www.ics.forth.gr/isl/CRMdig/L54_is_same-as", graphid=self.NODE_NODETYPE_GRAPHID)
        collector_graph.save()

    def test_node_update(self):
        """
        test to make sure that node groups and card are properly managed
        when changing a nodegroup value on a node being updated

        """

        # create a graph, append the node/node type graph and confirm is has the correct
        # number of nodegroups then remove the appended branches group and reconfirm that
        # the proper number of groups are properly relfected in the graph

        graph = Graph.objects.get(pk=self.rootNode.graph.graphid)
        graph.append_branch("http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", graphid=self.NODE_NODETYPE_GRAPHID)

        node_to_update = None
        for node_id, node in list(graph.nodes.items()):
            if node.name == "Node":
                node_to_update = JSONDeserializer().deserialize(JSONSerializer().serialize(node))
            if node.name == "Node Type":
                node_type_node = JSONDeserializer().deserialize(JSONSerializer().serialize(node))

        # confirm that nulling out a child group will then make that group a part of the parent group
        node_to_update["nodegroup_id"] = None
        graph.update_node(node_to_update)
        self.assertEqual(len(graph.get_nodegroups()), 1)
        self.assertEqual(len(graph.cards), 1)
        for node in list(graph.nodes.values()):
            self.assertEqual(graph.root.nodegroup, node.nodegroup)

        graph.append_branch(
            "http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", nodeid=node_type_node["nodeid"], graphid=self.SINGLE_NODE_GRAPHID
        )
        for edge in list(graph.edges.values()):
            if str(edge.domainnode_id) == str(node_type_node["nodeid"]):
                child_nodegroup_node = JSONDeserializer().deserialize(JSONSerializer().serialize(edge.rangenode))

        # make a node group with a single node and confirm that that node is now not part of it's parent node group
        child_nodegroup_node["nodegroup_id"] = child_nodegroup_node["nodeid"]
        graph.update_node(child_nodegroup_node)
        self.assertEqual(len(graph.get_nodegroups()), 2)
        for node_id, node in list(graph.nodes.items()):
            if node_id == child_nodegroup_node["nodeid"]:
                self.assertNotEqual(graph.root.nodegroup, node.nodegroup)
            else:
                self.assertEqual(graph.root.nodegroup, node.nodegroup)

        # make another node group with a node (that has a child) and confirm that that node and
        # it's child are now not part of it's parent node group and that both nodes are grouped together
        node_to_update["nodegroup_id"] = node_to_update["nodeid"]
        graph.update_node(node_to_update)
        self.assertEqual(len(graph.get_nodegroups()), 3)
        children = graph.get_child_nodes(node_to_update["nodeid"])
        for child in children:
            if child.nodeid == child_nodegroup_node["nodeid"]:
                self.assertEqual(child.nodeid, child.nodegroup_id)
            else:
                self.assertEqual(child.nodegroup_id, node_to_update["nodegroup_id"])

        # remove a node's node group and confirm that that node takes the node group of it's parent
        child_nodegroup_node["nodegroup_id"] = None
        graph.update_node(child_nodegroup_node)
        self.assertEqual(len(graph.get_nodegroups()), 2)
        children = graph.get_child_nodes(node_to_update["nodeid"])
        for child in children:
            self.assertEqual(child.nodegroup_id, node_to_update["nodegroup_id"])

    def test_move_node(self):
        """
        test if a node can be successfully moved to another node in the graph

        """

        # test moving a single node to another branch
        # this node should be grouped with it's new parent nodegroup
        graph = Graph.objects.get(pk=self.rootNode.graph.graphid)
        branch_one = graph.append_branch("http://www.ics.forth.gr/isl/CRMdig/L54_is_same-as", graphid=self.NODE_NODETYPE_GRAPHID)
        for node in list(branch_one.nodes.values()):
            if node is branch_one.root:
                node.name = "branch_one_root"
            else:
                node.name = "branch_one_child"
        branch_two = graph.append_branch("http://www.ics.forth.gr/isl/CRMdig/L54_is_same-as", graphid=self.NODE_NODETYPE_GRAPHID)
        for node in list(branch_two.nodes.values()):
            if node is branch_two.root:
                node.name = "branch_two_root"
            else:
                node.name = "branch_two_child"
        branch_three = graph.append_branch("http://www.ics.forth.gr/isl/CRMdig/L54_is_same-as", graphid=self.SINGLE_NODE_GRAPHID)
        branch_three.root.name = "branch_three_root"
        self.assertEqual(len(graph.edges), 5)
        self.assertEqual(len(graph.nodes), 6)

        branch_three_nodeid = next(iter(list(branch_three.nodes.keys())))
        branch_one_rootnodeid = branch_one.root.nodeid
        graph.move_node(branch_three_nodeid, "http://www.ics.forth.gr/isl/CRMdig/L54_is_same-as", branch_one_rootnodeid)
        self.assertEqual(len(graph.edges), 5)
        self.assertEqual(len(graph.nodes), 6)

        new_parent_nodegroup = None
        moved_branch_nodegroup = None
        for node_id, node in list(graph.nodes.items()):
            if node_id == branch_one_rootnodeid:
                new_parent_nodegroup = node.nodegroup
            if node_id == branch_three_nodeid:
                moved_branch_nodegroup = node.nodegroup

        self.assertIsNotNone(new_parent_nodegroup)
        self.assertIsNotNone(moved_branch_nodegroup)
        self.assertEqual(new_parent_nodegroup, moved_branch_nodegroup)

        # test moving a branch to another branch
        # this branch should NOT be grouped with it's new parent nodegroup
        branch_two_rootnodeid = branch_two.root.nodeid
        graph.move_node(
            branch_one_rootnodeid, "http://www.ics.forth.gr/isl/CRMdig/L54_is_same-as", branch_two_rootnodeid, skip_validation=True
        )
        self.assertEqual(len(graph.edges), 5)
        self.assertEqual(len(graph.nodes), 6)

        new_parent_nodegroup = None
        moved_branch_nodegroup = None
        for node_id, node in list(graph.nodes.items()):
            if node_id == branch_two_rootnodeid:
                new_parent_nodegroup = node.nodegroup
            if node_id == branch_one_rootnodeid:
                moved_branch_nodegroup = node.nodegroup

        self.assertIsNotNone(new_parent_nodegroup)
        self.assertIsNotNone(moved_branch_nodegroup)
        self.assertNotEqual(new_parent_nodegroup, moved_branch_nodegroup)

        updated_edge = None
        for edge_id, edge in list(graph.edges.items()):
            if edge.domainnode_id == branch_two_rootnodeid and edge.rangenode_id == branch_one_rootnodeid:
                updated_edge = edge

        self.assertIsNotNone(updated_edge)

        # save and retrieve the graph from the database and confirm that
        # the graph shape has been saved properly
        graph.save()
        for node in list(branch_two.nodes.values()):
            node.datatype = "semantic"
        graph.save()
        graph = Graph.objects.get(pk=self.rootNode.graph.graphid)
        tree = graph.get_tree()

        self.assertEqual(len(tree["children"]), 1)
        level_one_node = tree["children"][0]

        self.assertEqual(branch_two_rootnodeid, level_one_node["node"].nodeid)
        self.assertEqual(len(level_one_node["children"]), 2)
        for child in level_one_node["children"]:
            if child["node"].nodeid == branch_one_rootnodeid:
                self.assertEqual(len(child["children"]), 2)
                found_branch_three = False
                for child in child["children"]:
                    if child["node"].nodeid == branch_three_nodeid:
                        found_branch_three = True
                self.assertTrue(found_branch_three)
            else:
                self.assertEqual(len(child["children"]), 0)

        # Pressumed final graph shape
        #
        #                                                self.rootNode
        #                                                      |
        #                                            branch_two_rootnodeid (Node)
        #                                                    /   \
        #                         branch_one_rootnodeid (Node)    branch_two_child (NodeType)
        #                                 /   \
        # branch_one_childnodeid (NodeType)    branch_three_nodeid (Node)

    def test_get_valid_ontology_classes(self):
        """
        test to see if we return the proper ontology classes for a graph that uses an ontology system

        """

        graph = Graph.objects.get(pk=self.rootNode.graph.graphid)
        ret = graph.get_valid_ontology_classes(nodeid=self.rootNode.nodeid)
        self.assertTrue(len(ret) == 1)

        self.assertEqual(ret[0]["ontology_property"], "")
        self.assertEqual(len(ret[0]["ontology_classes"]), models.OntologyClass.objects.filter(ontology_id=graph.ontology_id).count())

    def test_get_valid_ontology_classes_on_resource_with_no_ontology_set(self):
        """
        test to see if we return the proper ontology classes for a graph that doesn't use an ontology system

        """

        self.rootNode.graph.ontology_id = None
        graph = Graph.objects.get(pk=self.rootNode.graph.graphid)

        graph.ontology_id = None
        ret = graph.get_valid_ontology_classes(nodeid=self.rootNode.nodeid)
        self.assertTrue(len(ret) == 0)

    def test_append_branch_to_resource_with_no_ontology_system(self):
        """
        test to see that we remove all ontologyclass and ontologyproperty references when appending a
        graph that uses an ontology system to a graph that doesn't

        """

        graph = Graph.objects.get(pk=self.rootNode.graph.graphid)
        graph.clear_ontology_references()
        graph.append_branch("http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", graphid=self.NODE_NODETYPE_GRAPHID)
        for node_id, node in list(graph.nodes.items()):
            self.assertTrue(node.ontologyclass is None)
        for edge_id, edge in list(graph.edges.items()):
            self.assertTrue(edge.ontologyproperty is None)

    def test_save_and_update_dont_orphan_records_in_the_db(self):
        """
        test that the proper number of nodes, edges, nodegroups, and cards are persisted
        to the database during save and update opertaions

        """

        nodes_count_before = models.Node.objects.count()
        edges_count_before = models.Edge.objects.count()
        nodegroups_count_before = models.NodeGroup.objects.count()
        card_count_before = models.CardModel.objects.count()

        # test that data is persisited propertly when creating a new graph
        graph = Graph.new(is_resource=False)

        nodes_count_after = models.Node.objects.count()
        edges_count_after = models.Edge.objects.count()
        nodegroups_count_after = models.NodeGroup.objects.count()
        card_count_after = models.CardModel.objects.count()

        self.assertEqual(nodes_count_after - nodes_count_before, 1)
        self.assertEqual(edges_count_after - edges_count_before, 0)
        self.assertEqual(nodegroups_count_after - nodegroups_count_before, 1)
        self.assertEqual(card_count_after - card_count_before, 1)

        # test that data is persisited propertly during an append opertation
        graph.append_branch("http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", graphid=self.NODE_NODETYPE_GRAPHID)
        graph.save()

        nodes_count_after = models.Node.objects.count()
        edges_count_after = models.Edge.objects.count()
        nodegroups_count_after = models.NodeGroup.objects.count()
        card_count_after = models.CardModel.objects.count()

        self.assertEqual(nodes_count_after - nodes_count_before, 3)
        self.assertEqual(edges_count_after - edges_count_before, 2)
        self.assertEqual(nodegroups_count_after - nodegroups_count_before, 2)
        self.assertEqual(card_count_after - card_count_before, 2)

        # test that removing a node group by setting it to None, removes it from the db
        node_to_update = None
        for node_id, node in list(graph.nodes.items()):
            if node.name == "Node":
                self.assertTrue(node.is_collector)
                node_to_update = JSONDeserializer().deserialize(JSONSerializer().serialize(node))

        node_to_update["nodegroup_id"] = None
        graph.update_node(node_to_update.copy())
        graph.save()

        nodegroups_count_after = models.NodeGroup.objects.count()
        card_count_after = models.CardModel.objects.count()

        self.assertEqual(nodegroups_count_after - nodegroups_count_before, 1)
        self.assertEqual(card_count_after - card_count_before, 1)

        # test that adding back a node group adds it back to the db
        node_to_update["nodegroup_id"] = node_to_update["nodeid"]
        graph.update_node(node_to_update)
        graph.save()

        nodegroups_count_after = models.NodeGroup.objects.count()
        card_count_after = models.CardModel.objects.count()

        self.assertEqual(nodegroups_count_after - nodegroups_count_before, 2)
        self.assertEqual(card_count_after - card_count_before, 2)

    def test_delete_graph(self):
        """
        test the graph delete method

        """

        graph = Graph.objects.get(graphid=self.NODE_NODETYPE_GRAPHID)
        self.assertEqual(len(graph.nodes), 2)
        self.assertEqual(len(graph.edges), 1)
        self.assertEqual(len(graph.get_nodegroups()), 1)

        nodes_count_before = models.Node.objects.count()
        edges_count_before = models.Edge.objects.count()
        nodegroups_count_before = models.NodeGroup.objects.count()
        card_count_before = models.CardModel.objects.count()

        graph.delete()

        nodes_count_after = models.Node.objects.count()
        edges_count_after = models.Edge.objects.count()
        nodegroups_count_after = models.NodeGroup.objects.count()
        card_count_after = models.CardModel.objects.count()

        self.assertEqual(nodes_count_before - nodes_count_after, 2)
        self.assertEqual(edges_count_before - edges_count_after, 1)
        self.assertEqual(nodegroups_count_before - nodegroups_count_after, 1)
        self.assertEqual(card_count_before - card_count_after, 1)

        node_count = models.Node.objects.filter(graph_id=self.NODE_NODETYPE_GRAPHID).count()
        edge_count = models.Edge.objects.filter(graph_id=self.NODE_NODETYPE_GRAPHID).count()
        self.assertEqual(node_count, 0)
        self.assertEqual(edge_count, 0)

    def test_delete_node(self):
        """
        test the node delete method

        """
        graph = Graph.new(name="TEST", is_resource=False, author="TEST")
        graph.append_branch("http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", graphid=self.NODE_NODETYPE_GRAPHID)
        graph.save()
        node = models.Node.objects.get(graph=graph, name="Node")

        nodes_count_before = models.Node.objects.count()
        edges_count_before = models.Edge.objects.count()
        nodegroups_count_before = models.NodeGroup.objects.count()
        card_count_before = models.CardModel.objects.count()

        graph.delete_node(node)

        nodes_count_after = models.Node.objects.count()
        edges_count_after = models.Edge.objects.count()
        nodegroups_count_after = models.NodeGroup.objects.count()
        card_count_after = models.CardModel.objects.count()

        self.assertEqual(nodes_count_before - nodes_count_after, 2)
        self.assertEqual(edges_count_before - edges_count_after, 2)
        self.assertEqual(nodegroups_count_before - nodegroups_count_after, 1)
        self.assertEqual(card_count_before - card_count_after, 1)

        graph = Graph.objects.get(graphid=graph.pk)
        self.assertEqual(len(graph.nodes), 1)
        self.assertEqual(len(graph.edges), 0)
        self.assertEqual(len(graph.cards), 1)
        self.assertEqual(len(graph.get_nodegroups()), 1)

        graph.append_branch("http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", graphid=self.NODE_NODETYPE_GRAPHID)
        graph.save()
        node = models.Node.objects.get(graph=graph, name="Node Type")
        graph.delete_node(node)
        graph = Graph.objects.get(graphid=graph.pk)
        self.assertEqual(len(graph.nodes), 2)
        self.assertEqual(len(graph.edges), 1)
        self.assertEqual(len(graph.cards), 2)
        self.assertEqual(len(graph.get_nodegroups()), 2)

    def test_derive_card_values(self):
        """
        test to make sure we get the proper name and description for display in the ui

        """

        # TESTING A GRAPH
        graph = Graph.new(name="TEST", is_resource=False, author="TEST")
        graph.description = "A test description"

        self.assertEqual(len(graph.cards), 1)
        for card in graph.get_cards():
            self.assertEqual(card["name"], graph.name)
            self.assertEqual(card["description"], graph.description)
            card = Card.objects.get(pk=card["cardid"])
            card.name = "TEST card name"
            card.description = "TEST card description"
            card.save()

        for card in graph.get_cards():
            self.assertEqual(card["name"], "TEST")
            self.assertEqual(card["description"], "A test description")

        graph.append_branch("http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", graphid=self.SINGLE_NODE_GRAPHID)
        graph.save()

        for node in list(graph.nodes.values()):
            if node is not graph.root:
                nodeJson = JSONSerializer().serializeToPython(node)
                nodeJson["nodegroup_id"] = nodeJson["nodeid"]
                graph.update_node(nodeJson)

        graph.save()

        self.assertEqual(len(graph.get_cards()), 2)
        for card in graph.get_cards():
            if str(card["nodegroup_id"]) == str(graph.root.nodegroup_id):
                self.assertEqual(card["name"], graph.name)
                self.assertEqual(card["description"], graph.description)
            else:
                self.assertTrue(len(graph.nodes[card["nodegroup_id"]].name) > 0)
                self.assertTrue(len(graph.nodes[card["nodegroup_id"]].description) > 0)
                self.assertEqual(card["name"], graph.nodes[card["nodegroup_id"]].name)
                self.assertEqual(card["description"], graph.nodes[card["nodegroup_id"]].description)

        # TESTING A RESOURCE
        resource_graph = Graph.new(name="TEST RESOURCE", is_resource=True, author="TEST")
        resource_graph.description = "A test resource description"
        resource_graph.append_branch("http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", graphid=graph.graphid)
        resource_graph.save()

        self.assertEqual(len(resource_graph.get_cards()), 2)

        for card in resource_graph.get_cards():
            cardobj = Card.objects.get(pk=card["cardid"])
            if cardobj.nodegroup.parentnodegroup is None:
                self.assertEqual(card["name"], graph.name)
                self.assertEqual(card["description"], graph.description)
            else:
                self.assertEqual(card["name"], resource_graph.nodes[card["nodegroup_id"]].name)
                self.assertEqual(card["description"], resource_graph.nodes[card["nodegroup_id"]].description)
                self.assertTrue(len(resource_graph.nodes[card["nodegroup_id"]].name) > 0)
                self.assertTrue(len(resource_graph.nodes[card["nodegroup_id"]].description) > 0)

        resource_graph.delete()

        # TESTING A RESOURCE
        resource_graph = Graph.new(name="TEST", is_resource=True, author="TEST")
        resource_graph.description = "A test description"
        resource_graph.append_branch("http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", graphid=self.NODE_NODETYPE_GRAPHID)
        resource_graph.save()

        self.assertEqual(len(resource_graph.cards), 1)
        the_card = next(iter(list(resource_graph.cards.values())))
        for card in resource_graph.get_cards():
            self.assertEqual(card["name"], the_card.name)
            self.assertEqual(card["description"], the_card.description)

        # after removing the card name and description, the cards should take on the node name and description
        the_card.name = ""
        the_card.description = ""
        for card in resource_graph.get_cards():
            self.assertEqual(card["name"], resource_graph.nodes[card["nodegroup_id"]].name)
            self.assertEqual(card["description"], resource_graph.nodes[card["nodegroup_id"]].description)

    def test_get_root_nodegroup(self):
        """
        test we can get the right parent NodeGroup

        """

        graph = Graph.new(name="TEST", is_resource=False, author="TEST")
        graph.append_branch("http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", graphid=self.NODE_NODETYPE_GRAPHID)

        for node in list(graph.nodes.values()):
            if node.is_collector:
                if node.nodegroup.parentnodegroup is None:
                    self.assertEqual(graph.get_root_nodegroup(), node.nodegroup)

    def test_get_root_card(self):
        """
        test we can get the right parent card

        """

        graph = Graph.new(name="TEST", is_resource=False, author="TEST")
        graph.append_branch("http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", graphid=self.NODE_NODETYPE_GRAPHID)

        for card in list(graph.cards.values()):
            if card.nodegroup.parentnodegroup is None:
                self.assertEqual(graph.get_root_card(), card)

    def test_graph_validation_of_null_ontology_class(self):
        """
        test to make sure null ontology classes aren't allowed

        """

        graph = Graph.objects.get(graphid=self.rootNode.graph_id)
        new_node = graph.add_node({"nodeid": uuid.uuid1(), "datatype": "semantic"})  # A blank node with no ontology class is specified
        graph.add_edge({"domainnode_id": self.rootNode.pk, "rangenode_id": new_node.pk, "ontologyproperty": None})

        with self.assertRaises(GraphValidationError) as cm:
            graph.save()
        the_exception = cm.exception
        self.assertEqual(the_exception.code, 1000)

    def test_graph_validation_of_invalid_ontology_class(self):
        """
        test to make sure invalid ontology classes aren't allowed

        """

        graph = Graph.objects.get(graphid=self.rootNode.graph_id)
        new_node = graph.add_node(
            {"nodeid": uuid.uuid1(), "datatype": "semantic", "ontologyclass": "InvalidOntologyClass"}
        )  # A blank node with an invalid ontology class specified
        graph.add_edge({"domainnode_id": self.rootNode.pk, "rangenode_id": new_node.pk, "ontologyproperty": None})

        with self.assertRaises(GraphValidationError) as cm:
            graph.save()
        the_exception = cm.exception
        self.assertEqual(the_exception.code, 1001)

    def test_graph_validation_of_null_ontology_property(self):
        """
        test to make sure null ontology properties aren't allowed

        """

        graph = Graph.objects.get(graphid=self.rootNode.graph_id)
        graph.append_branch(None, graphid=self.NODE_NODETYPE_GRAPHID)

        with self.assertRaises(GraphValidationError) as cm:
            graph.save()
        the_exception = cm.exception
        self.assertEqual(the_exception.code, 1002)

    def test_graph_validation_of_incorrect_ontology_property(self):
        """
        test to make sure a valid ontology property but incorrect use of the property fails

        """

        graph = Graph.objects.get(graphid=self.rootNode.graph_id)
        graph.append_branch("http://www.cidoc-crm.org/cidoc-crm/P1_is_identified_by", graphid=self.NODE_NODETYPE_GRAPHID)

        with self.assertRaises(GraphValidationError) as cm:
            graph.save()
        the_exception = cm.exception
        self.assertEqual(the_exception.code, 1003)

    def test_graph_validation_of_invalid_ontology_property(self):
        """
        test to make sure we use a valid ontology property value

        """

        graph = Graph.objects.get(graphid=self.rootNode.graph_id)
        graph.append_branch("some invalid property", graphid=self.NODE_NODETYPE_GRAPHID)

        with self.assertRaises(GraphValidationError) as cm:
            graph.save()
        the_exception = cm.exception
        self.assertEqual(the_exception.code, 1004)

    def test_graph_validation_of_branch_with_ontology_appended_to_graph_with_no_ontology(self):
        """
        test to make sure we can't append a branch with ontology defined to a graph with no ontology defined

        """

        graph = Graph.new()
        graph.name = "TEST GRAPH"
        graph.ontology = None
        graph.save()

        graph.root.name = "ROOT NODE"
        graph.root.description = "Test Root Node"
        graph.root.ontologyclass = "http://www.cidoc-crm.org/cidoc-crm/E1_CRM_Entity"
        graph.root.datatype = "semantic"
        graph.root.save()

        with self.assertRaises(GraphValidationError) as cm:
            graph.save()
        the_exception = cm.exception
        self.assertEqual(the_exception.code, 1005)

    def test_appending_a_branch_with_an_invalid_ontology_property(self):
        graph = Graph.objects.get(graphid=self.NODE_NODETYPE_GRAPHID)
        graph.append_branch("http://www.cidoc-crm.org/cidoc-crm/P43_has_dimension", graphid=self.NODE_NODETYPE_GRAPHID)

        with self.assertRaises(GraphValidationError) as cm:
            graph.save()

    def test_appending_a_branch_with_an_invalid_ontology_class(self):
        graph = Graph.new()
        graph.name = "TEST GRAPH"
        graph.subtitle = "ARCHES TEST GRAPH"
        graph.author = "Arches"
        graph.description = "ARCHES TEST GRAPH"
        graph.ontology = models.Ontology.objects.get(pk="e6e8db47-2ccf-11e6-927e-b8f6b115d7dd")
        graph.version = "v1.0.0"
        graph.isactive = False
        graph.iconclass = "fa fa-building"
        graph.nodegroups = []

        graph.root.name = "ROOT NODE"
        graph.root.description = "Test Root Node"
        graph.root.ontologyclass = "http://www.cidoc-crm.org/cidoc-crm/E21_Person"
        graph.root.datatype = "semantic"

        graph.save()

        graph.append_branch("http://www.cidoc-crm.org/cidoc-crm/P43_has_dimension", graphid=self.NODE_NODETYPE_GRAPHID)

        with self.assertRaises(GraphValidationError) as cm:
            graph.save()