"""Phase 4D UI contracts; existing domain fixtures/services remain authoritative."""
from datetime import timedelta
from pathlib import Path
from uuid import uuid4
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.db import connection
from django.test import TestCase, Client
from django.test.utils import CaptureQueriesContext
from django.urls import reverse
from django.utils import timezone
from apps.access.models import Role
from apps.access.services import set_role_permissions, create_role_assignment
from apps.organization.assignment_services import create_assignment
from apps.frontdesk.tests import prepare
from apps.frontdesk import services as desk
from apps.service.tests import intake
from apps.customers.services import update_customer
from apps.devices import services as devices
from apps.service.services import cancel_service_case
from apps.sla import services as sla
from apps.communications import services as communication
from apps.commercial import test_payment as payment_fixture
from . import queries


def grant(user, company, permissions, center=None):
    role = Role.objects.create(code="UI-"+str(uuid4())[:8],name="Synthetic arbitrary capability bundle")
    selected = []
    for permission in permissions:
        app,code = permission.split(".")
        selected.append(Permission.objects.get(content_type__app_label=app,codename=code))
    set_role_permissions(role=role,permissions=selected)
    path = create_assignment(user=user,company=company,region=center.region if center else None,service_center=center)
    create_role_assignment(user=user,role=role,organization_assignment=path)
    return role


class WorkspaceTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        prepare(cls)
        cls.case = intake(cls,received_at=timezone.now()-timedelta(hours=2))
        cls.foreign = intake(cls,company=cls.other_company,service_center=cls.other_center,customer=cls.outsider,device=cls.device2)
        cls.second_case = intake(cls,service_center=cls.center2)
        cls.reader = get_user_model().objects.create_user(username="ui-reader")
        cls.role = grant(cls.reader,cls.company,["service.view_servicecase","devices.view_device"],cls.center)

    def setUp(self):
        self.client.force_login(self.reader)

    def test_authentication_required(self):
        self.client.logout()
        for url in ("/","/search/","/workspace/cases/",reverse("operations:detail",args=["cases",self.case.pk])):
            self.assertEqual(self.client.get(url).status_code,302)

    def test_shell_navigation_and_no_hardcoded_role_names(self):
        response = self.client.get("/")
        self.assertContains(response,'id="workspace-navigation"')
        self.assertContains(response,'id="main-content"')
        self.assertContains(response,"ui-reader")
        self.assertContains(response,"Service cases")
        self.assertNotContains(response,"Communication history")
        self.assertNotContains(response,">Quotations</a>")
        self.assertNotContains(response,"Django Admin")

    def test_staff_and_native_permissions_do_not_grant_business_scope(self):
        self.user.is_staff=True
        self.user.save()
        self.user.user_permissions.set(Permission.objects.filter(content_type__app_label="commercial"))
        self.client.force_login(self.user)
        response=self.client.get("/")
        self.assertContains(response,"No operational capabilities assigned")
        self.assertNotContains(response,">Invoices</a>")
        self.assertFalse(queries.rows(self.user,"invoices").exists())

    def test_dashboard_counts_are_scoped_and_current(self):
        response=self.client.get("/")
        cards={c["label"]:c["value"] for c in response.context["cards"]}
        self.assertEqual(cards["Open service cases"],1)
        self.assertEqual(cards["Ready for delivery"],0)
        cancel_service_case(service_case=self.case,reason="Synthetic",cancelled_by=self.admin)
        response=self.client.get("/")
        self.assertEqual({c["label"]:c["value"] for c in response.context["cards"]}["Open service cases"],0)

    def test_frontdesk_permission_changes_cards_not_role_name(self):
        operator=get_user_model().objects.create_user(username="arbitrary-name")
        grant(operator,self.company,["frontdesk.view_appointment","frontdesk.view_queue"],self.center)
        desk.book_appointment(actor=self.admin,slot=self.slot,customer=self.customer)
        desk.add_walk_in(actor=self.admin,service_center=self.center,customer=self.customer)
        self.client.force_login(operator)
        cards={c["label"]:c for c in self.client.get("/").context["cards"]}
        self.assertEqual(cards["Appointments today (all statuses)"]["value"],1)
        self.assertEqual(cards["Waiting queue today"]["value"],1)
        self.assertEqual(cards["Waiting queue today"]["period"],str(self.today))
        self.assertNotIn("Open service cases",cards)

    def test_sla_cards_reuse_derived_state(self):
        sla.save_policy(actor=self.admin,company=self.company,code="test",name="Test",effective_from=timezone.localdate(self.case.received_at),target_minutes=60,warning_minutes=10)
        sla.enroll(actor=self.admin,service_case=self.case)
        set_role_permissions(role=self.role,permissions=Permission.objects.filter(content_type__app_label="sla",codename="view_sla"))
        cards={c["label"]:c["value"] for c in self.client.get("/").context["cards"]}
        self.assertEqual(cards,{"SLA due soon":0,"SLA overdue":1})

    def test_communication_cards_use_existing_status(self):
        customer=update_customer(customer=self.customer,primary_mobile="+8801700000091")
        template=communication.save_template(actor=self.admin,company=self.company,code="ui",name="Synthetic",channel="SMS",subject="",body="Synthetic")
        communication.request_notification(actor=self.admin,template=template,customer=customer,service_center=self.center,event_key="ui-test")
        set_role_permissions(role=self.role,permissions=Permission.objects.filter(content_type__app_label="communications",codename="view_notification"))
        cards={c["label"]:c["value"] for c in self.client.get("/").context["cards"]}
        self.assertEqual(cards,{"Communications pending":1,"Communications failed":0})

    def test_search_job_reference_disambiguates_scope(self):
        # All three centers have JOB-00000001; only the permitted one is returned.
        response=self.client.get("/search/",{"q":self.case.job_number})
        results=[r for g in response.context["groups"] for r in g["cards"]]
        self.assertEqual([r["url"] for r in results],[reverse("operations:detail",args=["cases",self.case.pk])])
        self.assertNotContains(response,str(self.foreign.pk))
        self.assertNotContains(response,str(self.second_case.pk))

    def test_search_customer_contact_requires_company_scope_and_masks_contact(self):
        mobile="+8801700000091"
        update_customer(customer=self.customer,primary_mobile=mobile)
        self.assertEqual(self.client.get("/search/",{"q":mobile}).context["groups"],[])
        viewer=get_user_model().objects.create_user(username="customer-reader")
        grant(viewer,self.company,["customers.view_customer"])
        self.client.force_login(viewer)
        response=self.client.get("/search/",{"q":"+880 1700 000091"})
        self.assertEqual(len(response.context["groups"][0]["cards"]),1)
        facts=response.context["groups"][0]["cards"][0]["facts"]
        self.assertNotIn(mobile,str(facts))
        self.assertNotContains(response,str(self.outsider.pk))

    def test_customer_names_are_scoped_and_escaped(self):
        update_customer(customer=self.customer,full_name="<script>workspace</script>")
        grant(self.user,self.company,["customers.view_customer"])
        self.client.force_login(self.user)
        response=self.client.get("/search/",{"q":"workspace"})
        self.assertContains(response,"&lt;script&gt;workspace&lt;/script&gt;")
        self.assertNotContains(response,"<script>workspace</script>")

    def test_device_identifier_exact_scoped_lookup(self):
        devices.replace_device_identifier(device=self.device,identifier_type="SERIAL",new_value="UI-EXACT-001")
        response=self.client.get("/search/",{"q":"UI-EXACT-001"})
        self.assertEqual({g["label"] for g in response.context["groups"]},{"Service cases","Devices"})
        self.assertEqual(self.client.get("/search/",{"q":"UI-EXACT"}).context["groups"],[])

    def test_foreign_identifier_does_not_leak(self):
        devices.replace_device_identifier(device=self.device2,identifier_type="SERIAL",new_value="UI-FOREIGN-001")
        self.assertEqual(self.client.get("/search/",{"q":"UI-FOREIGN-001"}).context["groups"],[])

    def test_idor_and_unsupported_kind(self):
        for kind,pk in [("cases",self.foreign.pk),("cases",self.second_case.pk),("customers",self.outsider.pk),("devices",self.device2.pk),("invalid",self.case.pk)]:
            self.assertEqual(self.client.get(reverse("operations:detail",args=[kind,pk])).status_code,404)

    def test_readonly_views_reject_posts(self):
        for url in ("/","/search/","/workspace/cases/",reverse("operations:detail",args=["cases",self.case.pk])):
            self.assertEqual(self.client.post(url,{}).status_code,405)

    def test_search_length_empty_and_result_cap(self):
        self.assertEqual(self.client.get("/search/",{"q":"x"*129}).status_code,400)
        self.assertEqual(self.client.get("/search/").context["groups"],[])
        grant(self.user,self.company,["customers.view_customer"])
        from apps.customers.tests import create
        for _ in range(12):
            create(self.company)
        self.client.force_login(self.user)
        result=self.client.get("/search/",{"q":"Synthetic"}).context["groups"][0]
        self.assertEqual(len(result["cards"]),10)
        self.assertTrue(result["more"])

    def test_cross_module_links_do_not_grant_customer_access(self):
        response=self.client.get(reverse("operations:detail",args=["cases",self.case.pk]))
        self.assertContains(response,reverse("operations:detail",args=["devices",self.device.pk]))
        self.assertNotContains(response,reverse("operations:detail",args=["customers",self.customer.pk]))
        self.assertNotContains(response,"Existing Admin detail")

    def test_global_login_defaults_to_workspace_and_honors_safe_next(self):
        self.reader.set_password("synthetic-workspace-password")
        self.reader.save()
        self.client.logout()
        response=self.client.post("/login/",{"username":self.reader.username,"password":"synthetic-workspace-password"})
        self.assertRedirects(response,"/")
        self.client.logout()
        response=self.client.post("/login/?next=https://evil.example/",{"username":self.reader.username,"password":"synthetic-workspace-password"})
        self.assertEqual(response.url,"/")

    def test_logout_is_post_and_csrf_protected(self):
        self.assertEqual(self.client.get("/logout/").status_code,405)
        client=Client(enforce_csrf_checks=True)
        client.force_login(self.reader)
        self.assertEqual(client.post("/logout/").status_code,403)
        self.assertEqual(self.client.post("/logout/").status_code,302)

    def test_existing_module_layouts_share_shell(self):
        self.client.force_login(self.admin)
        for url in ("/front-desk/",reverse("frontdesk:desk",args=[self.center.pk]),"/communications/","/communications/templates/","/sla/","/sla/policies/","/reports/"):
            with self.subTest(url=url):
                response=self.client.get(url)
                self.assertEqual(response.status_code,200)
                self.assertTemplateUsed(response,"operations/base.html")
                self.assertContains(response,"operations/workspace.css")

    def test_mobile_structure_accessibility_and_static_strategy(self):
        response=self.client.get("/")
        for text in ('name="viewport"','aria-controls="workspace-navigation"','Skip to content','aria-label="Application navigation"'):
            self.assertContains(response,text)
        css=(Path(settings.BASE_DIR)/"apps/operations/static/operations/workspace.css").read_text()
        js=(Path(settings.BASE_DIR)/"apps/operations/static/operations/workspace.js").read_text()
        self.assertIn("@media(max-width:760px)",css)
        self.assertIn("td[data-label]::before",css)
        self.assertIn(":focus-visible",css)
        self.assertIn("cell.dataset.label",js)
        self.assertNotIn("innerHTML",js)

    def test_dashboard_query_count_is_bounded_as_records_grow(self):
        with CaptureQueriesContext(connection) as before:
            self.client.get("/")
        for _ in range(12):
            intake(self)
        with CaptureQueriesContext(connection) as after:
            response=self.client.get("/")
        self.assertEqual(len(before),len(after))
        self.assertLessEqual(len(after),65)
        self.assertEqual({c["label"]:c["value"] for c in response.context["cards"]}["Open service cases"],13)

    def test_search_query_count_is_bounded(self):
        self.client.force_login(self.admin)
        with CaptureQueriesContext(connection) as capture:
            self.client.get("/search/",{"q":self.case.job_number})
        self.assertLessEqual(len(capture),60)

    def test_reporting_navigation_preserves_filters_and_hides_export(self):
        grant(self.user,self.company,["reporting.view_operational_dashboard"],self.center)
        self.client.force_login(self.user)
        response=self.client.get("/reports/",{"service_center":str(self.center.pk)})
        self.assertEqual(response.status_code,200)
        self.assertContains(response,"service_center="+str(self.center.pk))
        self.assertNotContains(response,">Service analytics</a>")
        self.assertNotContains(response,"Export this scoped report as CSV")

    def test_scope_is_not_combined_across_permission_paths(self):
        scoped=get_user_model().objects.create_user(username="ui-mixed")
        grant(scoped,self.company,["service.view_servicecase"],self.center)
        grant(scoped,self.other_company,["customers.view_customer"])
        self.assertFalse(queries.customers(scoped).filter(pk=self.customer.pk).exists())
        self.assertFalse(queries.cases(scoped).filter(pk=self.foreign.pk).exists())

    def test_all_readonly_lists_and_record_presenters(self):
        self.client.force_login(self.admin)
        appointment=desk.book_appointment(actor=self.admin,slot=self.slot,customer=self.customer)
        for kind in queries.KINDS:
            self.assertEqual(self.client.get(reverse("operations:list",args=[kind])).status_code,200)
        for kind,pk in [("cases",self.case.pk),("customers",self.customer.pk),("devices",self.device.pk),("appointments",appointment.pk)]:
            self.assertEqual(self.client.get(reverse("operations:detail",args=[kind,pk])).status_code,200)


class CommercialWorkspaceTests(payment_fixture.PaymentFixture,TestCase):
    @classmethod
    def setUpTestData(cls):
        payment_fixture.shared_payment_data(cls)
        cls.ui_actor=get_user_model().objects.create_user(username="ui-commercial")
        grant(cls.ui_actor,cls.company,["commercial.view_servicequotation","commercial.view_serviceinvoice","commercial.view_servicepayment"],cls.center)

    def setUp(self):
        self.client.force_login(self.ui_actor)

    def test_balance_dashboard_uses_authoritative_settlement(self):
        response=self.client.get("/")
        cards={c["label"]:c["value"] for c in response.context["cards"]}
        self.assertEqual(cards["Finalized invoices with customer balance"],1)
        self.pay()
        cards={c["label"]:c["value"] for c in self.client.get("/").context["cards"]}
        self.assertEqual(cards["Finalized invoices with customer balance"],0)

    def test_invoice_quotation_and_receipt_search_and_details(self):
        payment=self.pay()
        for kind,term,pk in [("quotations",self.invoice.quotation.family.number,self.invoice.quotation_id),
                            ("invoices",self.invoice.number,self.invoice.pk),("payments",payment.receipt.number,payment.pk)]:
            response=self.client.get("/search/",{"q":term})
            self.assertContains(response,reverse("operations:detail",args=[kind,pk]))
            self.assertEqual(self.client.get(reverse("operations:detail",args=[kind,pk])).status_code,200)

    def test_commercial_search_and_detail_deny_other_scope(self):
        unrelated=get_user_model().objects.create_user(username="ui-unrelated")
        from apps.organization.test_lifecycle import make_tree
        company,_,center,_=make_tree("UI-OTHER")
        grant(unrelated,company,["commercial.view_serviceinvoice"],center)
        self.client.force_login(unrelated)
        self.assertEqual(self.client.get("/search/",{"q":self.invoice.number}).context["groups"],[])
        self.assertEqual(self.client.get(reverse("operations:detail",args=["invoices",self.invoice.pk])).status_code,404)


class InventoryWorkspaceTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        from apps.inventory.tests import make_inventory_fixture
        make_inventory_fixture(cls)
        cls.viewer=get_user_model().objects.create_user(username="ui-inventory")
        grant(cls.viewer,cls.company,["inventory.view_stock","parts.view_sparepart"],cls.center)

    def test_inventory_center_scope_and_part_presenter(self):
        self.client.force_login(self.viewer)
        response=self.client.get(reverse("operations:detail",args=["inventory",self.destination.pk]))
        self.assertContains(response,"Stock on hand")
        for row in (self.foreign,self.location):
            self.assertEqual(self.client.get(reverse("operations:detail",args=["inventory",row.pk])).status_code,404)
        response=self.client.get(reverse("operations:detail",args=["parts",self.part.pk]))
        self.assertContains(response,self.part.part_code)

    def test_inventory_ledger_display_uses_existing_sum(self):
        from apps.inventory.services import receive_stock
        receive_stock(actor=self.actor,destination=self.destination,spare_part=self.part,quantity=7,reference="Synthetic UI",idempotency_key=uuid4())
        self.client.force_login(self.viewer)
        response=self.client.get(reverse("operations:detail",args=["inventory",self.destination.pk]))
        self.assertEqual(response.context["stock"][0]["on_hand"],7)
