import uuid
from django.contrib.auth.models import Permission, Group
from django.core.exceptions import PermissionDenied, ValidationError
from django.db import connection
from django.test import TestCase, Client
from django.test.utils import CaptureQueriesContext
from django.urls import reverse
from apps.inventory.tests import grant
from apps.access.services import set_role_permissions
from .test_payment import PaymentFixture, shared_payment_data
from . import payment_queries as q
from .models import ServicePayment, ServicePaymentReceipt, PaymentReversal, ServiceFinancialRelease


class PaymentSecurityTests(PaymentFixture, TestCase):
    @classmethod
    def setUpTestData(cls): shared_payment_data(cls)

    def grant_finance(self, company=None, center=None, names=None):
        role = grant(self.user, company or self.company, center=center)
        perms = Permission.objects.filter(content_type__app_label="commercial")
        if names is not None: perms = perms.filter(codename__in=names)
        set_role_permissions(role=role, permissions=perms)
        return role

    def test_company_isolation(self):
        self.grant_finance(company=self.other_company)
        with self.assertRaises(PermissionDenied): self.pay(actor=self.user)
        self.assertFalse(q.settlement_invoices(actor=self.user).exists())

    def test_other_center_scope(self):
        self.grant_finance(center=self.center2)
        with self.assertRaises(PermissionDenied): self.pay(actor=self.user)
        self.assertFalse(q.service_payments(actor=self.user).exists())

    def test_same_path_scope_and_permission_required(self):
        self.grant_finance(company=self.other_company)
        self.grant_finance(names=[])
        with self.assertRaises(PermissionDenied): self.pay(actor=self.user)

    def test_receive_does_not_grant_reversal_or_release(self):
        self.grant_finance(names=["receive_servicepayment"])
        payment = self.pay(actor=self.user, amount="20")
        with self.assertRaises(PermissionDenied): self.reverse_payment(payment, actor=self.user)
        with self.assertRaises(PermissionDenied): self.release_due(actor=self.user)

    def test_staff_group_and_direct_permissions_do_not_create_business_scope(self):
        self.user.is_staff = True
        self.user.save()
        perms = Permission.objects.filter(content_type__app_label="commercial")
        self.user.user_permissions.set(perms)
        group = Group.objects.create(name="Synthetic cashier group")
        group.permissions.set(perms)
        self.user.groups.add(group)
        with self.assertRaises(PermissionDenied): self.pay(actor=self.user)
        self.assertFalse(q.settlement_invoices(actor=self.user).exists())

    def test_engineer_assignment_is_not_financial_authority(self):
        with self.assertRaises(PermissionDenied): self.pay(actor=self.engineer)
        with self.assertRaises(PermissionDenied): self.release_due(actor=self.engineer)

    def test_inactive_actor_rejected(self):
        from .payment_services import receive_service_payment
        revision = self.summary()["revision"]
        self.actor.is_active = False
        self.actor.save()
        with self.assertRaises(ValidationError):
            receive_service_payment(actor=self.actor, invoice=self.invoice,
                expected_revision=revision, amount="100", method="CASH")
        self.assertFalse(ServicePayment.objects.exists())

    def test_inactive_center_blocks_financial_writes(self):
        from apps.organization.services import deactivate_service_center
        deactivate_service_center(service_center=self.center)
        with self.assertRaises(ValidationError): self.pay()

    def test_read_queries_do_not_leak_receipts(self):
        self.pay()
        self.grant_finance(company=self.other_company)
        self.assertFalse(q.receipt_history(actor=self.user).exists())
        self.assertEqual(q.outstanding_customer_balance(actor=self.user, customer=self.customer, currency="BDT"), 0)


class PaymentAdminTests(PaymentFixture, TestCase):
    @classmethod
    def setUpTestData(cls): shared_payment_data(cls)

    def setUp(self): self.client.force_login(self.actor)

    def url(self): return reverse("admin:commercial_invoice_settlement", args=[self.invoice.pk])

    def post(self, operation, **data):
        token = self.client.get(self.url()).context_data["revision_token"]
        return self.client.post(self.url(), dict(operation=operation, revision_token=token)|data)

    def test_receiving_and_receipt_view(self):
        self.assertEqual(self.post("receive", amount="40", method="CASH").status_code, 302)
        receipt = ServicePaymentReceipt.objects.get()
        self.assertEqual(self.client.get(reverse("admin:commercial_servicepaymentreceipt_change", args=[receipt.pk])).status_code, 200)
        self.assertEqual(self.summary()["balance"], 60)

    def test_admin_reversal_and_due_release(self):
        payment = self.pay(amount="40")
        self.assertEqual(self.post("reverse", payment=str(payment.pk), reason="Synthetic duplicate entry").status_code, 302)
        self.assertEqual(self.post("release", reason="Approved synthetic collection arrangement").status_code, 302)
        self.assertEqual(self.summary()["balance"], 100)
        self.assertTrue(self.summary()["financially_clear"])

    def test_csrf_rejects_mutation(self):
        client = Client(enforce_csrf_checks=True)
        client.force_login(self.actor)
        self.assertEqual(client.post(self.url(), dict(operation="receive", amount="100", method="CASH")).status_code, 403)
        self.assertFalse(ServicePayment.objects.exists())

    def test_stale_payment_and_reversal_forms(self):
        token = self.client.get(self.url()).context_data["revision_token"]
        payment = self.pay(amount="10")
        for operation in ("receive", "reverse", "release"):
            response = self.client.post(self.url(), dict(operation=operation, revision_token=token, amount="10", method="CASH", payment=str(payment.pk), reason="Synthetic"))
            self.assertEqual(response.status_code, 400)
        self.assertEqual(self.summary()["paid"], 10)

    def test_tampered_signature_and_forged_payment(self):
        self.assertEqual(self.client.post(self.url(), dict(operation="receive", revision_token="tampered", amount="10", method="CASH")).status_code, 400)
        self.assertEqual(self.post("reverse", payment=str(uuid.uuid4()), reason="Forged").status_code, 400)
        self.assertFalse(PaymentReversal.objects.exists())

    def test_overpayment_and_hidden_allocation_input(self):
        self.assertEqual(self.post("receive", amount="101", method="CASH").status_code, 400)
        self.assertEqual(self.post("receive", amount="40", method="CASH", currency="USD", invoice=str(uuid.uuid4()), allocated_amount="100").status_code, 302)
        payment = ServicePayment.objects.get()
        self.assertEqual((payment.currency, payment.invoice_id, payment.allocation.amount), ("BDT", self.invoice.pk, 40))

    def test_notes_render_as_text(self):
        self.pay(amount="10", note="<script>alert('synthetic')</script>")
        self.release_due(reason="<script>alert('synthetic')</script>")
        response = self.client.get(self.url())
        self.assertContains(response, "&lt;script&gt;")
        self.assertNotContains(response, "<script>alert")

    def test_financial_evidence_cannot_be_deleted(self):
        payment = self.pay()
        for model, row in (("servicepayment", payment), ("servicepaymentreceipt", payment.receipt)):
            self.assertEqual(self.client.post(reverse(f"admin:commercial_{model}_delete", args=[row.pk]), {"post":"yes"}).status_code, 403)

    def test_native_permission_and_business_scope_both_required(self):
        self.user.is_staff = True
        self.user.save()
        self.client.force_login(self.user)
        self.assertEqual(self.client.get(self.url()).status_code, 403)
        self.user.user_permissions.set(Permission.objects.filter(content_type__app_label="commercial"))
        self.assertEqual(self.client.get(self.url()).status_code, 404)

    def test_payment_display_query_budget_no_n_plus_one(self):
        self.client.get(self.url())
        with CaptureQueriesContext(connection) as empty:
            self.client.get(self.url())
        self.pay(amount="20")
        self.pay(amount="20")
        with CaptureQueriesContext(connection) as populated:
            response = self.client.get(self.url())
        self.assertEqual(response.status_code, 200)
        self.assertLessEqual(len(populated), len(empty))
        self.assertLessEqual(len(populated), 20)
