import os
import shutil
import unittest
from contextlib import contextmanager
from pathlib import Path

import frappe
from frappe import scrub
from frappe.core.doctype.doctype.test_doctype import new_doctype
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.model.meta import trim_table
from frappe.modules import export_customizations, export_module_json, get_module_path
from frappe.modules.utils import export_doc, sync_customizations
from frappe.tests import IntegrationTestCase
from frappe.utils import now_datetime


def write_file(path, content):
	with open(path, "w") as f:
		f.write(content)


def delete_file(path):
	if path:
		os.remove(path)


def delete_path(path):
	if path:
		shutil.rmtree(path, ignore_errors=True)


class TestUtils(IntegrationTestCase):
	def setUp(self):
		self._dev_mode = frappe.local.conf.developer_mode
		frappe.local.conf.developer_mode = True

	def tearDown(self):
		frappe.db.rollback()
		frappe.local.conf.developer_mode = self._dev_mode
		frappe.local.flags.pop("in_import", None)

	def test_export_module_json_no_export(self):
		frappe.local.flags.in_import = True
		doc = frappe.get_last_doc("DocType")
		self.assertIsNone(export_module_json(doc=doc, is_standard=True, module=doc.module))

	@unittest.skipUnless(
		os.access(frappe.get_app_path("frappe"), os.W_OK), "Only run if frappe app paths is writable"
	)
	def test_export_module_json(self):
		doc = frappe.get_last_doc("DocType", {"issingle": 0, "custom": 0})
		export_doc_path = os.path.join(
			get_module_path(doc.module),
			scrub(doc.doctype),
			scrub(doc.name),
			f"{scrub(doc.name)}.json",
		)
		with open(export_doc_path) as f:
			export_doc_before = frappe.parse_json(f.read())

		last_modified_before = os.path.getmtime(export_doc_path)
		self.addCleanup(write_file, path=export_doc_path, content=frappe.as_json(export_doc_before))

		frappe.flags.in_import = False
		frappe.conf.developer_mode = True
		export_path = export_module_json(doc=doc, is_standard=True, module=doc.module)

		last_modified_after = os.path.getmtime(export_doc_path)

		with open(f"{export_path}.json") as f:
			frappe.parse_json(f.read())  # export_doc_after

		self.assertTrue(last_modified_after > last_modified_before)

	@unittest.skipUnless(
		os.access(frappe.get_app_path("frappe"), os.W_OK), "Only run if frappe app paths is writable"
	)
	def test_export_customizations(self):
		with note_customizations():
			file_path = export_customizations(module="Custom", doctype="Note")
			self.addCleanup(delete_file, path=file_path)
			self.assertTrue(file_path.endswith("/custom/custom/note.json"))
			self.assertTrue(os.path.exists(file_path))

	@unittest.skipUnless(
		os.access(frappe.get_app_path("frappe"), os.W_OK), "Only run if frappe app paths is writable"
	)
	def test_export_customizations_with_module_filter(self):
		# create two customizations, one matching the module, one under a different module
		with note_customizations() as (custom_field, property_setter, _doctype_link):
			custom_field.db_set("module", "Custom")
			property_setter.db_set("module", "Custom")

			# create module def called OtherModule
			other_module = frappe.new_doc("Module Def")

			other_module.update({"module_name": "OtherModule", "app_name": "frappe"})
			other_module.save(ignore_permissions=True)
			self.addCleanup(other_module.delete)

			# create a customization belonging to another module (should be excluded)
			other_cf = create_custom_field(
				"Note",
				df={
					"fieldname": "other_mod_field",
					"label": "Other Mod Field",
					"fieldtype": "Data",
					"module": "OtherModule",
				},
			)

			self.addCleanup(other_cf.delete)

			file_path = export_customizations(
				module="Custom",
				doctype="Note",
				apply_module_export_filter=True,
			)
			self.addCleanup(delete_file, path=file_path)

			self.assertTrue(os.path.exists(file_path))

			with open(file_path) as f:
				exported = frappe.parse_json(f.read())

			exported_fields = {f["fieldname"] for f in exported.get("custom_fields", [])}

			self.assertIn("test_export_customizations_field", exported_fields)

			self.assertNotIn("other_mod_field", exported_fields)

	@unittest.skipUnless(
		os.access(frappe.get_app_path("frappe"), os.W_OK), "Only run if frappe app paths is writable"
	)
	def test_sync_customizations(self):
		with note_customizations() as (custom_field, property_setter, doctype_link):
			file_path = export_customizations(module="Custom", doctype="Note", sync_on_migrate=True)
			custom_field.db_set("modified", now_datetime())
			custom_field.reload()

			# Untracked property setter
			custom_prop_setter = make_property_setter(
				"Note", fieldname="content", property="bold", value="1", property_type="Check"
			)

			self.assertTrue(file_path.endswith("/custom/custom/note.json"))
			self.assertTrue(os.path.exists(file_path))
			last_modified_before = custom_field.modified

			sync_customizations(app="frappe")
			self.assertTrue(property_setter.doctype, property_setter.name)
			self.assertTrue(custom_prop_setter.doctype, custom_prop_setter.name)
			self.assertTrue(doctype_link.doctype, doctype_link.name)

			self.assertTrue(file_path.endswith("/custom/custom/note.json"))
			self.assertTrue(os.path.exists(file_path))
			custom_field.reload()
			last_modified_after = custom_field.modified

			self.assertNotEqual(last_modified_after, last_modified_before)
			self.addCleanup(delete_file, path=file_path)

	def test_reload_doc(self):
		frappe.db.set_value("DocType", "Note", "migration_hash", "", update_modified=False)
		self.assertFalse(frappe.db.get_value("DocType", "Note", "migration_hash"))
		frappe.db.set_value(
			"DocField",
			{"parent": "Note", "fieldname": "title"},
			"fieldtype",
			"Text",
			update_modified=False,
		)
		self.assertEqual(
			frappe.db.get_value("DocField", {"parent": "Note", "fieldname": "title"}, "fieldtype"),
			"Text",
		)
		frappe.reload_doctype("Note")
		self.assertEqual(
			frappe.db.get_value("DocField", {"parent": "Note", "fieldname": "title"}, "fieldtype"),
			"Data",
		)
		self.assertTrue(frappe.db.get_value("DocType", "Note", "migration_hash"))

	@unittest.skipUnless(
		os.access(frappe.get_app_path("frappe"), os.W_OK), "Only run if frappe app paths is writable"
	)
	def test_export_doc(self):
		note = frappe.new_doc("Note")
		note.title = frappe.generate_hash(length=10)
		note.save()
		export_doc(doctype="Note", name=note.name)
		exported_doc_path = Path(
			frappe.get_app_path("frappe", "desk", "note", note.name, f"{note.name}.json")
		)
		self.assertTrue(os.path.exists(exported_doc_path))
		self.addCleanup(delete_path, path=exported_doc_path.parent.parent)

	@unittest.skipUnless(
		os.access(frappe.get_app_path("frappe"), os.W_OK), "Only run if frappe app paths is writable"
	)
	def test_make_boilerplate(self):
		with temp_doctype() as doctype:
			scrubbed = frappe.scrub(doctype.name)
			path = frappe.get_app_path("frappe", "core", "doctype", scrubbed, f"{scrubbed}.json")
			self.assertFalse(os.path.exists(path))
			doctype.custom = False
			doctype.save()
			self.assertTrue(os.path.exists(path))


@contextmanager
def temp_doctype():
	try:
		doctype = new_doctype().insert()
		yield doctype
	finally:
		doctype.delete(force=True)
		frappe.db.sql_ddl(f"DROP TABLE `tab{doctype.name}`")


@contextmanager
def note_customizations():
	try:
		df = {
			"fieldname": "test_export_customizations_field",
			"label": "Custom Data Field",
			"fieldtype": "Data",
		}
		custom_field = create_custom_field("Note", df=df)

		property_setter = make_property_setter(
			"Note", fieldname="content", property="bold", value="1", property_type="Check"
		)

		doctype_link = frappe.get_doc(
			{
				"doctype": "DocType Link",
				"parent": "Note",
				"parenttype": "DocType",
				"parentfield": "links",
				"link_doctype": "User",
				"link_fieldname": "owner",
				"group": "Test Group",
			}
		).insert()

		yield custom_field, property_setter, doctype_link
	finally:
		custom_field.delete()
		property_setter.delete()
		doctype_link.delete()
		trim_table("Note", dry_run=False)
		delete_path(frappe.get_module_path("Desk", "Note"))
