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

from unittest.mock import patch

import frappe

from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.stock_balance import update_bin_qty
from erpnext.stock.utils import _create_bin, get_bin
from erpnext.tests.utils import ERPNextTestSuite


class TestBin(ERPNextTestSuite):
	def test_concurrent_inserts(self):
		"""Ensure no duplicates are possible in case of concurrent inserts"""
		item_code = "_TestConcurrentBin"
		make_item(item_code)
		warehouse = "_Test Warehouse - _TC"

		bin1 = frappe.get_doc(doctype="Bin", item_code=item_code, warehouse=warehouse)
		bin1.insert()

		bin2 = frappe.get_doc(doctype="Bin", item_code=item_code, warehouse=warehouse)
		with self.assertRaises(frappe.UniqueValidationError):
			bin2.insert()

		# util method should handle it
		bin = _create_bin(item_code, warehouse)
		self.assertEqual(bin.item_code, item_code)

	def test_update_bin_qty_keeps_stock_movement_after_read(self):
		item_code = make_item(properties={"is_stock_item": 1}).name
		warehouse = "_Test Warehouse - _TC"
		make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=100)

		def get_bin_then_issue(item, wh):
			bin = get_bin(item, wh)
			make_stock_entry(item_code=item, source=wh, qty=4)
			return bin

		with patch("erpnext.stock.utils.get_bin", side_effect=get_bin_then_issue):
			update_bin_qty(item_code, warehouse, {"ordered_qty": 5})

		bin = frappe.db.get_value(
			"Bin",
			{"item_code": item_code, "warehouse": warehouse},
			["actual_qty", "projected_qty"],
			as_dict=1,
		)
		self.assertEqual((bin.actual_qty, bin.projected_qty), (6, 11))

	def test_repost_recomputes_projected_qty(self):
		from erpnext.stock.stock_balance import repost_stock

		item_code = make_item(properties={"is_stock_item": 1}).name
		warehouse = "_Test Warehouse - _TC"
		make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=100)

		bin_name = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse})
		frappe.db.set_value("Bin", bin_name, {"actual_qty": 7, "projected_qty": 7})

		repost_stock(item_code, warehouse)

		bin = frappe.db.get_value("Bin", bin_name, ["actual_qty", "projected_qty"], as_dict=1)
		self.assertEqual((bin.actual_qty, bin.projected_qty), (10, 10))

	def test_stock_manager_can_recalculate_values(self):
		from frappe.core.doctype.user_permission.test_user_permission import create_user

		item_code = make_item(properties={"is_stock_item": 1}).name
		warehouse = "_Test Warehouse - _TC"
		make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=100)

		bin = frappe.get_doc("Bin", {"item_code": item_code, "warehouse": warehouse})
		bin.db_set("actual_qty", 0)

		user = create_user("test_bin_recalculate@example.com", "Stock Manager")
		frappe.set_user(user.name)
		try:
			bin.reload()
			bin.recalculate_values()
		finally:
			frappe.set_user("Administrator")

		self.assertEqual(frappe.db.get_value("Bin", bin.name, "actual_qty"), 10)

	def test_recalculate_values(self):
		from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry

		item_code = make_item().name
		warehouse = "_Test Warehouse - _TC"
		make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=100)

		bin = frappe.get_doc("Bin", {"item_code": item_code, "warehouse": warehouse})
		bin.db_set({"actual_qty": 0, "valuation_rate": 0, "stock_value": 0})
		bin.reload()
		bin.recalculate_values()

		self.assertEqual(bin.actual_qty, 10)
		self.assertEqual(bin.valuation_rate, 100)
		self.assertEqual(bin.stock_value, 1000)

	def test_recalculate_values_ignores_client_supplied_fields(self):
		item_code = make_item(properties={"is_stock_item": 1}).name
		warehouse = "_Test Warehouse - _TC"
		make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=100)

		bin = frappe.get_doc("Bin", {"item_code": item_code, "warehouse": warehouse})
		reserved_stock = bin.reserved_stock
		bin.reserved_stock = 999
		bin.actual_qty = 999
		bin.recalculate_values()

		values = frappe.db.get_value("Bin", bin.name, ["reserved_stock", "actual_qty"], as_dict=1)
		self.assertEqual(values.reserved_stock, reserved_stock)
		self.assertEqual(values.actual_qty, 10)

	def test_bin_column_update_clears_cached_bin(self):
		from erpnext.stock.doctype.bin.bin import update_bin_columns

		item_code = make_item(properties={"is_stock_item": 1}).name
		bin = _create_bin(item_code, "_Test Warehouse - _TC")
		frappe.get_cached_doc("Bin", bin.name)

		update_bin_columns(bin.name, {"ordered_qty": 5})

		cached_bin = frappe.get_cached_doc("Bin", bin.name)
		self.assertEqual((cached_bin.ordered_qty, cached_bin.projected_qty), (5, 5))

	def test_recalculate_values_without_sle(self):
		item_code = make_item().name
		warehouse = "_Test Warehouse - _TC"

		bin = _create_bin(item_code, warehouse)
		bin.db_set({"actual_qty": 5, "valuation_rate": 50, "stock_value": 250})
		bin.reload()
		bin.recalculate_values()

		self.assertEqual(bin.actual_qty, 0)
		self.assertEqual(bin.valuation_rate, 0)
		self.assertEqual(bin.stock_value, 0)

	def test_repost_resets_bin_without_sle(self):
		"""A repost must zero the bin when the ledger is empty, e.g. after entries were deleted."""
		from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
		from erpnext.stock.stock_ledger import update_entries_after

		item_code = make_item().name
		warehouse = "_Test Warehouse - _TC"
		make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=100)

		# deleting a transaction with `delete_linked_ledger_entries` on drops its entries outright
		frappe.db.delete("Stock Ledger Entry", {"item_code": item_code, "warehouse": warehouse})

		update_entries_after(
			{
				"item_code": item_code,
				"warehouse": warehouse,
				"posting_date": "1900-01-01",
				"posting_time": "00:01",
			}
		)

		bin = frappe.get_doc("Bin", {"item_code": item_code, "warehouse": warehouse})
		self.assertEqual(bin.actual_qty, 0)
		self.assertEqual(bin.valuation_rate, 0)
		self.assertEqual(bin.stock_value, 0)

	def test_cancelling_last_entry_resets_bin(self):
		"""Cancelling the only voucher must clear stock value, not just quantity."""
		from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry

		item_code = make_item().name
		warehouse = "_Test Warehouse - _TC"
		se = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=100)

		se.cancel()

		bin = frappe.get_doc("Bin", {"item_code": item_code, "warehouse": warehouse})
		self.assertEqual(bin.actual_qty, 0)
		self.assertEqual(bin.valuation_rate, 0)
		self.assertEqual(bin.stock_value, 0)

	def test_cancelling_transfer_restores_bin_stock_value(self):
		"""Cancelling a transfer must put back the stock value of both bins, not just the quantity."""
		from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry

		item_code = make_item().name
		source = "_Test Warehouse - _TC"
		target = "_Test Warehouse 1 - _TC"
		make_stock_entry(item_code=item_code, target=source, qty=10, rate=100, posting_time="01:00:00")
		make_stock_entry(item_code=item_code, target=target, qty=10, rate=200, posting_time="02:00:00")

		se = make_stock_entry(
			item_code=item_code, source=source, target=target, qty=5, posting_time="03:00:00"
		)
		se.cancel()

		for warehouse, valuation_rate, stock_value in ((source, 100, 1000), (target, 200, 2000)):
			bin = frappe.get_doc("Bin", {"item_code": item_code, "warehouse": warehouse})
			self.assertEqual(bin.actual_qty, 10)
			self.assertEqual(bin.valuation_rate, valuation_rate)
			self.assertEqual(bin.stock_value, stock_value)

	def test_deleting_last_voucher_resets_bin(self):
		"""Deleting the only voucher wipes its ledger entries outright, the bin must still be cleared."""
		from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry

		item_code = make_item().name
		warehouse = "_Test Warehouse - _TC"
		delete_entries = frappe.get_single_value("Accounts Settings", "delete_linked_ledger_entries")
		frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 1)

		try:
			se = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=100)
			se.cancel()
			frappe.delete_doc("Stock Entry", se.name, force=1)
		finally:
			frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", delete_entries)

		bin = frappe.get_doc("Bin", {"item_code": item_code, "warehouse": warehouse})
		self.assertEqual(bin.actual_qty, 0)
		self.assertEqual(bin.valuation_rate, 0)
		self.assertEqual(bin.stock_value, 0)

	def test_index_exists(self):
		indexes = frappe.db.sql("show index from tabBin where Non_unique = 0", as_dict=1)
		if not any(index.get("Key_name") == "unique_item_warehouse" for index in indexes):
			self.fail("Expected unique index on item-warehouse")
