from datetime import timedelta
from decimal import Decimal

from django.contrib.auth import get_user_model
from django.test import TestCase
from django.utils import timezone
from rest_framework.test import APIClient

from shop_management.models.data.account_transaction import AccountTransaction, AccountTransactionType
from shop_management.models.data.inventory import InventoryBatch, StockBalance, StockMovement
from shop_management.models.data.accounting import JournalEntry
from shop_management.models.data.customer import Customer
from shop_management.models.data.organization import Warehouse
from shop_management.models.data.product import Product
from shop_management.models.data.sale import Sale
from shop_management.models.data.sale_return import SaleReturn
from shop_management.models.data.shop import Shop
from shop_management.models.data.shop_account import Account
from shop_management.models.data.utils import Currency, Unit
from shop_management.services.inventory import receive_stock


class SaleReturnLifecycleTests(TestCase):
    def setUp(self):
        self.user = get_user_model().objects.create_user(username="returns", password="test", is_staff=True)
        self.user.allowed_tabs = ["sales", "sale_return"]
        self.user.save(update_fields=["allowed_tabs"])
        self.shop = Shop.objects.create(owner=self.user, name="Kabul")
        self.warehouse = Warehouse.objects.create(shop=self.shop, code="MAIN", name="Main")
        self.currency = Currency.objects.create(shop=self.shop, title="AFN")
        self.account = Account.objects.create(shop=self.shop, name="Cash", currency=self.currency)
        self.customer = Customer.objects.create(shop=self.shop, name="Registered Customer")
        self.unit = Unit.objects.create(shop=self.shop, title="Each")
        self.product = Product.objects.create(shop=self.shop, name="Medicine", default_unit=self.unit, requires_batch=True, requires_expiry=True)
        self.batch_a = InventoryBatch.objects.create(product=self.product, batch_number="A", expiry_date=timezone.localdate() + timedelta(days=20))
        self.batch_b = InventoryBatch.objects.create(product=self.product, batch_number="B", expiry_date=timezone.localdate() + timedelta(days=60))
        receive_stock(warehouse=self.warehouse, product=self.product, batch=self.batch_a, quantity=2, unit_cost=10, currency=self.currency)
        receive_stock(warehouse=self.warehouse, product=self.product, batch=self.batch_b, quantity=4, unit_cost=20, currency=self.currency)
        self.client = APIClient()
        self.client.force_authenticate(self.user)
        response = self.client.post("/api/shop/sales/", {
            "shop": self.shop.id, "warehouse": self.warehouse.id, "currency": self.currency.id,
            "customer": self.customer.id,
            "sales_type": "CASH", "invoice_date": str(timezone.localdate()),
            "discount_type": "PERCENTAGE", "discount_value": "10", "received_amount": "0",
            "items": [{"product": self.product.id, "unit": self.unit.id, "quantity": 5, "sale_price": "30"}],
        }, format="json")
        self.assertEqual(response.status_code, 201, response.data)
        self.sale = Sale.objects.get(pk=response.data["id"])
        posted = self.client.post(f"/api/shop/sales/{self.sale.id}/post/")
        self.assertEqual(posted.status_code, 200, posted.data)
        self.sale.received_amount = Decimal("135")
        self.sale.save(update_fields=["received_amount"])
        StockMovement.objects.filter(movement_type="OPENING").delete()

    def create_return(self, quantity, cash_refund="0"):
        response = self.client.post("/api/shop/sale-returns/", {
            "shop": self.shop.id, "sale": self.sale.id, "reason": "Customer return",
            "refund_account": self.account.id if Decimal(cash_refund) else None,
            "cash_refund_amount": cash_refund,
            "return_items": [{"sale_item": self.sale.sale_items.get().id, "return_quantity": quantity}],
        }, format="json")
        self.assertEqual(response.status_code, 201, response.data)
        return SaleReturn.objects.get(pk=response.data["id"])

    def test_partial_return_restores_original_batches_and_discounted_value(self):
        document = self.create_return(3)
        self.assertEqual(document.document_status, "DRAFT")
        self.assertEqual(StockBalance.objects.get(batch=self.batch_a).quantity, 0)
        response = self.client.post(f"/api/shop/sale-returns/{document.id}/post/")
        self.assertEqual(response.status_code, 200, response.data)
        document.refresh_from_db()
        restorations = list(document.return_items.get().batch_restorations.order_by("batch__batch_number"))
        self.assertEqual([row.quantity for row in restorations], [Decimal("2"), Decimal("1")])
        self.assertEqual(document.total_refound_amount, Decimal("81.00"))
        self.assertEqual(StockBalance.objects.get(batch=self.batch_a).quantity, 2)
        self.assertEqual(StockBalance.objects.get(batch=self.batch_b).quantity, 2)

    def test_multiple_returns_cannot_exceed_sold_quantity_atomically(self):
        first = self.create_return(4)
        self.assertEqual(self.client.post(f"/api/shop/sale-returns/{first.id}/post/").status_code, 200)
        second = self.create_return(2)
        before = StockBalance.objects.get(batch=self.batch_b).quantity
        response = self.client.post(f"/api/shop/sale-returns/{second.id}/post/")
        self.assertEqual(response.status_code, 400)
        second.refresh_from_db()
        self.assertEqual(second.document_status, "DRAFT")
        self.assertEqual(StockBalance.objects.get(batch=self.batch_b).quantity, before)

    def test_cash_refund_and_void_are_reversed(self):
        document = self.create_return(2, "40")
        self.assertEqual(self.client.post(f"/api/shop/sale-returns/{document.id}/post/").status_code, 200)
        self.assertEqual(AccountTransaction.objects.get().transaction_type, AccountTransactionType.DEBIT)
        response = self.client.post(f"/api/shop/sale-returns/{document.id}/void/", {"reason": "Return cancelled"}, format="json")
        self.assertEqual(response.status_code, 200, response.data)
        document.refresh_from_db()
        self.assertEqual(document.document_status, "VOID")
        self.assertEqual(list(AccountTransaction.objects.order_by("id").values_list("transaction_type", flat=True)), ["debit", "credit"])
        self.assertEqual(StockBalance.objects.get(batch=self.batch_a).quantity, 0)

    def test_voiding_sale_cascades_posted_return_without_double_restoring_stock(self):
        document = self.create_return(2)
        self.assertEqual(
            self.client.post(f"/api/shop/sale-returns/{document.id}/post/").status_code,
            200,
        )

        response = self.client.post(
            f"/api/shop/sales/{self.sale.id}/void/",
            {"reason": "Original invoice cancelled"},
            format="json",
        )

        self.assertEqual(response.status_code, 200, response.data)
        document.refresh_from_db()
        self.sale.refresh_from_db()
        self.assertEqual(document.document_status, SaleReturn.DocumentStatus.VOID)
        self.assertEqual(self.sale.document_status, Sale.DocumentStatus.VOID)
        self.assertEqual(StockBalance.objects.get(batch=self.batch_a).quantity, Decimal("2"))
        self.assertEqual(StockBalance.objects.get(batch=self.batch_b).quantity, Decimal("4"))
        self.assertFalse(
            JournalEntry.objects.filter(
                source_type__in=["SALE", "SALE_RETURN"],
                status=JournalEntry.Status.POSTED,
            ).exists()
        )
