from collections import ChainMap
from copy import deepcopy

from unittest import mock, TestCase

from arches.app.models import models
from arches.app.models.tile import Tile
from arches.app.models.resource import Resource
from arches.app.utils.label_based_graph import (
    LabelBasedGraph,
    LabelBasedNode,
    NODE_ID_KEY,
    TILE_ID_KEY,
    VALUE_KEY,
    NON_DATA_COLLECTING_NODE,
)

# these tests can be run from the command line via
# python manage.py test tests.utils.label_based_graph_test --settings="tests.test_settings"


class LabelBasedNodeTests(TestCase):
    @classmethod
    def setUp(cls):
        cls.test_node = LabelBasedNode(
            name="test_node_name",
            node_id="test_node_node_id",
            tile_id="test_node_tile_id",
            value="test_node_value",
        )

        cls.test_node_json_data = {
            NODE_ID_KEY: cls.test_node.node_id,
            TILE_ID_KEY: cls.test_node.tile_id,
            VALUE_KEY: cls.test_node.value,
        }

        cls.empty_node = LabelBasedNode(
            name="empty_node_name",
            node_id="empty_node_node_id",
            tile_id="empty_node_tile_id",
            value=None,
        )

        cls.child_node_1 = LabelBasedNode(
            name="child_node_val",
            node_id="child_node_node_id",
            tile_id="child_node_tile_id",
            value="child_node_value",
        )

        cls.child_node_2 = LabelBasedNode(
            name="child_node_val",
            node_id="child_node_node_id",
            tile_id="child_node_tile_id",
            value="child_node_value",
        )

        cls.child_node_3 = LabelBasedNode(
            name="child_node_val",
            node_id="child_node_node_id",
            tile_id="child_node_tile_id",
            value="child_node_value",
        )

    def test_is_empty_with_node_with_child_nodes(self):
        self.empty_node.child_nodes.append(self.test_node)
        self.assertFalse(self.empty_node.is_empty())

    def test_is_node_empty_with_empty_node(self):
        self.assertTrue(self.empty_node.is_empty())

    def test_as_json(self):
        self.assertEqual(
            self.test_node.as_json(), {self.test_node.name: self.test_node_json_data}
        )

    def test_as_json_compact(self):
        self.assertEqual(
            self.test_node.as_json(compact=True),
            {self.test_node.name: self.test_node.value},
        )

    def test_as_json_compact_data_collecting_node_with_child(self):
        self.test_node.child_nodes.append(self.child_node_1)

        self.assertEqual(
            self.test_node.as_json(compact=True),
            {
                self.test_node.name: {
                    self.child_node_1.name: self.child_node_1.value,
                    VALUE_KEY: self.test_node.value,
                }
            },
        )

    def test_as_json_single_child_node(self):
        self.test_node.child_nodes.append(self.child_node_1)

        self.test_node_json_data[self.child_node_1.name] = {
            NODE_ID_KEY: self.child_node_1.node_id,
            TILE_ID_KEY: self.child_node_1.tile_id,
            VALUE_KEY: self.child_node_1.value,
        }

        self.assertEqual(
            self.test_node.as_json(), {self.test_node.name: self.test_node_json_data}
        )

    def test_as_json_two_child_nodes(self):
        self.test_node.child_nodes.append(self.child_node_1)
        self.test_node.child_nodes.append(self.child_node_2)

        self.test_node_json_data[self.child_node_1.name] = [
            {
                NODE_ID_KEY: self.child_node_1.node_id,
                TILE_ID_KEY: self.child_node_1.tile_id,
                VALUE_KEY: self.child_node_1.value,
            },
            {
                NODE_ID_KEY: self.child_node_2.node_id,
                TILE_ID_KEY: self.child_node_2.tile_id,
                VALUE_KEY: self.child_node_2.value,
            },
        ]

        self.assertEqual(
            self.test_node.as_json(), {self.test_node.name: self.test_node_json_data}
        )

    def test_as_json_many_child_nodes(self):
        self.test_node.child_nodes.append(self.child_node_1)
        self.test_node.child_nodes.append(self.child_node_2)
        self.test_node.child_nodes.append(self.child_node_3)

        self.test_node_json_data[self.child_node_1.name] = [
            {
                NODE_ID_KEY: self.child_node_1.node_id,
                TILE_ID_KEY: self.child_node_1.tile_id,
                VALUE_KEY: self.child_node_1.value,
            },
            {
                NODE_ID_KEY: self.child_node_2.node_id,
                TILE_ID_KEY: self.child_node_2.tile_id,
                VALUE_KEY: self.child_node_2.value,
            },
            {
                NODE_ID_KEY: self.child_node_3.node_id,
                TILE_ID_KEY: self.child_node_3.tile_id,
                VALUE_KEY: self.child_node_3.value,
            },
        ]

        self.assertEqual(
            self.test_node.as_json(), {self.test_node.name: self.test_node_json_data}
        )


class LabelBasedGraphTests(TestCase):
    @classmethod
    def setUp(cls):
        cls.node_1 = LabelBasedNode(
            name="node_1_val",
            node_id="node_1_node_id",
            tile_id="node_1_tile_id",
            value="node_1_value",
        )
        cls.node_2 = LabelBasedNode(
            name="node_2_val",
            node_id="node_2_node_id",
            tile_id="node_2_tile_id",
            value=None,
        )

    @mock.patch("arches.app.utils.label_based_graph.models.NodeGroup")
    def test_generate_node_ids_to_tiles_reference_and_nodegroup_cardinality_reference(
        self, mock_NodeGroup
    ):
        mock_tile = mock.Mock(
            data={self.node_1.node_id: "test_val"}, nodegroup_id=self.node_1.node_id
        )
        mock_cardinality = "1"

        mock_NodeGroup.objects.filter.return_value.values.return_value = [
            {"nodegroupid": mock_tile.nodegroup_id, "cardinality": mock_cardinality}
        ]

        (
            node_ids_to_tiles_reference,
            nodegroup_cardinality_reference,
        ) = LabelBasedGraph.generate_node_ids_to_tiles_reference_and_nodegroup_cardinality_reference(
            resource=mock.Mock(tiles=[mock_tile])
        )

        self.assertEqual(
            mock_tile, node_ids_to_tiles_reference.get(self.node_1.node_id)[0]
        )
        self.assertEqual(
            nodegroup_cardinality_reference, {mock_tile.nodegroup_id: mock_cardinality}
        )

    @mock.patch.object(LabelBasedGraph, "_build_graph", side_effect=None)
    def test_from_tile(self, mock__build_graph):
        with mock.patch(
            "arches.app.utils.label_based_graph.models.Node", return_value=None
        ):
            LabelBasedGraph.from_tile(
                tile=mock.Mock(nodegroup_id=1), node_ids_to_tiles_reference=mock.Mock()
            )
            mock__build_graph.assert_called_once()


@mock.patch("arches.app.utils.label_based_graph.models.NodeGroup")
@mock.patch("arches.app.utils.label_based_graph.models.Node")
class LabelBasedGraph_FromResourceTests(TestCase):
    @classmethod
    def setUp(cls):
        cls.parent_nodegroup = models.NodeGroup()
        cls.nodegroup = models.NodeGroup(parentnodegroup=cls.parent_nodegroup)
        cls.grouping_node = models.Node(
            datatype="semantic", name="Test Node Grouping", nodegroup=cls.nodegroup
        )
        cls.string_node = models.Node(
            datatype="string", name="Test Node", nodegroup=cls.nodegroup
        )
        cls.grouping_tile = models.TileModel(
            data={}, nodegroup_id=str(cls.grouping_node.pk)
        )
        cls.string_tile = models.TileModel(
            data={
                str(cls.string_node.pk): {
                    "en": {"value": "value_1", "direction": "ltr"}
                }
            },
            nodegroup_id=str(cls.string_node.pk),
        )
        cls.hidden_card = models.CardModel(nodegroup=cls.nodegroup, visible=False)

        # let's mock Resource since it's minimally used
        # and complex to get `displayname`
        cls.test_resource = mock.Mock(displayname="Test Resource", tiles=[])

    def test_smoke(self, mock_Node, mock_NodeGroup):
        label_based_graph = LabelBasedGraph.from_resource(
            resource=self.test_resource, compact=False, hide_empty_nodes=False
        )

        self.assertEqual(label_based_graph, {})

    def test_handles_node_with_single_value(self, mock_Node, mock_NodeGroup):
        mock_Node.objects.get.return_value = self.string_node
        mock_NodeGroup.objects.filter.return_value.values.return_value = [
            {"nodegroupid": self.string_tile.nodegroup_id, "cardinality": "1"}
        ]

        self.test_resource.tiles.append(self.string_tile)

        label_based_graph = LabelBasedGraph.from_resource(
            resource=self.test_resource, compact=False, hide_empty_nodes=False
        )

        self.assertEqual(
            label_based_graph,
            {
                self.string_node.name: {
                    NODE_ID_KEY: str(self.string_node.pk),
                    TILE_ID_KEY: str(self.string_tile.pk),
                    VALUE_KEY: self.string_tile.data[str(self.string_node.pk)]["en"][
                        "value"
                    ],
                },
            },
        )

    def test_handles_node_with_multiple_values(self, mock_Node, mock_NodeGroup):
        mock_Node.objects.get.return_value = self.string_node
        mock_NodeGroup.objects.filter.return_value.values.return_value = [
            {"nodegroupid": self.string_tile.nodegroup_id, "cardinality": "1"}
        ]

        duplicate_node_tile = models.TileModel(
            data={
                str(self.string_node.pk): {
                    "en": {"value": "value_2", "direction": "ltr"}
                }
            },
            nodegroup_id=str(self.string_node.pk),
        )

        self.test_resource.tiles.append(self.string_tile)
        self.test_resource.tiles.append(duplicate_node_tile)

        label_based_graph = LabelBasedGraph.from_resource(
            resource=self.test_resource, compact=False, hide_empty_nodes=False
        )

        self.assertEqual(
            label_based_graph,
            {
                self.string_node.name: [
                    {
                        NODE_ID_KEY: str(self.string_node.pk),
                        TILE_ID_KEY: str(self.string_tile.pk),
                        VALUE_KEY: self.string_tile.data[str(self.string_node.pk)][
                            "en"
                        ]["value"],
                    },
                    {
                        NODE_ID_KEY: str(self.string_node.pk),
                        TILE_ID_KEY: str(duplicate_node_tile.pk),
                        VALUE_KEY: duplicate_node_tile.data[str(self.string_node.pk)][
                            "en"
                        ]["value"],
                    },
                ],
            },
        )

    def test_semantic_node_with_child(self, mock_Node, mock_NodeGroup):
        mock_Node.objects.get.return_value = self.grouping_node
        mock_NodeGroup.objects.filter.return_value.values.return_value = [
            {"nodegroupid": self.grouping_tile.nodegroup_id, "cardinality": "1"}
        ]

        self.grouping_node.get_direct_child_nodes = mock.Mock(
            return_value=[self.string_node]
        )

        self.grouping_tile.data = {
            str(self.string_node.pk): {"en": {"value": "value_2", "direction": "ltr"}}
        }
        self.test_resource.tiles.append(self.grouping_tile)
        label_based_graph = LabelBasedGraph.from_resource(
            resource=self.test_resource, compact=False, hide_empty_nodes=False
        )

        self.assertEqual(
            label_based_graph,
            {
                self.grouping_node.name: {
                    NODE_ID_KEY: str(self.grouping_node.pk),
                    TILE_ID_KEY: str(self.grouping_tile.pk),
                    VALUE_KEY: NON_DATA_COLLECTING_NODE,
                    self.string_node.name: {
                        NODE_ID_KEY: str(self.string_node.pk),
                        TILE_ID_KEY: str(self.grouping_tile.pk),
                        VALUE_KEY: self.grouping_tile.data[str(self.string_node.pk)][
                            "en"
                        ]["value"],
                    },
                },
            },
        )

    def test_handles_node_grouped_in_separate_card(self, mock_Node, mock_NodeGroup):
        mock_Node.objects.get.side_effect = [self.grouping_node, self.string_node]
        mock_NodeGroup.objects.filter.return_value.values.return_value = [
            {"nodegroupid": self.grouping_tile.nodegroup_id, "cardinality": "1"},
            {"nodegroupid": self.string_tile.nodegroup_id, "cardinality": "1"},
        ]

        self.grouping_node.get_direct_child_nodes = mock.Mock(
            return_value=[self.string_node]
        )

        self.string_tile.parenttile = self.grouping_tile

        self.test_resource.tiles.append(self.grouping_tile)
        self.test_resource.tiles.append(self.string_tile)

        label_based_graph = LabelBasedGraph.from_resource(
            resource=self.test_resource, compact=False, hide_empty_nodes=False
        )

        self.assertEqual(
            label_based_graph,
            {
                self.grouping_node.name: {
                    NODE_ID_KEY: str(self.grouping_node.pk),
                    TILE_ID_KEY: str(self.grouping_tile.pk),
                    VALUE_KEY: NON_DATA_COLLECTING_NODE,
                    self.string_node.name: {
                        NODE_ID_KEY: str(self.string_node.pk),
                        TILE_ID_KEY: str(self.string_tile.pk),
                        VALUE_KEY: self.string_tile.data[str(self.string_node.pk)][
                            "en"
                        ]["value"],
                    },
                },
            },
        )

    def test_handles_node_grouped_in_separate_card_with_cardinality_n(
        self, mock_Node, mock_NodeGroup
    ):
        mock_Node.objects.get.side_effect = [self.grouping_node, self.string_node]
        mock_NodeGroup.objects.filter.return_value.values.return_value = [
            {"nodegroupid": self.grouping_tile.nodegroup_id, "cardinality": "1"},
            {"nodegroupid": self.string_tile.nodegroup_id, "cardinality": "n"},
        ]

        self.grouping_node.get_direct_child_nodes = mock.Mock(
            return_value=[self.string_node]
        )

        self.string_tile.parenttile = self.grouping_tile

        self.test_resource.tiles.append(self.grouping_tile)
        self.test_resource.tiles.append(self.string_tile)

        label_based_graph = LabelBasedGraph.from_resource(
            resource=self.test_resource, compact=False, hide_empty_nodes=False
        )

        self.assertEqual(
            label_based_graph,
            {
                self.grouping_node.name: {
                    NODE_ID_KEY: str(self.grouping_node.pk),
                    TILE_ID_KEY: str(self.grouping_tile.pk),
                    VALUE_KEY: NON_DATA_COLLECTING_NODE,
                    self.string_node.name: [
                        {
                            NODE_ID_KEY: str(self.string_node.pk),
                            TILE_ID_KEY: str(self.string_tile.pk),
                            VALUE_KEY: self.string_tile.data[str(self.string_node.pk)][
                                "en"
                            ]["value"],
                        }
                    ],
                },
            },
        )

    def test_handles_empty_node_grouped_in_separate_card_with_cardinality_n(
        self, mock_Node, mock_NodeGroup
    ):
        mock_Node.objects.get.side_effect = [self.grouping_node, self.string_node]
        mock_NodeGroup.objects.filter.return_value.values.return_value = [
            {"nodegroupid": self.grouping_tile.nodegroup_id, "cardinality": "1"},
            {"nodegroupid": self.string_tile.nodegroup_id, "cardinality": "n"},
        ]

        self.grouping_node.get_direct_child_nodes = mock.Mock(
            return_value=[self.string_node]
        )

        self.string_tile.parenttile = self.grouping_tile

        self.test_resource.tiles.append(self.grouping_tile)

        label_based_graph = LabelBasedGraph.from_resource(
            resource=self.test_resource, compact=False, hide_empty_nodes=False
        )

        self.assertEqual(
            label_based_graph,
            {},
        )

    @mock.patch("arches.app.utils.label_based_graph.models.CardModel")
    def test_handle_hidden_nodes(self, mock_CardModel, mock_Node, mock_NodeGroup):
        filter_mock = mock.MagicMock()

        def filter_side_effect(nodegroup_id=None):
            if nodegroup_id:
                return filter_mock
            return mock.MagicMock()

        mock_CardModel.objects.filter.side_effect = filter_side_effect
        filter_mock.first.return_value = self.hidden_card
        mock_Node.objects.get.return_value = self.string_node
        mock_NodeGroup.objects.filter.return_value.values.return_value = [
            {"nodegroupid": self.string_tile.nodegroup_id, "cardinality": "1"}
        ]

        self.test_resource.tiles.append(self.string_tile)

        label_based_graph = LabelBasedGraph.from_resource(
            self.test_resource,
            compact=False,
            hide_empty_nodes=False,
            hide_hidden_nodes=True,
        )

        self.assertEqual(label_based_graph, {})
