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

import os
from contextlib import contextmanager, redirect_stdout
from io import StringIO
from random import choice, sample
from unittest.mock import patch

import jingrow
from jingrow.core.pagetype.pagetype.test_pagetype import new_pagetype
from jingrow.exceptions import DoesNotExistError
from jingrow.model.base_page import get_controller
from jingrow.model.rename_pg import bulk_rename, update_document_title
from jingrow.modules.utils import get_pg_path
from jingrow.tests.utils import JingrowTestCase
from jingrow.utils import add_to_date, now


@contextmanager
def patch_db(endpoints: list[str] | None = None):
	patched_endpoints = []

	for point in endpoints:
		x = patch(f"jingrow.db.{point}", new=lambda: True)
		patched_endpoints.append(x)

	savepoint = "SAVEPOINT_for_test_bulk_rename"
	jingrow.db.savepoint(save_point=savepoint)
	try:
		for x in patched_endpoints:
			x.start()
		yield
	finally:
		for x in patched_endpoints:
			x.stop()
		jingrow.db.rollback(save_point=savepoint)


class TestRenameDoc(JingrowTestCase):
	@classmethod
	def setUpClass(self):
		"""Setting Up data for the tests defined under TestRenameDoc"""
		# set developer_mode to rename pg controllers
		super().setUpClass()
		self._original_developer_flag = jingrow.conf.developer_mode
		jingrow.conf.developer_mode = 1

		# data generation: for base and merge tests
		self.available_documents = []
		self.test_pagetype = "ToDo"

		for num in range(1, 5):
			pg = jingrow.get_pg(
				{
					"pagetype": self.test_pagetype,
					"date": add_to_date(now(), days=num),
					"description": f"this is todo #{num}",
				}
			).insert()
			self.available_documents.append(pg.name)

		#  data generation: for controllers tests
		self.pagetype = jingrow._dict(
			{
				"old": "Test Rename Page Old",
				"new": "Test Rename Page New",
			}
		)

		jingrow.get_pg(
			{
				"pagetype": "PageType",
				"module": "Custom",
				"name": self.pagetype.old,
				"custom": 0,
				"fields": [{"label": "Some Field", "fieldname": "some_fieldname", "fieldtype": "Data"}],
				"permissions": [{"role": "System Manager", "read": 1}],
			}
		).insert()

	@classmethod
	def tearDownClass(self):
		"""Deleting data generated for the tests defined under TestRenameDoc"""
		# delete_pg doesnt drop tables
		# this is done to bypass inconsistencies in the db
		jingrow.delete_pg_if_exists("PageType", "Renamed Pg")
		jingrow.db.sql_ddl("drop table if exists `tabRenamed Pg`")

		# delete the documents created
		for pagename in self.available_documents:
			jingrow.delete_pg(self.test_pagetype, pagename)

		for dt in self.pagetype.values():
			if jingrow.db.exists("PageType", dt):
				jingrow.delete_pg("PageType", dt)
				jingrow.db.sql_ddl(f"DROP TABLE IF EXISTS `tab{dt}`")

		# reset original value of developer_mode conf
		jingrow.conf.developer_mode = self._original_developer_flag

	def setUp(self):
		jingrow.flags.link_fields = {}
		if self._testMethodName == "test_pg_rename_method":
			self.property_setter = jingrow.get_pg(
				{
					"pagetype": "Property Setter",
					"pagetype_or_field": "PageType",
					"pg_type": self.test_pagetype,
					"property": "allow_rename",
					"property_type": "Check",
					"value": "1",
				}
			).insert()

		super().setUp()

	def tearDown(self) -> None:
		if self._testMethodName == "test_pg_rename_method":
			self.property_setter.delete()
		return super().tearDown()

	def test_rename_pg(self):
		"""Rename an existing document via jingrow.rename_pg"""
		old_name = choice(self.available_documents)
		new_name = old_name + ".new"
		self.assertEqual(new_name, jingrow.rename_pg(self.test_pagetype, old_name, new_name, force=True))
		self.available_documents.remove(old_name)
		self.available_documents.append(new_name)

	def test_merging_pgs(self):
		"""Merge two documents via jingrow.rename_pg"""
		first_todo, second_todo = sample(self.available_documents, 2)

		second_todo_pg = jingrow.get_pg(self.test_pagetype, second_todo)
		second_todo_pg.priority = "High"
		second_todo_pg.save()

		merged_todo = jingrow.rename_pg(self.test_pagetype, first_todo, second_todo, merge=True, force=True)
		merged_todo_pg = jingrow.get_pg(self.test_pagetype, merged_todo)
		self.available_documents.remove(first_todo)

		with self.assertRaises(DoesNotExistError):
			jingrow.get_pg(self.test_pagetype, first_todo)

		self.assertEqual(merged_todo_pg.priority, second_todo_pg.priority)

	def test_rename_controllers(self):
		"""Rename pagetypes with controller code paths"""
		# check if module exists exists;
		# if custom, get_controller will return Page class
		# if not custom, a different class will be returned
		self.assertNotEqual(get_controller(self.pagetype.old), jingrow.model.page.Page)

		old_pagetype_path = get_pg_path("Custom", "PageType", self.pagetype.old)

		# rename pg via wrapper API accessible via /desk
		jingrow.rename_pg("PageType", self.pagetype.old, self.pagetype.new)

		# check if database and controllers are updated
		self.assertTrue(jingrow.db.exists("PageType", self.pagetype.new))
		self.assertFalse(jingrow.db.exists("PageType", self.pagetype.old))
		self.assertFalse(os.path.exists(old_pagetype_path))

	def test_rename_pagetype(self):
		"""Rename PageType via jingrow.rename_pg"""
		from jingrow.core.pagetype.pagetype.test_pagetype import new_pagetype

		if not jingrow.db.exists("PageType", "Rename This"):
			new_pagetype(
				"Rename This",
				fields=[
					{
						"label": "Linked To",
						"fieldname": "linked_to_pagetype",
						"fieldtype": "Link",
						"options": "PageType",
						"unique": 0,
					}
				],
			).insert()

		to_rename_record = jingrow.get_pg(
			{"pagetype": "Rename This", "linked_to_pagetype": "Rename This"}
		).insert()

		# Rename pagetype
		self.assertEqual(
			"Renamed Pg", jingrow.rename_pg("PageType", "Rename This", "Renamed Pg", force=True)
		)

		# Test if Pagetype value has changed in Link field
		linked_to_pagetype = jingrow.db.get_value("Renamed Pg", to_rename_record.name, "linked_to_pagetype")
		self.assertEqual(linked_to_pagetype, "Renamed Pg")

		# Test if there are conflicts between a record and a PageType
		# having the same name
		old_name = to_rename_record.name
		new_name = "ToDo"
		self.assertEqual(new_name, jingrow.rename_pg("Renamed Pg", old_name, new_name, force=True))

	def test_update_document_title_api(self):
		test_pagetype = "Module Def"
		test_pg = jingrow.get_pg(
			{
				"pagetype": test_pagetype,
				"module_name": f"Test-test_update_document_title_api-{jingrow.generate_hash()}",
				"custom": True,
			}
		)
		test_pg.insert(ignore_mandatory=True)

		dt = test_pg.pagetype
		dn = test_pg.name
		new_name = f"{dn}-new"

		# pass invalid types to API
		with self.assertRaises(TypeError):
			update_document_title(pagetype=dt, pagename=dn, title={}, name={"hack": "this"})

		pg_before = jingrow.get_pg(test_pagetype, dn)
		return_value = update_document_title(pagetype=dt, pagename=dn, new_name=new_name)
		pg_after = jingrow.get_pg(test_pagetype, return_value)

		pg_before_dict = pg_before.as_dict(no_nulls=True, no_default_fields=True)
		pg_after_dict = pg_after.as_dict(no_nulls=True, no_default_fields=True)
		pg_before_dict.pop("module_name")
		pg_after_dict.pop("module_name")

		self.assertEqual(new_name, return_value)
		self.assertDictEqual(pg_before_dict, pg_after_dict)
		self.assertEqual(pg_after.module_name, return_value)

		test_pg.delete()

	def test_bulk_rename(self):
		input_data = [[x, f"{x}-new"] for x in self.available_documents]

		with patch_db(["commit", "rollback"]), patch("jingrow.enqueue") as enqueue:
			message_log = bulk_rename(self.test_pagetype, input_data, via_console=False)
			self.assertEqual(len(message_log), len(self.available_documents))
			self.assertIsInstance(message_log, list)
			enqueue.assert_called_with(
				"jingrow.utils.global_search.rebuild_for_pagetype",
				pagetype=self.test_pagetype,
			)

	def test_pg_rename_method(self):
		name = choice(self.available_documents)
		new_name = f"{name}-{jingrow.generate_hash(length=4)}"
		pg = jingrow.get_pg(self.test_pagetype, name)
		pg.rename(new_name, merge=jingrow.db.exists(self.test_pagetype, new_name))
		self.assertEqual(pg.name, new_name)
		self.available_documents.append(new_name)
		self.available_documents.remove(name)

	def test_parenttype(self):
		child = new_pagetype(istable=1).insert()
		table_field = {
			"label": "Test Table",
			"fieldname": "test_table",
			"fieldtype": "Table",
			"options": child.name,
		}

		parent_a = new_pagetype(fields=[table_field], allow_rename=1, autoname="Prompt").insert()
		parent_b = new_pagetype(fields=[table_field], allow_rename=1, autoname="Prompt").insert()

		parent_a_instance = jingrow.get_pg(
			pagetype=parent_a.name, test_table=[{"some_fieldname": "x"}], name="XYZ"
		).insert()

		parent_b_instance = jingrow.get_pg(
			pagetype=parent_b.name, test_table=[{"some_fieldname": "x"}], name="XYZ"
		).insert()

		parent_b_instance.rename("ABC")
		parent_a_instance.reload()

		self.assertEqual(len(parent_a_instance.test_table), 1)
		self.assertEqual(len(parent_b_instance.test_table), 1)
