"""Synthetic payment evidence, balance derivation and delivery clearance."""
from decimal import Decimal
from unittest.mock import patch
from django.core.exceptions import ValidationError
from django.db import IntegrityError, transaction, connection
from django.test import TestCase
from apps.inventory.test_usage import setup_usage
from apps.service import test_handover as handover
from .test_invoice import InvoiceFixture
from . import payment_services as s, payment_queries as q
from .models import ServicePayment, PaymentAllocation, ServicePaymentReceipt, PaymentReversal, ReceiptSequence, ServiceFinancialRelease, ServiceInvoice


class PaymentFixture(InvoiceFixture):
    def summary(self, **kwargs):
        return q.invoice_settlement_summary(**(dict(actor=self.actor, invoice=self.invoice)|kwargs))

    def pay(self, **kwargs):
        return s.receive_service_payment(**(dict(actor=self.actor, invoice=self.invoice, expected_revision=self.summary()["revision"], amount="100", method="CASH")|kwargs))

    def reverse_payment(self, payment, **kwargs):
        return s.reverse_service_payment(**(dict(actor=self.actor, payment=payment, expected_revision=self.summary()["revision"], reason="Synthetic incorrect posting")|kwargs))

    def release_due(self, **kwargs):
        return s.authorize_due_release(**(dict(actor=self.actor, invoice=self.invoice, expected_revision=self.summary()["revision"], reason="Authorized synthetic collection arrangement")|kwargs))

    def deliver(self):
        self.received = []
        handover.released(self)
        return handover.handover(self)


def setup_payment(test, *, responsibility="CUSTOMER"):
    setup_usage(test)
    row = test.billed(responsibility=responsibility)
    test.ready()
    test.invoice = test.finalize(row)
    test.received = []


def shared_payment_data(cls):
    fixture = PaymentFixture()
    setup_payment(fixture)
    for name, value in vars(fixture).items(): setattr(cls, name, value)


class PaymentTests(PaymentFixture, TestCase):
    @classmethod
    def setUpTestData(cls): shared_payment_data(cls)

    def test_initial_customer_liability_is_unpaid(self):
        self.assertEqual((self.summary()["state"], self.summary()["balance"]), ("UNPAID", 100))

    def test_partial_and_multiple_methods_exact_settlement(self):
        first = self.pay(amount="40")
        self.assertEqual((self.summary()["state"], self.summary()["balance"]), ("PARTIALLY_PAID", 60))
        second = self.pay(amount="60", method="CARD", reference="SYNTHETIC-CARD-1")
        self.assertEqual((self.summary()["state"], self.summary()["paid"], self.summary()["balance"]), ("PAID", 100, 0))
        self.assertEqual(first.receipt.number, "RCT-00000001")
        self.assertEqual(second.receipt.number, "RCT-00000002")
        self.assertEqual(second.allocation.invoice, self.invoice)
        self.assertEqual(second.allocation.amount, second.amount)

    def test_overpayment_rejected_without_credit_or_receipt(self):
        self.pay(amount="60")
        with self.assertRaises(ValidationError): self.pay(amount="41")
        self.assertEqual(ServicePayment.objects.count(), 1)
        self.assertEqual(ReceiptSequence.objects.get().next_value, 2)
        self.assertEqual(self.summary()["balance"], 40)

    def test_zero_negative_float_and_overprecision_rejected(self):
        for amount in ("0", "-1", 1.5, True, "1.001", "NaN", "Infinity", "1000000000000"):
            with self.subTest(amount=amount), self.assertRaises(ValidationError): self.pay(amount=amount)
        self.assertFalse(ServicePayment.objects.exists())

    def test_explicit_methods_and_reference_requirements(self):
        for method in ("CARD", "BANK_TRANSFER", "MOBILE_FINANCIAL_SERVICE", "OTHER", "INVALID"):
            with self.subTest(method=method), self.assertRaises(ValidationError): self.pay(method=method)
        for method in ("CARD", "BANK_TRANSFER", "MOBILE_FINANCIAL_SERVICE", "OTHER"):
            self.pay(amount="10", method=method, reference=f"SYNTHETIC-{method}", note="Synthetic method explanation")
        self.assertEqual(self.summary()["paid"], 40)

    def test_duplicate_reference_remains_reserved_after_void(self):
        payment = self.pay(method="BANK_TRANSFER", reference="SYNTHETIC-BANK")
        self.reverse_payment(payment)
        with self.assertRaises(ValidationError): self.pay(method="BANK_TRANSFER", reference="SYNTHETIC-BANK")
        self.assertEqual(self.summary()["state"], "UNPAID")

    def test_reversal_restores_due_preserves_receipt_and_allocation(self):
        payment = self.pay()
        receipt = dict(ServicePaymentReceipt.objects.get().customer_snapshot)
        reversal = self.reverse_payment(payment)
        payment.refresh_from_db()
        self.assertEqual(payment.status, "VOIDED")
        self.assertEqual(reversal.amount, 100)
        self.assertEqual(self.summary()["balance"], 100)
        self.assertEqual(PaymentAllocation.objects.count(), 1)
        self.assertEqual(ServicePaymentReceipt.objects.get().customer_snapshot, receipt)
        self.assertEqual(self.pay().receipt.number, "RCT-00000002")

    def test_partial_reversal_derives_partial_balance(self):
        first = self.pay(amount="40")
        self.pay(amount="60")
        self.reverse_payment(first)
        self.assertEqual((self.summary()["state"], self.summary()["balance"]), ("PARTIALLY_PAID", 40))

    def test_reason_required_and_repeated_reversal_rejected(self):
        payment = self.pay()
        with self.assertRaises(ValidationError): self.reverse_payment(payment, reason="")
        self.reverse_payment(payment)
        with self.assertRaises(ValidationError): self.reverse_payment(payment)
        self.assertEqual(PaymentReversal.objects.count(), 1)

    def test_stale_revision_rejected_even_when_amount_would_fit(self):
        token = self.summary()["revision"]
        self.pay(amount="20")
        with self.assertRaises(ValidationError): self.pay(amount="20", expected_revision=token)
        self.assertEqual(self.summary()["paid"], 20)

    def test_stale_reversal_after_other_payment(self):
        payment = self.pay(amount="30")
        token = self.summary()["revision"]
        self.pay(amount="30")
        with self.assertRaises(ValidationError): self.reverse_payment(payment, expected_revision=token)
        self.assertEqual(self.summary()["paid"], 60)

    def test_unpaid_customer_blocks_delivery(self):
        with self.assertRaises(ValidationError): self.deliver()
        self.case.refresh_from_db()
        self.assertEqual(self.case.status, "READY_FOR_DELIVERY")

    def test_partial_customer_payment_blocks_delivery(self):
        self.pay(amount="50")
        with self.assertRaises(ValidationError): self.deliver()

    def test_full_payment_permits_delivery(self):
        self.pay()
        self.deliver()
        self.case.refresh_from_db()
        self.assertEqual(self.case.status, "DELIVERED")

    def test_due_release_allows_delivery_without_reducing_balance(self):
        self.pay(amount="20")
        release = self.release_due()
        self.assertEqual(release.outstanding_amount, 80)
        self.deliver()
        self.assertEqual((self.summary()["state"], self.summary()["balance"]), ("PARTIALLY_PAID", 80))

    def test_reversal_can_exceed_release_limit_and_block_handover(self):
        payment = self.pay(amount="60")
        self.release_due()
        self.reverse_payment(payment)
        self.assertFalse(self.summary()["financially_clear"])
        with self.assertRaises(ValidationError): self.deliver()

    def test_post_delivery_unsecured_reversal_rejected_atomically(self):
        payment = self.pay()
        self.deliver()
        with self.assertRaises(ValidationError): self.reverse_payment(payment)
        self.assertEqual(self.summary()["state"], "PAID")
        self.assertFalse(PaymentReversal.objects.exists())

    def test_post_delivery_reversal_with_prior_adequate_release(self):
        self.release_due()
        payment = self.pay()
        self.deliver()
        self.reverse_payment(payment)
        self.assertEqual(self.summary()["balance"], 100)
        self.assertTrue(self.summary()["financially_clear"])

    def test_payment_after_due_release_delivery_settles_debt(self):
        self.release_due()
        self.deliver()
        self.pay()
        self.assertEqual(self.summary()["state"], "PAID")

    def test_paid_and_duplicate_due_release_rejected(self):
        self.release_due()
        with self.assertRaises(ValidationError): self.release_due()
        self.pay()
        with self.assertRaises(ValidationError): self.release_due()

    def test_invoice_and_technical_evidence_unchanged(self):
        from apps.inventory.models import StockMovement
        from .models import InvoiceLine, ServiceQuotation, QuotationLine
        models = (ServiceInvoice, InvoiceLine, ServiceQuotation, QuotationLine, StockMovement)
        before = [list(model.objects.values()) for model in models]
        self.release_due()
        payment = self.pay()
        self.reverse_payment(payment)
        self.assertEqual(before, [list(model.objects.values()) for model in models])

    def test_receipt_customer_snapshot_survives_profile_change(self):
        from apps.customers.services import update_customer
        payment = self.pay()
        original = payment.receipt.customer_snapshot.copy()
        update_customer(customer=self.customer, full_name="Changed synthetic customer")
        self.assertEqual(ServicePaymentReceipt.objects.get().customer_snapshot, original)

    def test_receiving_historical_debt_with_inactive_customer_device(self):
        from apps.customers.services import deactivate_customer
        from apps.devices.services import deactivate_device
        deactivate_customer(customer=self.customer)
        deactivate_device(device=self.device)
        self.pay()
        self.assertEqual(self.summary()["state"], "PAID")

    def test_posted_evidence_immutable(self):
        payment = self.pay()
        for row in (payment, payment.allocation, payment.receipt):
            with self.assertRaises(ValidationError): row.save()
            with self.assertRaises(ValidationError): row.delete()
        for model, values in ((ServicePayment, dict(amount=90)), (PaymentAllocation, dict(amount=90)), (ServicePaymentReceipt, dict(reference="rewrite"))):
            with self.assertRaises(IntegrityError), transaction.atomic(): model.objects.update(**values)

    def test_reversal_and_release_history_immutable(self):
        self.release_due()
        self.reverse_payment(self.pay())
        for model in (PaymentReversal, ServiceFinancialRelease):
            with self.assertRaises(IntegrityError), transaction.atomic(): model.objects.update(reason="rewrite")
            with self.assertRaises(ValidationError): model.objects.all().delete()

    def test_database_rejects_status_without_reversal(self):
        self.pay()
        with self.assertRaises(IntegrityError), transaction.atomic():
            ServicePayment.objects.update(status="VOIDED")
            with connection.cursor() as cursor: cursor.execute("SET CONSTRAINTS ALL IMMEDIATE")

    def test_rollback_at_every_posting_stage(self):
        for model in (ServicePayment, PaymentAllocation, ServicePaymentReceipt):
            original = model._persist
            def fail(row, original=original):
                original(row)
                raise RuntimeError("Synthetic failure after persistence")
            with self.subTest(model=model), patch.object(model, "_persist", fail), self.assertRaises(RuntimeError): self.pay()
            self.assertFalse(ServicePayment.objects.exists())
            self.assertFalse(PaymentAllocation.objects.exists())
            self.assertFalse(ServicePaymentReceipt.objects.exists())
            self.assertFalse(ReceiptSequence.objects.exists())
        self.assertEqual(self.pay().receipt.number, "RCT-00000001")

    def test_release_and_reversal_rollback(self):
        with self.assertRaises(RuntimeError), transaction.atomic():
            self.release_due()
            raise RuntimeError("Synthetic abort")
        self.assertFalse(ServiceFinancialRelease.objects.exists())
        payment = self.pay()
        with self.assertRaises(RuntimeError), transaction.atomic():
            self.reverse_payment(payment)
            raise RuntimeError("Synthetic abort")
        self.assertEqual(self.summary()["state"], "PAID")

    def test_query_budgets_for_lists_queues_and_receipts(self):
        payment = self.pay(amount="40")
        self.pay(amount="20")
        with self.assertNumQueries(1): self.summary()
        with self.assertNumQueries(1):
            for row in q.service_payments(actor=self.actor):
                str(row.receipt); str(row.received_by); str(row.invoice); str(row.invoice.service_case.customer); row.allocation.amount
        with self.assertNumQueries(1): list(q.payments_for_invoice(actor=self.actor, invoice=self.invoice))
        with self.assertNumQueries(1): q.payment_detail(actor=self.actor, payment=payment)
        with self.assertNumQueries(1): list(q.receipt_history(actor=self.actor))
        with self.assertNumQueries(1): list(q.unsettled_invoices(actor=self.actor))
        with self.assertNumQueries(1): list(q.financially_cleared_delivery_queue(actor=self.actor))
        with self.assertNumQueries(1): self.assertEqual(q.outstanding_customer_balance(actor=self.actor, customer=self.customer, currency="BDT"), 40)


class PaymentCompatibilityTests(PaymentFixture, TestCase):
    def test_warranty_invoice_needs_no_payment(self):
        setup_payment(self, responsibility="WARRANTY")
        self.assertEqual(self.summary()["state"], "NO_CUSTOMER_DUE")
        with self.assertRaises(ValidationError): self.pay(amount="1")
        self.deliver()
        self.assertFalse(ServicePayment.objects.exists())
        self.assertFalse(ServicePaymentReceipt.objects.exists())

    def test_company_invoice_needs_no_payment(self):
        setup_payment(self, responsibility="COMPANY")
        self.assertEqual(self.summary()["state"], "NO_CUSTOMER_DUE")
        self.deliver()
        self.assertFalse(ServicePayment.objects.exists())

    def test_invoice_free_workflow_preserved(self):
        from apps.inventory.test_usage import deliver_case
        setup_usage(self)
        deliver_case(self)
        self.assertEqual(self.case.status, "DELIVERED")
        self.assertFalse(ServiceInvoice.objects.exists())

    def test_draft_invoice_cannot_receive_payment(self):
        setup_usage(self)
        row = self.billed()
        with self.assertRaises(ValidationError):
            s.receive_service_payment(actor=self.actor, invoice=row, expected_revision=f"{row.pk}:0", amount="1", method="CASH")
        self.assertFalse(ServicePayment.objects.exists())
