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
from shop_management.models.data.customer import Customer
from shop_management.models.data.customer_payment import CustomerPayment
from shop_management.models.data.sale import Sale
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


class CustomerPaymentTests(TestCase):
    def setUp(self):
        self.user = get_user_model().objects.create_user(username="collector", password="test", is_staff=True)
        self.user.allowed_tabs = ["sales_payments"]
        self.user.save(update_fields=["allowed_tabs"])
        self.shop = Shop.objects.create(owner=self.user, name="Kabul")
        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="Buyer")
        self.sales = [Sale.objects.create(
            shop=self.shop, customer=self.customer, currency=self.currency,
            uses_inventory_engine=True, document_status=Sale.DocumentStatus.POSTED,
            calculated_total=amount, invoice_date=timezone.localdate(),
        ) for amount in (Decimal("100"), Decimal("150"))]
        self.client = APIClient()
        self.client.force_authenticate(self.user)

    def create_payment(self, amount="300", allocations=None):
        return self.client.post("/api/shop/customer-payments/", {
            "customer": self.customer.id, "shop": self.shop.id,
            "financial_account": self.account.id, "currency": self.currency.id,
            "amount": amount, "exchange_rate_used": "1", "payment_date": str(timezone.localdate()),
            "reference": "REC-1", "allocations": allocations or [],
        }, format="json")

    def collect_payment(self, amount="180"):
        return self.client.post("/api/shop/customer-payments/collect/", {
            "customer": self.customer.id, "shop": self.shop.id,
            "financial_account": self.account.id, "currency": self.currency.id,
            "amount": amount, "exchange_rate_used": "1", "payment_date": str(timezone.localdate()),
            "reference": "REC-AUTO",
        }, format="json")

    def test_collect_posts_and_allocates_oldest_invoices_first(self):
        response = self.collect_payment("180")

        self.assertEqual(response.status_code, 201, response.data)
        payment = CustomerPayment.objects.get(pk=response.data["id"])
        allocations = list(payment.allocations.order_by("sale__invoice_date", "sale_id"))
        self.assertEqual(payment.status, CustomerPayment.Status.POSTED)
        self.assertEqual(
            [(row.sale_id, row.allocated_amount) for row in allocations],
            [
                (self.sales[0].id, Decimal("100")),
                (self.sales[1].id, Decimal("80")),
            ],
        )
        self.assertEqual(payment.unallocated_amount, Decimal("0"))
        self.assertEqual(self.account.transactions.get().amount, Decimal("180"))

    def test_collect_partial_payment_does_not_skip_oldest_invoice(self):
        response = self.collect_payment("60")

        self.assertEqual(response.status_code, 201, response.data)
        payment = CustomerPayment.objects.get(pk=response.data["id"])
        allocation = payment.allocations.get()
        self.assertEqual(allocation.sale_id, self.sales[0].id)
        self.assertEqual(allocation.allocated_amount, Decimal("60"))

    def test_multi_invoice_allocation_and_on_account_credit(self):
        response = self.create_payment(allocations=[
            {"sale": self.sales[0].id, "allocated_amount": "100"},
            {"sale": self.sales[1].id, "allocated_amount": "120"},
        ])
        self.assertEqual(response.status_code, 201, response.data)
        payment = CustomerPayment.objects.get(pk=response.data["id"])
        self.assertEqual(payment.unallocated_amount, Decimal("80"))
        posted = self.client.post(f"/api/shop/customer-payments/{payment.id}/post/")
        self.assertEqual(posted.status_code, 200, posted.data)
        self.assertEqual(AccountTransaction.objects.filter(transaction_type="credit").count(), 1)
        self.assertEqual(self.account.transactions.get().amount, Decimal("300"))
        self.assertEqual(self.sales[0].total_paid["AFN"], Decimal("100"))
        self.assertEqual(self.customer.get_credit_by_currency(), {"AFN": 80.0})

    def test_overallocation_rolls_back(self):
        response = self.create_payment(amount="50", allocations=[{"sale": self.sales[0].id, "allocated_amount": "60"}])
        payment = CustomerPayment.objects.get(pk=response.data["id"])
        posted = self.client.post(f"/api/shop/customer-payments/{payment.id}/post/")
        self.assertEqual(posted.status_code, 400)
        payment.refresh_from_db()
        self.assertEqual(payment.status, "DRAFT")
        self.assertEqual(AccountTransaction.objects.count(), 0)

    def test_void_reverses_cash_and_removes_paid_allocation(self):
        response = self.create_payment(amount="100", allocations=[{"sale": self.sales[0].id, "allocated_amount": "100"}])
        payment_id = response.data["id"]
        self.client.post(f"/api/shop/customer-payments/{payment_id}/post/")
        voided = self.client.post(f"/api/shop/customer-payments/{payment_id}/void/", {"reason": "Wrong customer"}, format="json")
        self.assertEqual(voided.status_code, 200, voided.data)
        self.assertEqual(AccountTransaction.objects.filter(transaction_type="debit").count(), 1)
        self.assertEqual(self.sales[0].total_paid["AFN"], Decimal("0"))

    def test_unallocated_receipt_reduces_customer_opening_balance(self):
        self.customer.opening_balance = Decimal("2000")
        self.customer.opening_balance_currency = self.currency
        self.customer.save(update_fields=["opening_balance", "opening_balance_currency"])
        response = self.create_payment(amount="1000")
        self.assertEqual(response.status_code, 201, response.data)
        payment_id = response.data["id"]

        posted = self.client.post(f"/api/shop/customer-payments/{payment_id}/post/")
        self.assertEqual(posted.status_code, 200, posted.data)
        self.assertEqual(self.customer.get_total_owed_by_currency(), {"AFN": 1000.0})

        voided = self.client.post(
            f"/api/shop/customer-payments/{payment_id}/void/",
            {"reason": "Incorrect receipt"},
            format="json",
        )
        self.assertEqual(voided.status_code, 200, voided.data)
        self.assertEqual(self.customer.get_total_owed_by_currency(), {"AFN": 2000.0})
