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

import time
import unittest

from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_full_jitter

import jingrow
from jingrow.core.pagetype.pagetype.test_pagetype import new_pagetype
from jingrow.model.naming import (
	InvalidNamingSeriesError,
	NamingSeries,
	append_number_if_name_exists,
	determine_consecutive_week_number,
	getseries,
	make_autoname,
	parse_naming_series,
	revert_series_if_last,
)
from jingrow.query_builder.utils import db_type_is
from jingrow.tests.test_query_builder import run_only_if
from jingrow.tests.utils import JingrowTestCase, patch_hooks
from jingrow.utils import now_datetime, nowdate, nowtime


class TestNaming(JingrowTestCase):
	def setUp(self):
		jingrow.db.delete("Note")

	def tearDown(self):
		jingrow.db.rollback()

	def test_append_number_if_name_exists(self):
		"""
		Append number to name based on existing values
		if Bottle exists
		        Bottle -> Bottle-1
		if Bottle-1 exists
		        Bottle -> Bottle-2
		"""
		TITLE = "Bottle"
		PAGETYPE = "Note"

		note = jingrow.get_pg({"pagetype": PAGETYPE, "title": TITLE}).insert()

		self.assertEqual(append_number_if_name_exists(PAGETYPE, note.name), f"{note.name}-1")
		self.assertEqual(append_number_if_name_exists(PAGETYPE, TITLE, "title", "_"), f"{TITLE}_1")

	def test_field_autoname_name_sync(self):
		country = jingrow.get_last_pg("Country")
		original_name = country.name
		country.country_name = "Not a country"
		country.save()
		country.reload()

		self.assertEqual(country.name, original_name)
		self.assertEqual(country.name, country.country_name)

	def test_child_table_naming(self):
		child_dt_with_naming = new_pagetype(istable=1, autoname="field:some_fieldname").insert()
		dt_with_child_autoname = new_pagetype(
			fields=[
				{
					"label": "table with naming",
					"fieldname": "table_with_naming",
					"options": child_dt_with_naming.name,
					"fieldtype": "Table",
				}
			],
		).insert()

		name = jingrow.generate_hash(length=10)

		pg = jingrow.new_pg(dt_with_child_autoname.name)
		pg.append("table_with_naming", {"some_fieldname": name})
		pg.save()
		self.assertEqual(pg.table_with_naming[0].name, name)

		# change autoname field
		pg.table_with_naming[0].some_fieldname = "Something else"
		pg.save()

		self.assertEqual(pg.table_with_naming[0].name, name)
		self.assertEqual(pg.table_with_naming[0].some_fieldname, name)

		pg.delete()
		dt_with_child_autoname.delete()
		child_dt_with_naming.delete()

	def test_format_autoname(self):
		"""
		Test if braced params are replaced in format autoname
		"""
		pagetype = new_pagetype(autoname="format:TODO-{MM}-{some_fieldname}-{##}").insert()

		description = "Format"

		pg = jingrow.new_pg(pagetype.name)
		pg.some_fieldname = description
		pg.insert()

		series = getseries("", 2)
		series = int(series) - 1

		self.assertEqual(pg.name, f"TODO-{now_datetime().strftime('%m')}-{description}-{series:02}")

	def test_format_autoname_for_datetime_field(self):
		"""Test if datetime, date and time objects get converted to strings for naming."""
		pagetype = new_pagetype(autoname="format:TODO-{field}-{##}").insert()

		for field in [now_datetime(), nowdate(), nowtime()]:
			pg = jingrow.new_pg(pagetype.name)
			pg.field = field
			pg.insert()

			series = getseries("", 2)
			series = int(series) - 1

			self.assertEqual(pg.name, f"TODO-{field}-{series:02}")

	def test_format_autoname_for_consecutive_week_number(self):
		"""
		Test if braced params are replaced for consecutive week number in format autoname
		"""
		pagetype = "ToDo"

		todo_pagetype = jingrow.get_pg("PageType", pagetype)
		todo_pagetype.autoname = "format:TODO-{WW}-{##}"
		todo_pagetype.save()

		description = "Format"

		todo = jingrow.new_pg(pagetype)
		todo.description = description
		todo.insert()

		series = getseries("", 2)

		series = str(int(series) - 1)

		if len(series) < 2:
			series = "0" + series

		week = determine_consecutive_week_number(now_datetime())

		self.assertEqual(todo.name, f"TODO-{week}-{series}")

	def test_revert_series(self):
		from datetime import datetime

		year = datetime.now().year

		series = f"TEST-{year}-"
		key = "TEST-.YYYY.-"
		name = f"TEST-{year}-00001"
		jingrow.db.sql("""INSERT INTO `tabSeries` (name, current) values (%s, 1)""", (series,))
		revert_series_if_last(key, name)
		current_index = jingrow.db.sql(
			"""SELECT current from `tabSeries` where name = %s""", series, as_dict=True
		)[0]

		self.assertEqual(current_index.get("current"), 0)
		jingrow.db.delete("Series", {"name": series})

		series = f"TEST-{year}-"
		key = "TEST-.YYYY.-.#####"
		name = f"TEST-{year}-00002"
		jingrow.db.sql("""INSERT INTO `tabSeries` (name, current) values (%s, 2)""", (series,))
		revert_series_if_last(key, name)
		current_index = jingrow.db.sql(
			"""SELECT current from `tabSeries` where name = %s""", series, as_dict=True
		)[0]

		self.assertEqual(current_index.get("current"), 1)
		jingrow.db.delete("Series", {"name": series})

		series = "TEST-"
		key = "TEST-"
		name = "TEST-00003"
		jingrow.db.delete("Series", {"name": series})
		jingrow.db.sql("""INSERT INTO `tabSeries` (name, current) values (%s, 3)""", (series,))
		revert_series_if_last(key, name)
		current_index = jingrow.db.sql(
			"""SELECT current from `tabSeries` where name = %s""", series, as_dict=True
		)[0]

		self.assertEqual(current_index.get("current"), 2)
		jingrow.db.delete("Series", {"name": series})

		series = "TEST1-"
		key = "TEST1-.#####.-2021-22"
		name = "TEST1-00003-2021-22"
		jingrow.db.delete("Series", {"name": series})
		jingrow.db.sql("""INSERT INTO `tabSeries` (name, current) values (%s, 3)""", (series,))
		revert_series_if_last(key, name)
		current_index = jingrow.db.sql(
			"""SELECT current from `tabSeries` where name = %s""", series, as_dict=True
		)[0]

		self.assertEqual(current_index.get("current"), 2)
		jingrow.db.delete("Series", {"name": series})

		series = ""
		key = ".#####.-2021-22"
		name = "00003-2021-22"
		jingrow.db.delete("Series", {"name": series})
		jingrow.db.sql("""INSERT INTO `tabSeries` (name, current) values (%s, 3)""", (series,))
		revert_series_if_last(key, name)
		current_index = jingrow.db.sql(
			"""SELECT current from `tabSeries` where name = %s""", series, as_dict=True
		)[0]

		self.assertEqual(current_index.get("current"), 2)

		jingrow.db.delete("Series", {"name": series})

	def test_naming_for_cancelled_and_amended_pg(self):
		submittable_pagetype = jingrow.get_pg(
			{
				"pagetype": "PageType",
				"module": "Core",
				"custom": 1,
				"is_submittable": 1,
				"permissions": [{"role": "System Manager", "read": 1}],
				"name": "Submittable Pagetype",
			}
		).insert(ignore_if_duplicate=True)

		pg = jingrow.new_pg("Submittable Pagetype")
		pg.save()
		original_name = pg.name

		pg.submit()
		pg.cancel()
		cancelled_name = pg.name
		self.assertEqual(cancelled_name, original_name)

		amended_pg = jingrow.copy_pg(pg)
		amended_pg.pagestatus = 0
		amended_pg.amended_from = pg.name
		amended_pg.save()
		self.assertEqual(amended_pg.name, f"{original_name}-1")

		amended_pg.submit()
		amended_pg.cancel()
		self.assertEqual(amended_pg.name, f"{original_name}-1")

		submittable_pagetype.delete()

	def test_determine_consecutive_week_number(self):
		from datetime import datetime

		dt = datetime.fromisoformat("2019-12-31")
		w = determine_consecutive_week_number(dt)
		self.assertEqual(w, "53")

		dt = datetime.fromisoformat("2020-01-01")
		w = determine_consecutive_week_number(dt)
		self.assertEqual(w, "01")

		dt = datetime.fromisoformat("2020-01-15")
		w = determine_consecutive_week_number(dt)
		self.assertEqual(w, "03")

		dt = datetime.fromisoformat("2021-01-01")
		w = determine_consecutive_week_number(dt)
		self.assertEqual(w, "00")

		dt = datetime.fromisoformat("2021-12-31")
		w = determine_consecutive_week_number(dt)
		self.assertEqual(w, "52")

	def test_naming_validations(self):
		# case 1: check same name as pagetype
		# set name via prompt
		tag = jingrow.get_pg({"pagetype": "Tag", "__newname": "Tag"})
		self.assertRaises(jingrow.NameError, tag.insert)

		# set by passing set_name as ToDo
		self.assertRaises(jingrow.NameError, make_invalid_todo)

		# name (via title field) cannot be the same as the pagetype
		note = jingrow.get_pg({"pagetype": "Currency", "currency_name": "Currency"})
		self.assertRaises(jingrow.NameError, note.insert)

		# case 2: set name with "New ---"
		tag = jingrow.get_pg({"pagetype": "Tag", "__newname": "New Tag"})
		self.assertRaises(jingrow.NameError, tag.insert)

		# case 3: set name with special characters
		tag = jingrow.get_pg({"pagetype": "Tag", "__newname": "Tag<>"})
		self.assertRaises(jingrow.NameError, tag.insert)

		# case 4: no name specified
		tag = jingrow.get_pg({"pagetype": "Tag", "__newname": ""})
		self.assertRaises(jingrow.ValidationError, tag.insert)

	def test_autoincremented_naming(self):
		from jingrow.core.pagetype.pagetype.test_pagetype import new_pagetype

		pagetype = "autoinc_pagetype" + jingrow.generate_hash(length=5)
		dt = new_pagetype(pagetype, autoname="autoincrement").insert(ignore_permissions=True)

		for i in range(1, 20):
			self.assertEqual(jingrow.new_pg(pagetype).save(ignore_permissions=True).name, i)

		dt.delete(ignore_permissions=True)

	def test_naming_series_prefix(self):
		today = now_datetime()
		year = today.strftime("%y")
		month = today.strftime("%m")

		prefix_test_cases = {
			"SINV-.YY.-.####": f"SINV-{year}-",
			"SINV-.YY.-.MM.-.####": f"SINV-{year}-{month}-",
			"SINV": "SINV",
			"SINV-.": "SINV-",
		}

		for series, prefix in prefix_test_cases.items():
			self.assertEqual(prefix, NamingSeries(series).get_prefix())

	def test_naming_series_validation(self):
		dns = jingrow.get_pg("Page Naming Settings")
		exisiting_series = dns.get_transactions_and_prefixes()["prefixes"]
		valid = ["SINV-", "SI-.{field}.", "SI-#.###", "", *exisiting_series]
		invalid = ["$INV-", r"WINDOWS\NAMING"]

		for series in valid:
			if series.strip():
				try:
					NamingSeries(series).validate()
				except Exception as e:
					self.fail(f"{series} should be valid\n{e}")

		for series in invalid:
			self.assertRaises(InvalidNamingSeriesError, NamingSeries(series).validate)

	def test_naming_using_fields(self):
		webhook = jingrow.new_pg("Webhook")
		webhook.webhook_docevent = "on_update"
		name = NamingSeries("KOOH-.{webhook_docevent}.").generate_next_name(webhook)
		self.assertTrue(
			name.startswith("KOOH-on_update"), f"incorrect name generated {name}, missing field value"
		)

	def test_naming_with_empty_part(self):
		# check naming with empty part (duplicate dots)

		webhook = jingrow.new_pg("Webhook")
		webhook.webhook_docevent = "on_update"

		series = "KOOH-..{webhook_docevent}.-.####"

		name = parse_naming_series(series, pg=webhook)
		self.assertTrue(
			name.startswith("KOOH-on_update"), f"incorrect name generated {name}, missing field value"
		)

	def test_naming_with_unsupported_part(self):
		# check naming with empty part (duplicate dots)

		webhook = jingrow.new_pg("Webhook")
		webhook.webhook_docevent = {"dict": "not supported"}

		series = "KOOH-..{webhook_docevent}.-.####"

		name = parse_naming_series(series, pg=webhook)
		self.assertTrue(name.startswith("KOOH-"), f"incorrect name generated {name}, missing field value")

	def test_naming_with_empty_field(self):
		# check naming with empty field value

		webhook = jingrow.new_pg("Webhook")
		series = "KOOH-.{request_structure}.-.request_structure.-.####"

		name = parse_naming_series(series, pg=webhook)
		self.assertTrue(name.startswith("KOOH---"), f"incorrect name generated {name}")

	@run_only_if(db_type_is.MARIADB)
	def test_hash_collision(self):
		pagetype = new_pagetype(autoname="hash").insert().name
		name = jingrow.generate_hash()
		for _ in range(10):
			jingrow.flags.in_import = True
			jingrow.new_pg(pagetype).update({"name": name}).insert()
		jingrow.flags.pop("in_import", None)

	def test_custom_parser(self):
		# check naming with custom parser
		todo = jingrow.new_pg("ToDo")
		series = "TODO-.PM.-.####"

		jingrow.clear_cache()
		with patch_hooks(
			{
				"naming_series_variables": {
					"PM": ["jingrow.tests.test_naming.parse_naming_series_variable"],
				},
			},
		):
			name = parse_naming_series(series, pg=todo)
			expected_name = "TODO-" + nowdate().split("-")[1] + "-" + "0001"
			self.assertEqual(name, expected_name)

	@retry(
		retry=retry_if_exception_type(AssertionError),
		stop=stop_after_attempt(3),
		wait=wait_full_jitter(),
		reraise=True,
	)
	def test_hash_naming_is_roughly_sequential(self):
		"""hash naming is supposed to be sequential *most of the time*"""
		names = []
		for _ in range(10):
			time.sleep(0.1)
			names.append(make_autoname("hash"))
		self.assertEqual(names, sorted(names))


def parse_naming_series_variable(pg, variable):
	if variable == "PM":
		return nowdate().split("-")[1]


def make_invalid_todo():
	jingrow.get_pg({"pagetype": "ToDo", "description": "Test"}).insert(set_name="ToDo")
