# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt

import unittest

import frappe
from frappe.utils.data import (
	add_days,
	add_months,
	add_to_date,
	date_diff,
	flt,
	get_date_str,
	nowdate,
)

from erpnext.accounts.doctype.subscription.subscription import get_prorata_factor

test_dependencies = ("UOM", "Item Group", "Item")


def create_plan():
	if not frappe.db.exists("Subscription Plan", "_Test Plan Name"):
		plan = frappe.new_doc("Subscription Plan")
		plan.plan_name = "_Test Plan Name"
		plan.item = "_Test Non Stock Item"
		plan.price_determination = "Fixed Rate"
		plan.cost = 900
		plan.billing_interval = "Month"
		plan.billing_interval_count = 1
		plan.insert()

	if not frappe.db.exists("Subscription Plan", "_Test Plan Name 2"):
		plan = frappe.new_doc("Subscription Plan")
		plan.plan_name = "_Test Plan Name 2"
		plan.item = "_Test Non Stock Item"
		plan.price_determination = "Fixed Rate"
		plan.cost = 1999
		plan.billing_interval = "Month"
		plan.billing_interval_count = 1
		plan.insert()

	if not frappe.db.exists("Subscription Plan", "_Test Plan Name 3"):
		plan = frappe.new_doc("Subscription Plan")
		plan.plan_name = "_Test Plan Name 3"
		plan.item = "_Test Non Stock Item"
		plan.price_determination = "Fixed Rate"
		plan.cost = 1999
		plan.billing_interval = "Day"
		plan.billing_interval_count = 14
		plan.insert()

	# Defined a quarterly Subscription Plan
	if not frappe.db.exists("Subscription Plan", "_Test Plan Name 4"):
		plan = frappe.new_doc("Subscription Plan")
		plan.plan_name = "_Test Plan Name 4"
		plan.item = "_Test Non Stock Item"
		plan.price_determination = "Monthly Rate"
		plan.cost = 20000
		plan.billing_interval = "Month"
		plan.billing_interval_count = 3
		plan.insert()

	if not frappe.db.exists("Subscription Plan", "_Test Plan Multicurrency"):
		plan = frappe.new_doc("Subscription Plan")
		plan.plan_name = "_Test Plan Multicurrency"
		plan.item = "_Test Non Stock Item"
		plan.price_determination = "Fixed Rate"
		plan.cost = 50
		plan.currency = "USD"
		plan.billing_interval = "Month"
		plan.billing_interval_count = 1
		plan.insert()


def create_parties():
	if not frappe.db.exists("Supplier", "_Test Supplier"):
		supplier = frappe.new_doc("Supplier")
		supplier.supplier_name = "_Test Supplier"
		supplier.supplier_group = "All Supplier Groups"
		supplier.insert()

	if not frappe.db.exists("Customer", "_Test Subscription Customer"):
		customer = frappe.new_doc("Customer")
		customer.customer_name = "_Test Subscription Customer"
		customer.billing_currency = "USD"
		customer.append(
			"accounts", {"company": "_Test Company", "account": "_Test Receivable USD - _TC"}
		)
		customer.insert()


class TestSubscription(unittest.TestCase):
	def setUp(self):
		create_plan()
		create_parties()

	def test_create_subscription_with_trial_with_correct_period(self):
		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.trial_period_start = nowdate()
		subscription.trial_period_end = add_months(nowdate(), 1)
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.save()

		self.assertEqual(subscription.trial_period_start, nowdate())
		self.assertEqual(subscription.trial_period_end, add_months(nowdate(), 1))
		self.assertEqual(
			add_days(subscription.trial_period_end, 1), get_date_str(subscription.current_invoice_start)
		)
		self.assertEqual(
			add_to_date(subscription.current_invoice_start, months=1, days=-1),
			get_date_str(subscription.current_invoice_end),
		)
		self.assertEqual(subscription.invoices, [])
		self.assertEqual(subscription.status, "Trialling")

		subscription.delete()

	def test_create_subscription_without_trial_with_correct_period(self):
		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.save()

		self.assertEqual(subscription.trial_period_start, None)
		self.assertEqual(subscription.trial_period_end, None)
		self.assertEqual(subscription.current_invoice_start, nowdate())
		self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1))
		# No invoice is created
		self.assertEqual(len(subscription.invoices), 0)
		self.assertEqual(subscription.status, "Active")

		subscription.delete()

	def test_create_subscription_trial_with_wrong_dates(self):
		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.trial_period_end = nowdate()
		subscription.trial_period_start = add_days(nowdate(), 30)
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})

		self.assertRaises(frappe.ValidationError, subscription.save)
		subscription.delete()

	def test_create_subscription_multi_with_different_billing_fails(self):
		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.trial_period_end = nowdate()
		subscription.trial_period_start = add_days(nowdate(), 30)
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.append("plans", {"plan": "_Test Plan Name 3", "qty": 1})

		self.assertRaises(frappe.ValidationError, subscription.save)
		subscription.delete()

	def test_invoice_is_generated_at_end_of_billing_period(self):
		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.start_date = "2018-01-01"
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.insert()

		self.assertEqual(subscription.status, "Active")
		self.assertEqual(subscription.current_invoice_start, "2018-01-01")
		self.assertEqual(subscription.current_invoice_end, "2018-01-31")
		subscription.process()

		self.assertEqual(len(subscription.invoices), 1)
		self.assertEqual(subscription.current_invoice_start, "2018-01-01")
		subscription.process()
		self.assertEqual(subscription.status, "Unpaid")
		subscription.delete()

	def test_status_goes_back_to_active_after_invoice_is_paid(self):
		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.start_date = "2018-01-01"
		subscription.insert()
		subscription.process()  # generate first invoice
		self.assertEqual(len(subscription.invoices), 1)

		# Status is unpaid as Days until Due is zero and grace period is Zero
		self.assertEqual(subscription.status, "Unpaid")

		subscription.get_current_invoice()
		current_invoice = subscription.get_current_invoice()

		self.assertIsNotNone(current_invoice)

		current_invoice.db_set("outstanding_amount", 0)
		current_invoice.db_set("status", "Paid")
		subscription.process()

		self.assertEqual(subscription.status, "Active")
		self.assertEqual(subscription.current_invoice_start, add_months(subscription.start_date, 1))
		self.assertEqual(len(subscription.invoices), 1)

		subscription.delete()

	def test_subscription_cancel_after_grace_period(self):
		settings = frappe.get_single("Subscription Settings")
		default_grace_period_action = settings.cancel_after_grace
		settings.cancel_after_grace = 1
		settings.save()

		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.start_date = "2018-01-01"
		subscription.insert()

		self.assertEqual(subscription.status, "Active")

		subscription.process()  # generate first invoice
		# This should change status to Cancelled since grace period is 0
		# And is backdated subscription so subscription will be cancelled after processing
		self.assertEqual(subscription.status, "Cancelled")

		settings.cancel_after_grace = default_grace_period_action
		settings.save()
		subscription.delete()

	def test_subscription_unpaid_after_grace_period(self):
		settings = frappe.get_single("Subscription Settings")
		default_grace_period_action = settings.cancel_after_grace
		settings.cancel_after_grace = 0
		settings.save()

		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.start_date = "2018-01-01"
		subscription.insert()
		subscription.process()  # generate first invoice

		# Status is unpaid as Days until Due is zero and grace period is Zero
		self.assertEqual(subscription.status, "Unpaid")

		settings.cancel_after_grace = default_grace_period_action
		settings.save()
		subscription.delete()

	def test_subscription_invoice_days_until_due(self):
		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.days_until_due = 10
		subscription.start_date = add_months(nowdate(), -1)
		subscription.insert()
		subscription.process()  # generate first invoice
		self.assertEqual(len(subscription.invoices), 1)
		self.assertEqual(subscription.status, "Active")

		subscription.delete()

	def test_subscription_is_past_due_doesnt_change_within_grace_period(self):
		settings = frappe.get_single("Subscription Settings")
		grace_period = settings.grace_period
		settings.grace_period = 1000
		settings.save()

		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.start_date = add_days(nowdate(), -1000)
		subscription.insert()
		subscription.process()  # generate first invoice

		self.assertEqual(subscription.status, "Past Due Date")

		subscription.process()
		# Grace period is 1000 days so status should remain as Past Due Date
		self.assertEqual(subscription.status, "Past Due Date")

		subscription.process()
		self.assertEqual(subscription.status, "Past Due Date")

		subscription.process()
		self.assertEqual(subscription.status, "Past Due Date")

		settings.grace_period = grace_period
		settings.save()
		subscription.delete()

	def test_subscription_remains_active_during_invoice_period(self):
		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.save()
		subscription.process()  # no changes expected

		self.assertEqual(subscription.status, "Active")
		self.assertEqual(subscription.current_invoice_start, nowdate())
		self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1))
		self.assertEqual(len(subscription.invoices), 0)

		subscription.process()  # no changes expected still
		self.assertEqual(subscription.status, "Active")
		self.assertEqual(subscription.current_invoice_start, nowdate())
		self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1))
		self.assertEqual(len(subscription.invoices), 0)

		subscription.process()  # no changes expected yet still
		self.assertEqual(subscription.status, "Active")
		self.assertEqual(subscription.current_invoice_start, nowdate())
		self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1))
		self.assertEqual(len(subscription.invoices), 0)

		subscription.delete()

	def test_subscription_cancelation(self):
		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.save()
		subscription.cancel_subscription()

		self.assertEqual(subscription.status, "Cancelled")

		subscription.delete()

	def test_subscription_cancellation_invoices(self):
		settings = frappe.get_single("Subscription Settings")
		to_prorate = settings.prorate
		settings.prorate = 1
		settings.save()

		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.save()

		self.assertEqual(subscription.status, "Active")

		subscription.cancel_subscription()
		# Invoice must have been generated
		self.assertEqual(len(subscription.invoices), 1)

		invoice = subscription.get_current_invoice()
		diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1)
		plan_days = flt(
			date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1
		)
		prorate_factor = flt(diff / plan_days)

		self.assertEqual(
			flt(
				get_prorata_factor(
					subscription.current_invoice_end,
					subscription.current_invoice_start,
					subscription.generate_invoice_at_period_start,
				),
				2,
			),
			flt(prorate_factor, 2),
		)
		self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2))
		self.assertEqual(subscription.status, "Cancelled")

		subscription.delete()
		settings.prorate = to_prorate
		settings.save()

	def test_subscription_cancellation_invoices_with_prorata_false(self):
		settings = frappe.get_single("Subscription Settings")
		to_prorate = settings.prorate
		settings.prorate = 0
		settings.save()

		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.save()
		subscription.cancel_subscription()
		invoice = subscription.get_current_invoice()

		self.assertEqual(invoice.grand_total, 900)

		settings.prorate = to_prorate
		settings.save()

		subscription.delete()

	def test_subscription_cancellation_invoices_with_prorata_true(self):
		settings = frappe.get_single("Subscription Settings")
		to_prorate = settings.prorate
		settings.prorate = 1
		settings.save()

		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.save()
		subscription.cancel_subscription()

		invoice = subscription.get_current_invoice()
		diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1)
		plan_days = flt(
			date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1
		)
		prorate_factor = flt(diff / plan_days)

		self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2))

		settings.prorate = to_prorate
		settings.save()

		subscription.delete()

	def test_subcription_cancellation_and_process(self):
		settings = frappe.get_single("Subscription Settings")
		default_grace_period_action = settings.cancel_after_grace
		settings.cancel_after_grace = 1
		settings.save()

		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.start_date = "2018-01-01"
		subscription.insert()
		subscription.process()  # generate first invoice
		invoices = len(subscription.invoices)

		subscription.cancel_subscription()
		self.assertEqual(subscription.status, "Cancelled")
		self.assertEqual(len(subscription.invoices), invoices)

		subscription.process()
		self.assertEqual(subscription.status, "Cancelled")
		self.assertEqual(len(subscription.invoices), invoices)

		subscription.process()
		self.assertEqual(subscription.status, "Cancelled")
		self.assertEqual(len(subscription.invoices), invoices)

		settings.cancel_after_grace = default_grace_period_action
		settings.save()
		subscription.delete()

	def test_subscription_restart_and_process(self):
		settings = frappe.get_single("Subscription Settings")
		default_grace_period_action = settings.cancel_after_grace
		settings.grace_period = 0
		settings.cancel_after_grace = 0
		settings.save()

		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.start_date = "2018-01-01"
		subscription.insert()
		subscription.process()  # generate first invoice

		# Status is unpaid as Days until Due is zero and grace period is Zero
		self.assertEqual(subscription.status, "Unpaid")

		subscription.cancel_subscription()
		self.assertEqual(subscription.status, "Cancelled")

		subscription.restart_subscription()
		self.assertEqual(subscription.status, "Active")
		self.assertEqual(len(subscription.invoices), 0)

		subscription.process()
		self.assertEqual(subscription.status, "Active")
		self.assertEqual(len(subscription.invoices), 0)

		subscription.process()
		self.assertEqual(subscription.status, "Active")
		self.assertEqual(len(subscription.invoices), 0)

		settings.cancel_after_grace = default_grace_period_action
		settings.save()
		subscription.delete()

	def test_subscription_unpaid_back_to_active(self):
		settings = frappe.get_single("Subscription Settings")
		default_grace_period_action = settings.cancel_after_grace
		settings.cancel_after_grace = 0
		settings.save()

		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.start_date = "2018-01-01"
		subscription.insert()

		subscription.process()  # generate first invoice
		# This should change status to Unpaid since grace period is 0
		self.assertEqual(subscription.status, "Unpaid")

		invoice = subscription.get_current_invoice()
		invoice.db_set("outstanding_amount", 0)
		invoice.db_set("status", "Paid")

		subscription.process()
		self.assertEqual(subscription.status, "Active")

		# A new invoice is generated
		subscription.process()
		self.assertEqual(subscription.status, "Unpaid")

		settings.cancel_after_grace = default_grace_period_action
		settings.save()
		subscription.delete()

	def test_restart_active_subscription(self):
		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.save()

		self.assertRaises(frappe.ValidationError, subscription.restart_subscription)

		subscription.delete()

	def test_subscription_invoice_discount_percentage(self):
		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.additional_discount_percentage = 10
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.save()
		subscription.cancel_subscription()

		invoice = subscription.get_current_invoice()

		self.assertEqual(invoice.additional_discount_percentage, 10)
		self.assertEqual(invoice.apply_discount_on, "Grand Total")

		subscription.delete()

	def test_subscription_invoice_discount_amount(self):
		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.additional_discount_amount = 11
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.save()
		subscription.cancel_subscription()

		invoice = subscription.get_current_invoice()

		self.assertEqual(invoice.discount_amount, 11)
		self.assertEqual(invoice.apply_discount_on, "Grand Total")

		subscription.delete()

	def test_prepaid_subscriptions(self):
		# Create a non pre-billed subscription, processing should not create
		# invoices.
		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.save()
		subscription.process()

		self.assertEqual(len(subscription.invoices), 0)

		# Change the subscription type to prebilled and process it.
		# Prepaid invoice should be generated
		subscription.generate_invoice_at_period_start = True
		subscription.save()
		subscription.process()

		self.assertEqual(len(subscription.invoices), 1)

	def test_prepaid_subscriptions_with_prorate_true(self):
		settings = frappe.get_single("Subscription Settings")
		to_prorate = settings.prorate
		settings.prorate = 1
		settings.save()

		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Customer"
		subscription.generate_invoice_at_period_start = True
		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
		subscription.save()
		subscription.process()
		subscription.cancel_subscription()

		self.assertEqual(len(subscription.invoices), 1)

		current_inv = subscription.get_current_invoice()
		self.assertEqual(current_inv.status, "Unpaid")

		prorate_factor = 1

		self.assertEqual(flt(current_inv.grand_total, 2), flt(prorate_factor * 900, 2))

		settings.prorate = to_prorate
		settings.save()

		subscription.delete()

	def test_subscription_with_follow_calendar_months(self):
		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Supplier"
		subscription.party = "_Test Supplier"
		subscription.generate_invoice_at_period_start = 1
		subscription.follow_calendar_months = 1

		# select subscription start date as '2018-01-15'
		subscription.start_date = "2018-01-15"
		subscription.end_date = "2018-07-15"
		subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
		subscription.save()

		# even though subscription starts at '2018-01-15' and Billing interval is Month and count 3
		# First invoice will end at '2018-03-31' instead of '2018-04-14'
		self.assertEqual(get_date_str(subscription.current_invoice_end), "2018-03-31")

	def test_subscription_generate_invoice_past_due(self):
		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Supplier"
		subscription.party = "_Test Supplier"
		subscription.generate_invoice_at_period_start = 1
		subscription.generate_new_invoices_past_due_date = 1
		# select subscription start date as '2018-01-15'
		subscription.start_date = "2018-01-01"
		subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
		subscription.save()

		# Process subscription and create first invoice
		# Subscription status will be unpaid since due date has already passed
		subscription.process()
		self.assertEqual(len(subscription.invoices), 1)
		self.assertEqual(subscription.status, "Unpaid")

		# Now the Subscription is unpaid
		# Even then new invoice should be created as we have enabled `generate_new_invoices_past_due_date` in
		# subscription

		subscription.process()
		self.assertEqual(len(subscription.invoices), 2)

	def test_subscription_without_generate_invoice_past_due(self):
		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Supplier"
		subscription.party = "_Test Supplier"
		subscription.generate_invoice_at_period_start = 1
		# select subscription start date as '2018-01-15'
		subscription.start_date = "2018-01-01"
		subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
		subscription.save()

		# Process subscription and create first invoice
		# Subscription status will be unpaid since due date has already passed
		subscription.process()
		self.assertEqual(len(subscription.invoices), 1)
		self.assertEqual(subscription.status, "Unpaid")

		subscription.process()
		self.assertEqual(len(subscription.invoices), 1)

	def test_multicurrency_subscription(self):
		subscription = frappe.new_doc("Subscription")
		subscription.party_type = "Customer"
		subscription.party = "_Test Subscription Customer"
		subscription.generate_invoice_at_period_start = 1
		subscription.company = "_Test Company"
		# select subscription start date as '2018-01-15'
		subscription.start_date = "2018-01-01"
		subscription.append("plans", {"plan": "_Test Plan Multicurrency", "qty": 1})
		subscription.save()

		subscription.process()
		self.assertEqual(len(subscription.invoices), 1)
		self.assertEqual(subscription.status, "Unpaid")

		# Check the currency of the created invoice
		currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].invoice, "currency")
		self.assertEqual(currency, "USD")
