"""Phase 6D read/presentation contracts and reuse of authoritative commercial actions."""
import os
from pathlib import Path
from datetime import datetime, timezone as datetime_timezone
from unittest.mock import patch

from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.core.exceptions import ValidationError
from django.db import connection
from django.test import Client, TestCase
from django.test.utils import CaptureQueriesContext
from django.urls import reverse
from django.utils import timezone

from apps.commercial import test_payment as fixture
from apps.commercial.models import ServicePayment, ServicePaymentReceipt, PaymentReversal, PaymentAllocation
from apps.access.services import set_role_permissions
from apps.access.services import create_role_assignment
from apps.access.models import Role
from apps.organization.assignment_services import create_assignment
from apps.organization.models import Department
from apps.service import test_handover as handover
from apps.inventory import test_usage as usage
from apps.service import test_diagnosis as diagnosis
from apps.service import test_engineer_assignment as assignment
from apps.service.tests import intake
from apps.devices import services as devices
from . import tests as baseline
from .commercial_workspace import url, KINDS
from .test_ui import capture_page


def capture(response, name):
    capture_page(response, "commercial-" + name)
    if os.environ.get("CCARE_UI_CAPTURE"):
        path = Path(os.environ["CCARE_UI_CAPTURE"]) / ("commercial-" + name + ".html")
        html = path.read_text(encoding="utf-8")
        css = (Path(settings.BASE_DIR) / "apps/operations/static/operations/receipt.css").read_text()
        path.write_text(html.replace('<link rel="stylesheet" href="/static/operations/receipt.css">', '<style>' + css + '</style>'), encoding="utf-8")


class CommercialWorkspaceTests(fixture.PaymentFixture, TestCase):
    @classmethod
    def setUpTestData(cls):
        fixture.shared_payment_data(cls)
        cls.reader = get_user_model().objects.create_user(username="commercial-workspace-reader")
        cls.read_role = baseline.grant(cls.reader, cls.company, ["commercial.view_servicequotation", "commercial.view_serviceinvoice",
            "commercial.view_servicepayment", "service.view_servicecase"], cls.center)

    def setUp(self):
        self.client.force_login(self.reader)

    def operation(self, op):
        return reverse("operations:commercial_workflow", args=[self.case.pk, op])

    def test_navigation_and_all_collections(self):
        for kind in ["overview", *KINDS]:
            response = self.client.get(url(kind))
            self.assertEqual(response.status_code, 200, kind)
            self.assertContains(response, "Commercial overview")
            self.assertContains(response, "Due release does not reduce debt")
            capture(response, "empty-" + kind)

    def test_invoice_and_quotation_details_use_recorded_evidence(self):
        for kind, row in (("quotations", self.quote), ("invoices", self.invoice)):
            response = self.client.get(url(kind, row.pk))
            self.assertEqual(response.status_code, 200)
            self.assertNotContains(response, self.customer.display_name)
            self.assertContains(response, self.case.job_number)
            capture(response, kind + "-detail")
        self.assertContains(self.client.get(url("quotations", self.quote.pk)), "Recorded customer decision")
        self.assertContains(self.client.get(url("invoices", self.invoice.pk)), "Technical consumption allocations")

    def test_overview_metrics_are_record_counts_not_revenue(self):
        self.pay(amount="10")
        response = self.client.get(url())
        metrics = {label: value for label, value, _ in response.context["metrics"]}
        self.assertEqual(metrics["Finalized invoices with outstanding customer balance"], 1)
        self.assertEqual(metrics["Posted payments received today (local date)"], 1)
        self.assertEqual(metrics["Draft invoices awaiting finalization"], 0)
        self.assertEqual(metrics["Current quotations awaiting customer decision"], 0)
        capture(response, "overview-populated")

    def test_outstanding_uses_authoritative_summary(self):
        self.pay(amount="30")
        response = self.client.get(url("outstanding"))
        facts = dict(response.context["cards"][0]["facts"])
        summary = self.summary()
        self.assertEqual((facts["Posted payment allocations"], facts["Outstanding balance"]), (summary["paid"], summary["balance"]))
        self.assertEqual(facts["Financial clearance"], "Financially blocked")
        capture(response, "outstanding-partial")

    def test_today_metric_uses_application_local_date_and_posted_state(self):
        instants = [(1, 17, 59), (1, 18, 1), (1, 19, 1), (1, 20, 1), (2, 18, 1)]
        recorded = []
        with timezone.override("Asia/Dhaka"):
            for day, hour, minute in instants:
                instant = datetime(2030, 1, day, hour, minute, tzinfo=datetime_timezone.utc)
                with patch("django.utils.timezone.now", return_value=instant):
                    recorded.append(self.pay(amount="10"))
            today = datetime(2030, 1, 2, 0, 30, tzinfo=datetime_timezone.utc)
            with patch("django.utils.timezone.now", return_value=today):
                self.reverse_payment(recorded[3])
                # Create the session under the same clock as the metric request.
                self.client.force_login(self.reader)
                response = self.client.get(url())
            metrics = {label: value for label, value, _ in response.context["metrics"]}
            self.assertEqual(metrics["Posted payments received today (local date)"], 2)
            response = self.client.get(url("payments"), {"start": "2030-01-02", "end": "2030-01-02"})
            self.assertEqual({card["url"] for card in response.context["cards"]},
                {url("payments", row.pk) for row in recorded[1:4]})

    def test_clearance_matrix_and_due_release_do_not_rewrite_debt(self):
        def review(balance, state, clear):
            summary = self.summary()
            self.assertEqual((summary["balance"], summary["state"], summary["financially_clear"]), (balance, state, clear))
            response = self.client.get(url("invoices", self.invoice.pk))
            facts = dict(response.context["card"]["facts"])
            self.assertEqual((facts["Outstanding balance"], facts["Settlement state"]), (balance, state))
            capture(response, "review-clearance-" + state + ("-released" if clear and balance else ""))
        with self.assertRaises(ValidationError): self.deliver()
        review(100, "UNPAID", False)
        self.pay(amount="30")
        review(70, "PARTIALLY_PAID", False)
        with self.assertRaises(ValidationError): handover.handover(self)
        final_payment = self.pay(amount="70")
        review(0, "PAID", True)
        self.reverse_payment(final_payment)
        review(70, "PARTIALLY_PAID", False)
        with self.assertRaises(ValidationError): handover.handover(self)
        before = (ServicePayment.objects.count(), ServicePaymentReceipt.objects.count(), PaymentAllocation.objects.count())
        self.release_due()
        review(70, "PARTIALLY_PAID", True)
        self.assertEqual((ServicePayment.objects.count(), ServicePaymentReceipt.objects.count(), PaymentAllocation.objects.count()), before)
        handover.handover(self)
        self.case.refresh_from_db()
        self.assertEqual(self.case.status, "DELIVERED")
        self.assertEqual(self.summary()["balance"], 70)

    def test_due_release_retains_outstanding_and_history(self):
        release = self.release_due()
        response = self.client.get(url("outstanding"))
        self.assertContains(response, "Authorized due release; balance remains due")
        self.assertEqual(self.summary()["balance"], 100)
        self.assertFalse(ServicePayment.objects.exists())
        capture(response, "outstanding-release")
        response = self.client.get(url("releases", release.pk))
        self.assertContains(response, release.reason)
        self.assertContains(response, self.actor.username)
        capture(response, "release-detail")

    def test_receipt_reuses_identity_and_retains_reversal(self):
        payment = self.pay(amount="25")
        self.reverse_payment(payment)
        response = self.client.get(url("receipts", payment.receipt.pk))
        self.assertContains(response, payment.receipt.number)
        self.assertContains(response, "payment reversed, not deleted")
        self.assertEqual(ServicePaymentReceipt.objects.count(), 1)
        self.assertEqual(PaymentReversal.objects.count(), 1)
        self.assertNotContains(response, self.customer.display_name)
        self.assertContains(response, "receipt.css")
        capture(response, "receipt-reversed")
        capture(self.client.get(url("payments", payment.pk)), "payment-reversed")

    def test_customer_snapshot_requires_independent_company_permission(self):
        payment = self.pay()
        baseline.grant(self.reader, self.company, ["customers.view_customer"])
        response = self.client.get(url("receipts", payment.receipt.pk))
        self.assertContains(response, payment.receipt.customer_snapshot["name"])
        self.assertContains(response, reverse("operations:detail", args=["customers", self.customer.pk]))
        capture(response, "receipt-print")

    def test_payment_only_scope_hides_job_customer_and_invoice_destination(self):
        payment = self.pay()
        actor = get_user_model().objects.create_user(username="payment-only-reader")
        baseline.grant(actor, self.company, ["commercial.view_servicepayment"], self.center)
        self.client.force_login(actor)
        for kind, pk in (("payments", payment.pk), ("receipts", payment.receipt.pk)):
            response = self.client.get(url(kind, pk))
            self.assertEqual(response.status_code, 200)
            self.assertNotContains(response, self.case.job_number)
            self.assertNotContains(response, self.customer.display_name)
            self.assertNotContains(response, url("invoices", self.invoice.pk))
            self.assertNotContains(response, self.operation("payment-receive"))
            capture(response, "limited-" + kind)

    def test_invoice_only_does_not_disclose_settlement(self):
        actor = get_user_model().objects.create_user(username="invoice-only-reader")
        baseline.grant(actor, self.company, ["commercial.view_serviceinvoice"], self.center)
        self.client.force_login(actor)
        response = self.client.get(url("invoices", self.invoice.pk))
        self.assertNotContains(response, "Posted payment allocations")
        self.assertNotContains(response, "Financial clearance")
        self.assertEqual(self.client.get(url("payments")).status_code, 403)

    def test_cross_company_and_sibling_center_read_and_post_isolation(self):
        payment = self.pay(amount="10")
        for company, center in ((self.other_company, self.other_center), (self.company, self.center2)):
            actor = get_user_model().objects.create_user(username="outsider-" + str(center.pk))
            baseline.grant(actor, company, ["commercial.view_servicequotation", "commercial.view_serviceinvoice", "commercial.view_servicepayment", "commercial.receive_servicepayment", "service.view_servicecase"], center)
            self.client.force_login(actor)
            for kind, pk in (("quotations", self.quote.pk), ("invoices", self.invoice.pk), ("payments", payment.pk), ("receipts", payment.receipt.pk)):
                self.assertEqual(self.client.get(url(kind, pk)).status_code, 404)
                self.assertEqual(len(self.client.get(url(kind)).context["cards"]), 0)
            self.assertEqual(self.client.post(self.operation("payment-receive"), {"amount": "1", "method": "CASH"}).status_code, 404)

    def test_read_only_pages_reject_post_and_anonymous(self):
        self.assertEqual(self.client.post(url("invoices", self.invoice.pk)).status_code, 405)
        self.client.logout()
        self.assertEqual(self.client.get(url()).status_code, 302)

    def test_department_scope_does_not_expand_to_center_financial_records(self):
        actor = get_user_model().objects.create_user(username="department-finance")
        department = Department.objects.create(company=self.company, code="FIN", name="Finance")
        path = create_assignment(user=actor, company=self.company, department=department)
        role = Role.objects.create(code="DEPT-FIN", name="Department only")
        set_role_permissions(role=role, permissions=Permission.objects.filter(content_type__app_label="commercial"))
        create_role_assignment(user=actor, role=role, organization_assignment=path)
        self.client.force_login(actor)
        self.assertEqual(self.client.get(url("invoices", self.invoice.pk)).status_code, 404)
        self.assertEqual(self.client.get(url()).status_code, 403)

    def test_center_plus_department_retains_frozen_explicit_center_scope(self):
        actor = get_user_model().objects.create_user(username="center-department-finance")
        department = Department.objects.create(company=self.company, code="FIN", name="Finance")
        path = create_assignment(user=actor, company=self.company, region=self.region, service_center=self.center, department=department)
        role = Role.objects.create(code="CENTER-DEPT-FIN", name="Explicit center and department")
        set_role_permissions(role=role, permissions=Permission.objects.filter(content_type__app_label="commercial", codename="view_serviceinvoice"))
        create_role_assignment(user=actor, role=role, organization_assignment=path)
        self.client.force_login(actor)
        response = self.client.get(url("invoices", self.invoice.pk))
        self.assertEqual(response.status_code, 200)
        self.assertNotContains(response, self.customer.display_name)

    def test_related_job_links_require_permission_in_that_jobs_center(self):
        actor = get_user_model().objects.create_user(username="split-commercial-scopes")
        baseline.grant(actor, self.company, ["commercial.view_serviceinvoice", "service.view_servicecase"], self.center)
        baseline.grant(actor, self.company, ["commercial.view_servicequotation", "commercial.view_servicepayment"], self.center2)
        self.client.force_login(actor)
        response = self.client.get(url("invoices", self.invoice.pk))
        labels = [label for label, _ in response.context["card"]["links"]]
        self.assertNotIn("Quotations for this job", labels)
        self.assertNotIn("Payments for this job", labels)
        self.assertIn("Invoices for this job", labels)

    def test_financial_manager_authority_does_not_imply_collection(self):
        actor = get_user_model().objects.create_user(username="limited-financial-manager")
        baseline.grant(actor, self.company, ["commercial.view_serviceinvoice", "commercial.view_servicepayment", "service.view_servicecase",
            "commercial.reverse_servicepayment", "commercial.authorize_due_release"], self.center)
        self.client.force_login(actor)
        response = self.client.get(url("invoices", self.invoice.pk))
        self.assertContains(response, self.operation("payment-release"))
        self.assertContains(response, self.operation("payment-reverse"))
        self.assertNotContains(response, self.operation("payment-receive"))
        self.assertEqual(self.client.post(self.operation("payment-receive"), {}).status_code, 403)
        capture(response, "manager-invoice")

    def test_financial_notes_are_escaped(self):
        release = self.release_due(reason="<script>alert('untrusted')</script>")
        response = self.client.get(url("releases", release.pk))
        self.assertContains(response, "&lt;script&gt;")
        self.assertNotContains(response, "<script>alert")

    def test_filters_validate_without_cross_scope_or_unbounded_history(self):
        response = self.client.get(url("payments"), {"start": "2026-02-02", "end": "2026-01-01"})
        self.assertTrue(response.context["form"].errors)
        self.assertEqual(response.context["cards"], [])
        capture(response, "filter-error")
        for _ in range(26): self.pay(amount="1")
        first = self.client.get(url("payments"))
        second = self.client.get(url("payments"), {"page": 2})
        self.assertEqual((len(first.context["cards"]), len(second.context["cards"])), (25, 1))
        self.assertNotEqual(first.context["cards"][0]["url"], second.context["cards"][0]["url"])
        capture(first, "payment-history-populated")

    def test_cashier_does_not_gain_due_release_or_reversal(self):
        set_role_permissions(role=self.read_role, permissions=[*self.read_role.permissions.all(),
            Permission.objects.get(content_type__app_label="commercial", codename="receive_servicepayment")])
        response = self.client.get(url("invoices", self.invoice.pk))
        self.assertContains(response, self.operation("payment-receive"))
        for operation in ("payment-release", "payment-reverse", "invoice-finalize"):
            self.assertNotContains(response, self.operation(operation))
            self.assertEqual(self.client.post(self.operation(operation), {}).status_code, 403)
        capture(response, "cashier-invoice")

    def test_payment_form_replay_stale_and_overpayment(self):
        self.client.force_login(self.actor)
        target = self.operation("payment-receive")
        form = self.client.get(target)
        capture(form, "receive-form")
        token = form.context["revision"]
        self.assertEqual(self.client.post(target, {"revision": token, "amount": "30", "method": "CASH"}).status_code, 302)
        self.assertEqual(self.client.post(target, {"revision": token, "amount": "30", "method": "CASH"}).status_code, 400)
        token = self.client.get(target).context["revision"]
        response = self.client.post(target, {"revision": token, "amount": "71", "method": "CASH"})
        self.assertEqual(response.status_code, 400)
        self.assertEqual(self.summary()["paid"], 30)
        capture(response, "overpayment-error")

    def test_manager_existing_due_release_and_reversal_forms(self):
        self.client.force_login(self.actor)
        payment = self.pay(amount="10")
        for op, data in (("payment-release", {"reason": "Approved collection arrangement"}),
                         ("payment-reverse", {"payment": payment.pk, "reason": "Incorrect original posting"})):
            target = self.operation(op)
            response = self.client.get(target)
            capture(response, op)
            result = self.client.post(target, {"revision": response.context["revision"], **data})
            self.assertEqual(result.status_code, 302)
        self.assertEqual(self.summary()["balance"], 100)
        self.assertFalse(self.summary()["financially_clear"])

    def test_csrf_blocks_payment(self):
        client = Client(enforce_csrf_checks=True)
        client.force_login(self.actor)
        self.assertEqual(client.post(self.operation("payment-receive"), {"amount": "1", "method": "CASH"}).status_code, 403)
        self.assertFalse(ServicePayment.objects.exists())

    def test_delivery_remains_blocked_until_authoritative_clearance(self):
        with self.assertRaises(ValidationError): self.deliver()
        self.pay()
        handover.handover(self)
        self.case.refresh_from_db()
        self.assertEqual(self.case.status, "DELIVERED")

    def test_job_links_and_delivery_evidence(self):
        self.release_due()
        target = reverse("operations:job_section", args=[self.case.pk, "commercial"])
        response = self.client.get(target)
        self.assertContains(response, url("invoices") + "?case=" + str(self.case.pk))
        self.assertContains(response, "balance remains due")
        capture(response, "job-integration")

    def test_payment_receipt_and_history_query_growth(self):
        self.pay(amount="1")
        def counts():
            result = {}
            for kind in ("quotations", "invoices", "payments", "outstanding", "receipts", "releases"):
                with CaptureQueriesContext(connection) as captured:
                    response = self.client.get(url(kind))
                self.assertEqual(response.status_code, 200)
                result[kind] = len(captured)
                self.assertLessEqual(len(captured), 8 if kind == "quotations" else 9, kind)
                capture(response, "growth-" + kind)
            return result
        before = counts()
        for _ in range(8): self.pay(amount="1")
        after = counts()
        self.assertEqual(before, after)
        print(f"Phase 6D payment history growth (1 -> 9): {before} -> {after}")


class CommercialPlanningTests(fixture.PaymentFixture, TestCase):
    @classmethod
    def setUpTestData(cls):
        usage.setup_usage(cls)
        cls.reader = get_user_model().objects.create_user(username="planning-reader")
        baseline.grant(cls.reader, cls.company, ["commercial.view_servicequotation", "commercial.view_serviceinvoice", "commercial.view_servicepayment"], cls.center)

    def setUp(self): self.client.force_login(self.actor)

    def post_operation(self, operation, data=None):
        target = reverse("operations:commercial_workflow", args=[self.case.pk, operation])
        response = self.client.get(target)
        self.assertEqual(response.status_code, 200)
        capture(response, operation + "-form")
        return self.client.post(target, {"revision": response.context["revision"], **(data or {})})

    def test_customer_rejection_and_replacement_preserve_decision(self):
        row = self.lines(self.create())
        self.assertEqual(self.post_operation("quote-submit").status_code, 302)
        self.assertEqual(self.post_operation("quote-decision", dict(outcome="REJECTED", channel="PHONE",
            recipient_name="Synthetic customer", reference="Customer declined", note="Price declined")).status_code, 302)
        response = self.client.get(url("quotations", row.pk))
        self.assertContains(response, "REJECTED")
        capture(response, "quote-rejected")
        self.assertEqual(self.post_operation("quote-revise").status_code, 302)
        response = self.client.get(url("quotations", row.pk))
        self.assertContains(response, "REJECTED")
        row.refresh_from_db()
        self.assertFalse(row.is_current)
        self.assertEqual(row.status, "REJECTED")
        self.assertContains(response, "revision 2")
        self.assertEqual(response.context["actions"], [])
        capture(response, "quote-superseded")

    def test_customer_approval_is_explicit_recorded_decision(self):
        row = self.lines(self.create())
        self.assertEqual(self.post_operation("quote-submit").status_code, 302)
        self.assertEqual(self.post_operation("quote-decision", dict(outcome="APPROVED", channel="IN_PERSON",
            recipient_name="Synthetic customer", reference="Recorded confirmation")).status_code, 302)
        response = self.client.get(url("quotations", row.pk))
        self.assertContains(response, "APPROVED")
        self.assertContains(response, "Recorded customer decision")
        capture(response, "quote-approved")

    def test_invoice_reconciliation_form_and_finalization(self):
        row = self.billed()
        response = self.client.get(url("invoices", row.pk))
        self.assertContains(response, "Technical consumption allocations")
        capture(response, "invoice-draft")
        target = reverse("operations:commercial_workflow", args=[self.case.pk, "invoice-reconcile"])
        response = self.client.get(target)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.context["groups"][0][1].initial), 1)
        capture(response, "invoice-reconcile")
        self.ready()
        self.assertEqual(self.post_operation("invoice-finalize").status_code, 302)
        row.refresh_from_db()
        self.assertEqual(row.status, "FINALIZED")
        self.assertEqual(row.grand_total, 100)

    def test_quotation_and_invoice_queues_grow_without_queries(self):
        def bill():
            row = self.billed()
            self.ready()
            self.invoice = self.finalize(row)
            self.pay(amount="1")
            self.release_due()
        def measure(expected):
            self.client.force_login(self.reader)
            result = {}
            for kind in KINDS:
                with CaptureQueriesContext(connection) as captured:
                    response = self.client.get(url(kind))
                self.assertEqual(len(response.context["cards"]), expected)
                self.assertLessEqual(len(captured), 8 if kind == "quotations" else 9, kind)
                result[kind] = len(captured)
                capture(response, "populated-" + kind)
            return result
        bill()
        before = measure(1)
        for _ in range(8):
            self.device = devices.register_device(product_model=self.model)
            self.case = intake(self)
            self.assignment = assignment.assign(self)
            assessment = diagnosis.begin(self)
            diagnosis.add(self, assessment)
            self.assessment = diagnosis.complete(self, assessment)
            bill()
        after = measure(9)
        self.assertEqual(before, after)
        print(f"Phase 6D quotation/invoice growth (1 -> 9): {before} -> {after}")
