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.customer import Customer
from shop_management.models.data.customer_balance_payment import (
    CustomerBalancePayment,
    CustomerBalancePaymentAllocation,
)
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.supplier import Supplier
from shop_management.models.data.supplier_balance_payment import (
    SupplierBalancePayment,
    SupplierBalancePaymentAllocation,
)
from shop_management.models.data.utils import Currency


class PartyStatementTests(TestCase):
    def setUp(self):
        self.user = get_user_model().objects.create_user(
            username="statement-user", is_staff=True
        )
        self.user.allowed_tabs = [
            "reports",
            "customers",
            "suppliers",
            "sales",
            "purchases",
            "accounts",
        ]
        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.usd_currency = Currency.objects.create(shop=self.shop, title="USD")
        self.customer = Customer.objects.create(shop=self.shop, name="Customer")
        self.account = Account.objects.create(
            shop=self.shop, name="Cash", currency=self.currency
        )
        self.usd_account = Account.objects.create(
            shop=self.shop, name="USD Cash", currency=self.usd_currency
        )
        self.today = timezone.localdate()
        Sale.objects.create(
            shop=self.shop,
            customer=self.customer,
            currency=self.currency,
            invoice_date=self.today - timedelta(days=10),
            calculated_total=1000,
            uses_inventory_engine=True,
            document_status="POSTED",
            sale_number="SALE-1",
        )
        CustomerPayment.objects.create(
            shop=self.shop,
            customer=self.customer,
            financial_account=self.account,
            currency=self.currency,
            amount=300,
            base_amount=300,
            status="POSTED",
            payment_date=self.today,
        )
        self.client = APIClient()
        self.client.force_authenticate(self.user)

    def test_customer_statement_has_running_balance_and_opening_period(self):
        response = self.client.get(
            "/api/shop/party-statement-report/",
            {
                "type": "customer",
                "shop": self.shop.pk,
                "party": self.customer.pk,
                "start_date": self.today - timedelta(days=5),
                "end_date": self.today,
            },
        )
        self.assertEqual(response.status_code, 200, response.data)
        self.assertEqual(
            response.data["summary"],
            [{"currency": "AFN", "opening_balance": "1000.00", "closing_balance": "700.00"}],
        )
        self.assertEqual(response.data["rows"][0]["kind"], "Customer receipt")
        self.assertEqual(response.data["rows"][0]["running_balance"], "700.00")

    def test_party_must_belong_to_selected_branch(self):
        other_user = get_user_model().objects.create_user(username="statement-other")
        other_shop = Shop.objects.create(owner=other_user, name="Herat")
        other_customer = Customer.objects.create(shop=other_shop, name="Other")
        response = self.client.get(
            "/api/shop/party-statement-report/",
            {"type": "customer", "shop": self.shop.pk, "party": other_customer.pk},
        )
        self.assertEqual(response.status_code, 400)

    def test_customer_dual_currency_opening_balances_and_cross_settlement(self):
        # Customer with separate AFN and USD opening balances
        cust = Customer.objects.create(
            shop=self.shop,
            name="Dual Cust",
            opening_balance_afn=Decimal("7000.00"),
            opening_balance_usd=Decimal("100.00"),
        )
        # Pay 100 USD to settle 7000 AFN at rate 70 AFN/USD
        pay_res = self.client.post(
            "/api/shop/customer-balance-payments/",
            {
                "customer": cust.pk,
                "account": self.usd_account.pk,
                "currency": self.usd_currency.pk,
                "settlement_currency": self.currency.pk,
                "amount": 100,
                "discount": 0,
                "settlement_exchange_rate": 70,
                "apply_to_opening_balance": True,
            },
            format="json",
        )
        self.assertEqual(pay_res.status_code, 201, pay_res.data)
        pay_id = pay_res.data["id"]

        # Request party statement
        response = self.client.get(
            "/api/shop/party-statement-report/",
            {
                "type": "customer",
                "shop": self.shop.pk,
                "party": cust.pk,
                "start_date": self.today - timedelta(days=1),
                "end_date": self.today + timedelta(days=1),
            },
        )
        self.assertEqual(response.status_code, 200)
        # AFN opening balance 7000 was fully cleared by 7000 AFN allocation
        # USD opening balance 100 remains
        summaries = {s["currency"]: s for s in response.data["summary"]}
        self.assertEqual(summaries["AFN"]["closing_balance"], "0.00")
        self.assertEqual(summaries["USD"]["closing_balance"], "100.00")

        # Now void the payment
        void_res = self.client.post(
            f"/api/shop/customer-balance-payments/{pay_id}/void/",
            {"reason": "Bounced check"},
            format="json",
        )
        self.assertEqual(void_res.status_code, 200, void_res.data)

        # In party statement, voided payment appears with 0 debit, 0 credit, is_void=True
        # and AFN debt is restored to 7000.00
        response2 = self.client.get(
            "/api/shop/party-statement-report/",
            {
                "type": "customer",
                "shop": self.shop.pk,
                "party": cust.pk,
                "start_date": self.today - timedelta(days=1),
                "end_date": self.today + timedelta(days=1),
            },
        )
        self.assertEqual(response2.status_code, 200)
        summaries2 = {s["currency"]: s for s in response2.data["summary"]}
        self.assertEqual(summaries2["USD"]["closing_balance"], "100.00")
        self.assertEqual(summaries2["AFN"]["closing_balance"], "7000.00")

        # Verify voided payment row exists in output with is_void=True
        void_rows = [r for r in response2.data["rows"] if r.get("is_void")]
        self.assertTrue(len(void_rows) >= 1)
        self.assertEqual(void_rows[0]["debit"], "0.00")
        self.assertEqual(void_rows[0]["credit"], "0.00")
        self.assertIn("Cancelled", void_rows[0]["kind"])

    def test_supplier_party_statement_with_opening_and_void(self):
        supplier = Supplier.objects.create(
            shop=self.shop,
            name="Test Supplier",
            opening_balance_afn=Decimal("5000.00"),
        )
        pay_res = self.client.post(
            "/api/shop/supplier-balance-payments/",
            {
                "supplier": supplier.pk,
                "account": self.account.pk,
                "currency": self.currency.pk,
                "settlement_currency": self.currency.pk,
                "amount": 2000,
                "discount": 0,
                "settlement_exchange_rate": 1,
                "apply_to_opening_balance": True,
            },
            format="json",
        )
        self.assertEqual(pay_res.status_code, 201, pay_res.data)
        pay_id = pay_res.data["id"]

        response = self.client.get(
            "/api/shop/party-statement-report/",
            {
                "type": "supplier",
                "shop": self.shop.pk,
                "party": supplier.pk,
                "start_date": self.today - timedelta(days=1),
                "end_date": self.today + timedelta(days=1),
            },
        )
        self.assertEqual(response.status_code, 200)
        summaries = {s["currency"]: s for s in response.data["summary"]}
        self.assertEqual(summaries["AFN"]["closing_balance"], "3000.00")

        # Void supplier payment
        void_res = self.client.post(
            f"/api/shop/supplier-balance-payments/{pay_id}/void/",
            {"reason": "Erroneous wire"},
            format="json",
        )
        self.assertEqual(void_res.status_code, 200)

        response2 = self.client.get(
            "/api/shop/party-statement-report/",
            {
                "type": "supplier",
                "shop": self.shop.pk,
                "party": supplier.pk,
                "start_date": self.today - timedelta(days=1),
                "end_date": self.today + timedelta(days=1),
            },
        )
        self.assertEqual(response2.status_code, 200)
        summaries2 = {s["currency"]: s for s in response2.data["summary"]}
        self.assertEqual(summaries2["AFN"]["closing_balance"], "5000.00")

    def test_balance_payment_viewset_is_void_filter(self):
        cust = Customer.objects.create(
            shop=self.shop,
            name="Filter Cust",
            opening_balance_afn=Decimal("3000.00"),
        )
        pay1 = self.client.post(
            "/api/shop/customer-balance-payments/",
            {
                "customer": cust.pk,
                "account": self.account.pk,
                "currency": self.currency.pk,
                "settlement_currency": self.currency.pk,
                "amount": 1000,
                "settlement_exchange_rate": 1,
                "apply_to_opening_balance": True,
            },
            format="json",
        ).data
        pay2 = self.client.post(
            "/api/shop/customer-balance-payments/",
            {
                "customer": cust.pk,
                "account": self.account.pk,
                "currency": self.currency.pk,
                "settlement_currency": self.currency.pk,
                "amount": 500,
                "settlement_exchange_rate": 1,
                "apply_to_opening_balance": True,
            },
            format="json",
        ).data

        # Void pay2
        self.client.post(
            f"/api/shop/customer-balance-payments/{pay2['id']}/void/",
            {"reason": "Voiding pay2"},
            format="json",
        )

        # Filter is_void=false
        res_active = self.client.get(
            "/api/shop/customer-balance-payments/", {"is_void": "false"}
        )
        self.assertEqual(res_active.status_code, 200)
        active_ids = [p["id"] for p in (res_active.data.get("results") or res_active.data)]
        self.assertIn(pay1["id"], active_ids)
        self.assertNotIn(pay2["id"], active_ids)

        # Filter is_void=true
        res_voided = self.client.get(
            "/api/shop/customer-balance-payments/", {"is_void": "true"}
        )
        self.assertEqual(res_voided.status_code, 200)
        voided_ids = [p["id"] for p in (res_voided.data.get("results") or res_voided.data)]
        self.assertIn(pay2["id"], voided_ids)
        self.assertNotIn(pay1["id"], voided_ids)

    def test_payment_editing_notes_only_allowed_and_financial_locked(self):
        cust = Customer.objects.create(
            shop=self.shop,
            name="Edit Cust",
            opening_balance_afn=Decimal("2000.00"),
        )
        pay = self.client.post(
            "/api/shop/customer-balance-payments/",
            {
                "customer": cust.pk,
                "account": self.account.pk,
                "currency": self.currency.pk,
                "settlement_currency": self.currency.pk,
                "amount": 500,
                "settlement_exchange_rate": 1,
                "apply_to_opening_balance": True,
                "notes": "Initial note",
            },
            format="json",
        ).data

        # Updating notes only: succeeds
        patch_res = self.client.patch(
            f"/api/shop/customer-balance-payments/{pay['id']}/",
            {"notes": "Updated note only"},
            format="json",
        )
        self.assertEqual(patch_res.status_code, 200, patch_res.data)
        self.assertEqual(patch_res.data["notes"], "Updated note only")

        # Attempting to edit financial field (amount): rejected
        bad_res = self.client.patch(
            f"/api/shop/customer-balance-payments/{pay['id']}/",
            {"amount": 9999},
            format="json",
        )
        self.assertEqual(bad_res.status_code, 400)
        self.assertIn("cannot be modified", str(bad_res.data))

    def test_idempotency_duplicate_submission_prevented(self):
        cust = Customer.objects.create(
            shop=self.shop,
            name="Idemp Cust",
            opening_balance_afn=Decimal("5000.00"),
        )
        client_ref = "test-idemp-uuid-12345"
        payload = {
            "customer": cust.pk,
            "account": self.account.pk,
            "currency": self.currency.pk,
            "settlement_currency": self.currency.pk,
            "amount": 100,
            "settlement_exchange_rate": 1,
            "apply_to_opening_balance": True,
            "client_reference_id": client_ref,
        }
        res1 = self.client.post(
            "/api/shop/customer-balance-payments/",
            payload,
            format="json",
        )
        self.assertEqual(res1.status_code, 201)

        # Immediate retry with same client_reference_id
        res2 = self.client.post(
            "/api/shop/customer-balance-payments/",
            payload,
            format="json",
        )
        self.assertEqual(res2.status_code, 400)
        self.assertIn("has already been processed", str(res2.data))
