"""Phase 6A presentation contracts; frozen baseline tests are unchanged."""
import os
from datetime import timedelta
from pathlib import Path
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.db import connection
from django.test import TestCase, RequestFactory
from django.test.utils import CaptureQueriesContext
from django.urls import reverse
from django.utils import timezone

from apps.access.services import create_role_assignment, set_role_permissions
from apps.devices import services as devices
from apps.organization.assignment_services import create_assignment
from apps.organization.models import Department
from apps.service.tests import intake
from . import tests as baseline
from .context import shell


def capture_page(response, name):
    """Opt-in synthetic browser fixtures, outside the repository."""
    directory = os.environ.get("CCARE_UI_CAPTURE")
    if not directory:
        return
    root = Path(settings.BASE_DIR) / "apps/operations/static/operations"
    html = response.content.decode().replace(
        '<link rel="stylesheet" href="/static/operations/workspace.css">',
        '<style>' + (root / "workspace.css").read_text() + '</style>')
    html = html.replace('<script src="/static/operations/workspace.js" defer></script>',
        '<script>document.addEventListener("DOMContentLoaded", () => {' +
        (root / "workspace.js").read_text() + '});</script>')
    destination = Path(directory)
    destination.mkdir(parents=True, exist_ok=True)
    (destination / (name + ".html")).write_text(html, encoding="utf-8")


class OperationalUITests(TestCase):
    @classmethod
    def setUpTestData(cls):
        baseline.WorkspaceTests.setUpTestData.__func__(cls)

    def setUp(self):
        self.client.force_login(self.reader)

    def detail(self, case=None):
        return self.client.get(reverse("operations:detail", args=["cases", (case or self.case).pk]))

    def extend_role(self, names):
        additions = [Permission.objects.get(content_type__app_label=name.split(".")[0], codename=name.split(".")[1]) for name in names]
        set_role_permissions(role=self.role, permissions=[*self.role.permissions.all(), *additions])

    def test_today_intake_and_previews_are_scoped_and_date_bounded(self):
        old = intake(self, received_at=timezone.now()-timedelta(days=2))
        response = self.client.get("/")
        expected = int(timezone.localdate(self.case.received_at) == timezone.localdate())
        counts = {c["label"]: c["value"] for c in response.context["cards"]}
        self.assertEqual(counts["Today's intake"], expected)
        self.assertEqual(counts["Open service cases"], 2)
        queue = next(q for q in response.context["queues"] if q["label"] == "Today's intake")
        self.assertEqual(len(queue["cards"]), expected)
        for case in (old, self.second_case, self.foreign):
            self.assertNotContains(response, reverse("operations:detail", args=["cases", case.pk]))
        listing = self.client.get("/workspace/cases/?bucket=today")
        self.assertEqual(len(listing.context["cards"]), expected)
        capture_page(response, "dashboard")

    def test_queue_rows_are_bounded_and_have_received_and_engineer_context(self):
        for _ in range(7):
            intake(self)
        response = self.client.get("/")
        queue = next(q for q in response.context["queues"] if q["label"] == "Today's intake")
        self.assertEqual(len(queue["cards"]), 5)
        self.assertIn("Assigned engineer", dict(queue["cards"][0]["facts"]))
        self.assertIn("Accepted", dict(queue["cards"][0]["facts"]))

    def test_no_access_and_empty_work_are_distinct(self):
        self.client.force_login(self.user)
        response = self.client.get("/")
        self.assertEqual(response.context["queues"], [])
        self.assertContains(response, "No operational capabilities assigned")
        self.client.force_login(self.reader)
        self.assertContains(self.client.get("/"), "No authorized work in this queue")
        self.assertContains(self.client.get("/workspace/cases/?q=no-match"), "No authorized records match this view")

    def test_navigation_hides_ungranted_work_and_groups_destinations(self):
        response = self.client.get("/")
        self.assertNotContains(response, '?bucket=mine')
        self.assertNotContains(response, '?bucket=qc')
        self.assertNotContains(response, '/workspace/parts-requests/')
        self.assertNotContains(response, 'Django Admin')
        self.extend_role(["service.handle_servicecase", "service.perform_quality_control"])
        response = self.client.get("/")
        self.assertContains(response, '?bucket=mine')
        self.assertContains(response, '?bucket=qc')
        self.assertContains(response, "Engineer work")
        self.assertContains(response, 'role="search"')

    def test_queue_navigation_marks_only_the_selected_destination(self):
        self.extend_role(["service.handle_servicecase", "service.perform_quality_control"])
        for bucket, label in (("mine", "Engineer work"), ("qc", "Quality control"), ("open", "Service cases")):
            response = self.client.get("/workspace/cases/", {"bucket": bucket, "page": "1"})
            links = [link for group in response.context["workspace"]["groups"] for link in group["links"] if link.get("current")]
            self.assertEqual([link["label"] for link in links], [label])
            self.assertContains(response, 'aria-current="page"', count=1)
        for path in ("/", "/search/?q=missing"):
            self.assertContains(self.client.get(path), 'aria-current="page"', count=1)

    def test_unassignment_does_not_present_assignment_as_completed(self):
        from apps.service import engineer_services
        self.extend_role(["service.handle_servicecase"])
        with CaptureQueriesContext(connection) as before:
            self.detail()
        engineer_services.assign_service_case_engineer(service_case=self.case, engineer=self.reader, assigned_by=self.admin)
        response = self.client.get("/")
        counts = {card["label"]: card["value"] for card in response.context["cards"]}
        self.assertEqual(counts["Assigned, awaiting diagnosis"], 1)
        self.assertEqual(len(next(q for q in response.context["queues"] if q["label"] == "Engineer work")["cards"]), 1)
        engineer_services.unassign_service_case_engineer(service_case=self.case, unassigned_by=self.admin, reason="Review regression")
        for _ in range(3):
            engineer_services.assign_service_case_engineer(service_case=self.case, engineer=self.reader, assigned_by=self.admin)
            engineer_services.unassign_service_case_engineer(service_case=self.case, unassigned_by=self.admin, reason="History growth")
        with CaptureQueriesContext(connection) as after:
            job = self.detail().context["job"]
        self.assertEqual(len(before), len(after))
        print(f"Phase 6A.1 job overview queries: {len(after)} (constant with assignment history growth)")
        step = next(step for step in job["steps"] if step["label"] == "Assignment")
        self.assertEqual(step["state"], "pending")
        self.assertEqual(job["engineer"], "Unassigned")

    def test_search_query_count_does_not_grow_with_results(self):
        devices.replace_device_identifier(device=self.device, identifier_type="SERIAL", new_value="UI-SEARCH-REVIEW")
        with CaptureQueriesContext(connection) as before:
            self.client.get("/search/", {"q": "UI-SEARCH-REVIEW"})
        for _ in range(6):
            intake(self)
        with CaptureQueriesContext(connection) as after:
            response = self.client.get("/search/", {"q": "UI-SEARCH-REVIEW"})
        self.assertEqual(len(before), len(after))
        self.assertLessEqual(len(after), 60)
        self.assertEqual(len(next(group for group in response.context["groups"] if group["label"] == "Service cases")["cards"]), 7)
        print(f"Phase 6A.1 search queries: {len(after)}")

    def test_department_scope_does_not_widen_case_visibility(self):
        actor = get_user_model().objects.create_user(username="ui-department")
        department = Department.objects.create(company=self.company, code="UI", name="UI department")
        path = create_assignment(user=actor, company=self.company, department=department)
        create_role_assignment(user=actor, role=self.role, organization_assignment=path)
        self.client.force_login(actor)
        # Frozen center-target authorization excludes department-only grants.
        self.assertEqual(self.detail().status_code, 404)
        self.assertEqual(self.client.get("/").context["queues"], [])

    def test_center_department_scope_keeps_exact_center_visibility(self):
        actor = get_user_model().objects.create_user(username="ui-center-department")
        department = Department.objects.create(company=self.company, code="UIC", name="UI department")
        path = create_assignment(user=actor, company=self.company, region=self.region,
            service_center=self.center, department=department)
        create_role_assignment(user=actor, role=self.role, organization_assignment=path)
        self.client.force_login(actor)
        self.assertEqual(self.detail().status_code, 200)
        self.assertEqual(self.detail(self.second_case).status_code, 404)
        self.assertEqual(self.detail(self.foreign).status_code, 404)

    def test_job_journey_does_not_infer_unrecorded_completion_or_coverage(self):
        response = self.detail()
        self.assertEqual(response.status_code, 200)
        steps = {step["label"]: step for step in response.context["job"]["steps"]}
        self.assertEqual(len(steps), 10)
        self.assertEqual(steps["Intake"]["state"], "completed")
        for name in ("Diagnosis", "Repair", "Ready", "Handover", "Closed"):
            self.assertEqual(steps[name]["state"], "pending")
        self.assertEqual(steps["Quotation"]["state"], "unavailable")
        self.assertEqual(steps["Payment"]["state"], "unavailable")
        self.assertEqual(steps["QC"]["state"], "unavailable")
        self.assertContains(response, "payment or coverage is not inferred")
        self.assertContains(response, 'class="service-journey"')
        capture_page(response, "job")

    def test_identifiers_need_authorized_device_visibility(self):
        devices.replace_device_identifier(device=self.device, identifier_type="SERIAL", new_value="SENSITIVE-UI-SERIAL")
        self.assertContains(self.detail(), "SENSITIVE-UI-SERIAL")
        actor = get_user_model().objects.create_user(username="ui-case-only")
        baseline.grant(actor, self.company, ["service.view_servicecase"], self.center)
        self.client.force_login(actor)
        self.assertNotContains(self.detail(), "SENSITIVE-UI-SERIAL")
        self.assertEqual(self.detail(self.foreign).status_code, 404)
        self.assertEqual(self.detail(self.second_case).status_code, 404)

    def test_no_quotation_is_explicitly_unclassified(self):
        self.extend_role(["commercial.view_servicequotation", "commercial.view_servicepayment"])
        self.assertContains(self.detail(), "No quotation recorded; coverage is not inferred")

    def test_dashboard_query_count_is_constant_for_large_queues(self):
        with CaptureQueriesContext(connection) as before:
            self.client.get("/")
        for _ in range(9):
            intake(self)
        with CaptureQueriesContext(connection) as after:
            self.client.get("/")
        self.assertEqual(len(before), len(after))
        self.assertLessEqual(len(after), 9)
        print(f"Phase 6A scoped dashboard queries: {len(after)}")

    def test_navigation_bulk_resolution_and_request_cache(self):
        request = RequestFactory().get("/")
        request.user = self.reader
        from . import queries
        with patch("apps.operations.context.q.capability_map", wraps=queries.capability_map) as resolver:
            with CaptureQueriesContext(connection) as captured:
                first = shell(request)
                self.assertIs(first, shell(request))
            self.assertEqual(resolver.call_count, 1)
        self.assertLessEqual(len(captured), 4)
        print(f"Phase 6A navigation queries: {len(captured)}")

    def test_all_capabilities_dashboard_has_fixed_query_bound(self):
        self.client.force_login(self.admin)
        with CaptureQueriesContext(connection) as before:
            self.client.get("/")
        for _ in range(6):
            intake(self)
        with CaptureQueriesContext(connection) as after:
            response = self.client.get("/")
        self.assertEqual(len(before), len(after))
        self.assertLessEqual(len(after), 22)
        self.assertEqual({g["label"] for g in response.context["workspace"]["groups"]},
            {"Operations", "Parts & inventory", "Commercial", "Monitoring", "System"})
        print(f"Phase 6A all-capabilities dashboard queries: {len(after)}")

    def test_parts_queue_is_independently_scoped_and_readonly(self):
        response = self.client.get("/workspace/parts-requests/")
        self.assertEqual(response.status_code, 200)
        self.assertEqual(list(response.context["page"]), [])
        self.assertEqual(self.client.post("/workspace/parts-requests/").status_code, 405)

    def test_responsive_landmarks_and_form_protection_remain(self):
        response = self.detail()
        for markup in ('id="main-content"', 'aria-label="Application navigation"',
                       'aria-controls="workspace-navigation"', 'aria-label="Breadcrumb"',
                       'name="csrfmiddlewaretoken"', 'for="global-search"'):
            self.assertContains(response, markup)
        capture_page(self.client.get("/workspace/cases/"), "queue")


class JobEvidenceTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        from apps.service import test_handover as fixture
        fixture.setup_handover(cls)
        cls.reader = get_user_model().objects.create_user(username="ui-evidence")
        baseline.grant(cls.reader, cls.company, ["service.view_servicecase", "service.perform_quality_control"], cls.center)

    def steps(self):
        self.client.force_login(self.reader)
        response = self.client.get(reverse("operations:detail", args=["cases", self.case.pk]))
        return {step["label"]: step for step in response.context["job"]["steps"]}

    def test_passed_qc_does_not_infer_release_handover_or_closure(self):
        steps = self.steps()
        for name in ("Diagnosis", "Repair", "QC"):
            self.assertEqual(steps[name]["state"], "completed")
        for name in ("Ready", "Handover", "Closed"):
            self.assertEqual(steps[name]["state"], "pending")
        from apps.service import test_handover as fixture
        fixture.released(self)
        self.assertEqual(self.steps()["Ready"]["state"], "completed")

    def test_closed_job_uses_persisted_custody_evidence(self):
        from apps.service import test_handover as fixture
        fixture.delivered(self)
        fixture.close(self)
        steps = self.steps()
        for name in ("Ready", "Handover", "Closed", "Assignment"):
            self.assertEqual(steps[name]["state"], "completed")


class JobFinancialEvidenceTests(baseline.payment_fixture.PaymentFixture, TestCase):
    @classmethod
    def setUpTestData(cls):
        baseline.payment_fixture.shared_payment_data(cls)
        cls.reader = get_user_model().objects.create_user(username="ui-financial-evidence")
        baseline.grant(cls.reader, cls.company, ["service.view_servicecase", "commercial.view_servicepayment",
            "commercial.view_servicequotation"], cls.center)

    def test_payment_milestone_uses_authoritative_balance(self):
        self.client.force_login(self.reader)
        url = reverse("operations:detail", args=["cases", self.invoice.service_case_id])
        def payment():
            response = self.client.get(url)
            return next(step for step in response.context["job"]["steps"] if step["label"] == "Payment")
        self.assertEqual(payment()["state"], "blocked")
        self.pay()
        self.assertEqual(payment()["state"], "completed")

    def test_due_release_does_not_present_balance_as_paid(self):
        self.release_due()
        self.client.force_login(self.reader)
        response = self.client.get(reverse("operations:detail", args=["cases", self.invoice.service_case_id]))
        step = next(step for step in response.context["job"]["steps"] if step["label"] == "Payment")
        self.assertEqual(step["state"], "current")
        self.assertIn("balance remains due", step["evidence"])


class QualityControlUITests(TestCase):
    @classmethod
    def setUpTestData(cls):
        from apps.service import test_quality_control as fixture
        fixture.setup_qc(cls)
        permission = Permission.objects.get(content_type__app_label="service", codename="view_servicecase")
        set_role_permissions(role=cls.qc_role, permissions=[cls.qc_permission, permission])

    def test_eligible_qc_queue_and_current_to_rework_evidence(self):
        from apps.service import test_quality_control as fixture
        self.client.force_login(self.inspector)
        response = self.client.get("/")
        queue = next(q for q in response.context["queues"] if q["label"] == "Quality control")
        self.assertEqual(len(queue["cards"]), 1)
        url = reverse("operations:detail", args=["cases", self.case.pk])
        qc = fixture.prepared(self)
        response = self.client.get(url)
        step = next(step for step in response.context["job"]["steps"] if step["label"] == "QC")
        self.assertEqual(step["state"], "current")
        fixture.check(self, qc, result="FAIL", note="Synthetic boot failure")
        fixture.fail_qc(self, qc)
        response = self.client.get(url)
        step = next(step for step in response.context["job"]["steps"] if step["label"] == "QC")
        self.assertEqual(step["state"], "rework")
        self.assertEqual(step["evidence"], "FAILED")
        reader = get_user_model().objects.create_user(username="ui-no-qc-history")
        baseline.grant(reader, self.company, ["service.view_servicecase"], self.center)
        self.client.force_login(reader)
        response = self.client.get(url)
        step = next(step for step in response.context["job"]["steps"] if step["label"] == "QC")
        self.assertEqual(step["state"], "unavailable")
        self.assertNotContains(response, "FAILED")


class RepairOutcomeUITests(TestCase):
    @classmethod
    def setUpTestData(cls):
        from apps.service import test_repair as fixture
        fixture.setup_repair(cls)
        cls.reader = get_user_model().objects.create_user(username="ui-repair-review")
        baseline.grant(cls.reader, cls.company, ["service.view_servicecase"], cls.center)

    def test_unsuccessful_attempt_is_not_a_completed_repair_milestone(self):
        from apps.service import test_repair as fixture
        execution, _ = fixture.prepared(self)
        fixture.complete(self, execution, outcome="NOT_REPAIRED")
        self.client.force_login(self.reader)
        response = self.client.get(reverse("operations:detail", args=["cases", self.case.pk]))
        step = next(step for step in response.context["job"]["steps"] if step["label"] == "Repair")
        self.assertEqual(step["state"], "blocked")
        self.assertEqual(step["evidence"], "NOT_REPAIRED")
        self.case.refresh_from_db()
        self.assertEqual(self.case.status, "DIAGNOSED")
