# Copyright (c) 2022, JINGROW and Contributors
# License: MIT. See LICENSE

from unittest.mock import patch

import jingrow
from jingrow.core.pagetype.pagetype.test_pagetype import new_pagetype
from jingrow.query_builder import Field
from jingrow.query_builder.functions import Max
from jingrow.tests.utils import JingrowTestCase
from jingrow.utils import random_string
from jingrow.utils.nestedset import (
	NestedSetChildExistsError,
	NestedSetInvalidMergeError,
	NestedSetRecursionError,
	get_descendants_of,
	rebuild_tree,
	remove_subtree,
)

records = [
	{
		"some_fieldname": "Root Node",
		"parent_test_tree_pagetype": None,
		"is_group": 1,
	},
	{
		"some_fieldname": "Parent 1",
		"parent_test_tree_pagetype": "Root Node",
		"is_group": 1,
	},
	{
		"some_fieldname": "Parent 2",
		"parent_test_tree_pagetype": "Root Node",
		"is_group": 1,
	},
	{
		"some_fieldname": "Child 1",
		"parent_test_tree_pagetype": "Parent 1",
		"is_group": 0,
	},
	{
		"some_fieldname": "Child 2",
		"parent_test_tree_pagetype": "Parent 1",
		"is_group": 0,
	},
	{
		"some_fieldname": "Child 3",
		"parent_test_tree_pagetype": "Parent 2",
		"is_group": 0,
	},
]

TEST_PAGETYPE = "Test Tree PageType"


class NestedSetTestUtil:
	def setup_test_pagetype(self):
		jingrow.db.delete("PageType", TEST_PAGETYPE)
		jingrow.db.sql_ddl(f"drop table if exists `tab{TEST_PAGETYPE}`")

		self.tree_pagetype = new_pagetype(TEST_PAGETYPE, is_tree=True, autoname="field:some_fieldname")
		self.tree_pagetype.insert()

		for record in records:
			d = jingrow.new_pg(TEST_PAGETYPE)
			d.update(record)
			d.insert()

	def teardown_test_pagetype(self):
		self.tree_pagetype.delete()
		jingrow.db.sql_ddl(f"drop table if exists `{TEST_PAGETYPE}`")

	def move_it_back(self):
		parent_1 = jingrow.get_pg(TEST_PAGETYPE, "Parent 1")
		parent_1.parent_test_tree_pagetype = "Root Node"
		parent_1.save()

	def get_no_of_children(self, record_name: str) -> int:
		if not record_name:
			return jingrow.db.count(TEST_PAGETYPE)
		return len(get_descendants_of(TEST_PAGETYPE, record_name, ignore_permissions=True))


class TestNestedSet(JingrowTestCase):
	@classmethod
	def setUpClass(cls) -> None:
		cls.nsu = NestedSetTestUtil()
		cls.nsu.setup_test_pagetype()
		super().setUpClass()

	@classmethod
	def tearDownClass(cls) -> None:
		cls.nsu.teardown_test_pagetype()
		super().tearDownClass()

	def setUp(self) -> None:
		jingrow.db.rollback()

	def test_basic_tree(self):
		global records

		min_lft = 1
		max_rgt = jingrow.qb.from_(TEST_PAGETYPE).select(Max(Field("rgt"))).run(pluck=True)[0]

		for record in records:
			lft, rgt, parent_test_tree_pagetype = jingrow.db.get_value(
				TEST_PAGETYPE,
				record["some_fieldname"],
				["lft", "rgt", "parent_test_tree_pagetype"],
			)

			if parent_test_tree_pagetype:
				parent_lft, parent_rgt = jingrow.db.get_value(
					TEST_PAGETYPE, parent_test_tree_pagetype, ["lft", "rgt"]
				)
			else:
				# root
				parent_lft = min_lft - 1
				parent_rgt = max_rgt + 1

			self.assertTrue(lft)
			self.assertTrue(rgt)
			self.assertTrue(lft < rgt)
			self.assertTrue(parent_lft < parent_rgt)
			self.assertTrue(lft > parent_lft)
			self.assertTrue(rgt < parent_rgt)
			self.assertTrue(lft >= min_lft)
			self.assertTrue(rgt <= max_rgt)

			no_of_children = self.nsu.get_no_of_children(record["some_fieldname"])
			self.assertTrue(
				rgt == (lft + 1 + (2 * no_of_children)),
				msg=(record, no_of_children, self.nsu.get_no_of_children(record["some_fieldname"])),
			)

			no_of_children = self.nsu.get_no_of_children(parent_test_tree_pagetype)
			self.assertTrue(parent_rgt == (parent_lft + 1 + (2 * no_of_children)))

	def test_recursion(self):
		leaf_node = jingrow.get_pg(TEST_PAGETYPE, {"some_fieldname": "Parent 2"})
		leaf_node.parent_test_tree_pagetype = "Child 3"
		self.assertRaises(NestedSetRecursionError, leaf_node.save)
		leaf_node.reload()

	def test_rebuild_tree(self):
		rebuild_tree(TEST_PAGETYPE, "parent_test_tree_pagetype")
		self.test_basic_tree()

	def test_move_group_into_another(self):
		old_lft, old_rgt = jingrow.db.get_value(TEST_PAGETYPE, "Parent 2", ["lft", "rgt"])

		parent_1 = jingrow.get_pg(TEST_PAGETYPE, "Parent 1")
		lft, rgt = parent_1.lft, parent_1.rgt

		parent_1.parent_test_tree_pagetype = "Parent 2"
		parent_1.save()
		self.test_basic_tree()

		# after move
		new_lft, new_rgt = jingrow.db.get_value(TEST_PAGETYPE, "Parent 2", ["lft", "rgt"])

		# lft should reduce
		self.assertEqual(old_lft - new_lft, rgt - lft + 1)

		# adjacent siblings, hence rgt diff will be 0
		self.assertEqual(new_rgt - old_rgt, 0)

		self.nsu.move_it_back()
		self.test_basic_tree()

	def test_move_leaf_into_another_group(self):
		child_2 = jingrow.get_pg(TEST_PAGETYPE, "Child 2")

		# assert that child 2 is not already under parent 1
		parent_lft_old, parent_rgt_old = jingrow.db.get_value(TEST_PAGETYPE, "Parent 2", ["lft", "rgt"])
		self.assertTrue((parent_lft_old > child_2.lft) and (parent_rgt_old > child_2.rgt))

		child_2.parent_test_tree_pagetype = "Parent 2"
		child_2.save()
		self.test_basic_tree()

		# assert that child 2 is under parent 1
		parent_lft_new, parent_rgt_new = jingrow.db.get_value(TEST_PAGETYPE, "Parent 2", ["lft", "rgt"])
		self.assertFalse((parent_lft_new > child_2.lft) and (parent_rgt_new > child_2.rgt))

	def test_delete_leaf(self):
		global records
		el = {"some_fieldname": "Child 1", "parent_test_tree_pagetype": "Parent 1", "is_group": 0}

		child_1 = jingrow.get_pg(TEST_PAGETYPE, "Child 1")
		child_1.delete()
		records.remove(el)

		self.test_basic_tree()

		n = jingrow.new_pg(TEST_PAGETYPE)
		n.update(el)
		n.insert()
		records.append(el)

		self.test_basic_tree()

	def test_delete_group(self):
		# cannot delete group with child, but can delete leaf
		with self.assertRaises(NestedSetChildExistsError):
			jingrow.delete_pg(TEST_PAGETYPE, "Parent 1")

	def test_remove_subtree(self):
		remove_subtree(TEST_PAGETYPE, "Parent 2")
		self.test_basic_tree()

	def test_rename_nestedset(self):
		pagetype = new_pagetype(is_tree=True).insert()

		# Rename pagetype
		jingrow.rename_pg("PageType", pagetype.name, "Test " + random_string(10), force=True)

	def test_merge_groups(self):
		global records
		el = {"some_fieldname": "Parent 2", "parent_test_tree_pagetype": "Root Node", "is_group": 1}
		jingrow.rename_pg(TEST_PAGETYPE, "Parent 2", "Parent 1", merge=True)
		records.remove(el)
		self.test_basic_tree()

	def test_merge_leaves(self):
		global records
		el = {"some_fieldname": "Child 3", "parent_test_tree_pagetype": "Parent 2", "is_group": 0}

		jingrow.rename_pg(
			TEST_PAGETYPE,
			"Child 3",
			"Child 2",
			merge=True,
		)
		records.remove(el)
		self.test_basic_tree()

	def test_merge_leaf_into_group(self):
		with self.assertRaises(NestedSetInvalidMergeError):
			jingrow.rename_pg(TEST_PAGETYPE, "Child 1", "Parent 1", merge=True)

	def test_merge_group_into_leaf(self):
		with self.assertRaises(NestedSetInvalidMergeError):
			jingrow.rename_pg(TEST_PAGETYPE, "Parent 1", "Child 1", merge=True)

	def test_root_deletion(self):
		for pg in ["Child 3", "Child 2", "Child 1", "Parent 2", "Parent 1"]:
			jingrow.delete_pg(TEST_PAGETYPE, pg)

		root_node = jingrow.get_pg(TEST_PAGETYPE, "Root Node")

		# root deletion with allow_root_deletion
		# patched as delete_pg create a new instance of Root Node (using get_pg)
		root_node.allow_root_deletion = False
		with patch("jingrow.get_pg", return_value=root_node):
			with self.assertRaises(jingrow.ValidationError):
				root_node.delete()

		# root deletion without allow_root_deletion
		root_node.delete()
		self.assertFalse(jingrow.db.exists(TEST_PAGETYPE, "Root Node"))

	def test_desc_filters(self):
		linked_pagetype = (
			new_pagetype(
				fields=[
					{
						"fieldname": "link_field",
						"fieldtype": "Link",
						"options": TEST_PAGETYPE,
					}
				]
			)
			.insert()
			.name
		)

		record = "Child 1"

		exclusive_filter = {"name": ("descendants of", record)}
		inclusive_filter = {"name": ("descendants of (inclusive)", record)}
		exclusive_link = {"link_field": ("descendants of", record)}
		inclusive_link = {"link_field": ("descendants of (inclusive)", record)}

		# db_query
		self.assertNotIn(record, jingrow.get_all(TEST_PAGETYPE, exclusive_filter, run=0))
		self.assertIn(record, jingrow.get_all(TEST_PAGETYPE, inclusive_filter, run=0))
		self.assertNotIn(record, jingrow.get_all(linked_pagetype, exclusive_link, run=0))
		self.assertIn(record, jingrow.get_all(linked_pagetype, inclusive_link, run=0))

		# QB
		self.assertNotIn(record, str(jingrow.qb.get_query(TEST_PAGETYPE, filters=exclusive_filter)))
		self.assertIn(record, str(jingrow.qb.get_query(TEST_PAGETYPE, filters=inclusive_filter)))

		self.assertNotIn(record, str(jingrow.qb.get_query(table=linked_pagetype, filters=exclusive_link)))
		self.assertIn(record, str(jingrow.qb.get_query(table=linked_pagetype, filters=inclusive_link)))
