"""PERF-001/PERF-007: no new unpaginated list endpoint may be added silently.

DRF has no DEFAULT_PAGINATION_CLASS here, so a viewset without an explicit
`pagination_class` returns its entire queryset. Adding the global default in one
change would alter the response shape of >100 endpoints at once and break every
frontend hook that expects a bare array, so pagination is being rolled out per
domain instead.

This test freezes that migration. UNPAGINATED_ALLOWLIST is the debt still to be
paid, not a list of approved exceptions: entries should only ever be removed.
A new list endpoint fails this test until it either paginates or is added here
deliberately, with a reason.
"""

import pytest
from django.urls import get_resolver


# Endpoints that still return unbounded lists. Shrink this list; do not grow it.
UNPAGINATED_ALLOWLIST = {
    "apps.accounting.views.AccountViewSet",
    "apps.accounting.views.FundViewSet",
    "apps.accounting.views.SalesAccountingSettingsViewSet",
    "apps.asset_management.views.AssetCategoryViewSet",
    "apps.asset_management.views.AssetLocationViewSet",
    "apps.asset_management.views.LicenseViewSet",
    "apps.asset_management.views.RentalViewSet",
    "apps.asset_management.views.SellableItemViewSet",
    "apps.asset_management.views.StockItemViewSet",
    "apps.asset_management.views.VendorViewSet",
    "apps.chat.views.MessageAttachmentViewSet",
    "apps.chat.views.UserKeyViewSet",
    "apps.companies.finance_views.DepartmentFinanceViewSet",
    "apps.companies.views.DepartmentViewSet",
    "apps.companies.views.DirectorViewSet",
    "apps.companies.views.OrgProfileViewSet",
    "apps.contacts.views.NoteViewSet",
    "apps.contacts.views.ReminderViewSet",
    "apps.core.rbac_views.AdminUserViewSet",
    "apps.core.rbac_views.InvitationViewSet",
    "apps.core.rbac_views.RoleViewSet",
    "apps.core.views.AttachmentViewSet",
    "apps.core.views.CommentViewSet",
    "apps.core.views.TagViewSet",
    "apps.core.views.UserViewSet",
    "apps.donors.views.DonorCommentViewSet",
    "apps.donors.views.DonorDeliverableViewSet",
    "apps.donors.views.DonorFinanceSummaryViewSet",
    "apps.donors.views.DonorFundAllocationViewSet",
    "apps.donors.views.DonorFundDocumentViewSet",
    "apps.donors.views.DonorFundReportViewSet",
    "apps.donors.views.DonorFundTrancheViewSet",
    "apps.donors.views.DonorFundViewSet",
    "apps.donors.views.DonorIndicatorViewSet",
    "apps.donors.views.DonorInviteViewSet",
    "apps.donors.views.DonorMeetingViewSet",
    "apps.donors.views.DonorMilestoneViewSet",
    "apps.donors.views.DonorProjectViewSet",
    "apps.donors.views.DonorRiskViewSet",
    "apps.donors.views.DonorSummaryViewSet",
    "apps.donors.views.DonorTeamViewSet",
    "apps.events.views.EventBlocklistViewSet",
    "apps.events.views.EventDocumentViewSet",
    "apps.events.views.EventInvitationViewSet",
    "apps.events.views.EventLogViewSet",
    "apps.events.views.EventMediaViewSet",
    "apps.events.views.EventRegistrationViewSet",
    "apps.events.views.EventScheduleItemViewSet",
    "apps.events.views.EventSpeakerViewSet",
    "apps.events.views.EventTeamViewSet",
    "apps.events.views.EventTemplateViewSet",
    "apps.events.views.InvitationTemplateViewSet",
    "apps.events.views.OfficeCalendarEntryViewSet",
    "apps.events.views.RegistrationFormFieldViewSet",
    "apps.events.views.RegistrationFormSubmissionViewSet",
    "apps.events.views.RegistrationFormViewSet",
    "apps.finance.views.BankAccountViewSet",
    "apps.finance.views.BankStatementImportViewSet",
    "apps.finance.views.BudgetCategoryViewSet",
    "apps.finance.views.BudgetItemViewSet",
    "apps.finance.views.CurrencyViewSet",
    "apps.finance.views.InvoiceViewSet",
    "apps.finance.views.TaxRateViewSet",
    "apps.hr.views.BusinessTripViewSet",
    "apps.hr.views.CommunicationViewSet",
    "apps.hr.views.ContractViewSet",
    "apps.hr.views.EmployeeDocumentViewSet",
    "apps.hr.views.FamilyViewSet",
    "apps.hr.views.JobApplicationViewSet",
    "apps.hr.views.LeaveRequestViewSet",
    "apps.hr.views.OnboardingViewSet",
    "apps.hr.views.PayrollViewSet",
    "apps.hr.views.PublicRecruitmentViewSet",
    "apps.hr.views.RecruitmentViewSet",
    "apps.hr.views.ShiftTemplateViewSet",
    "apps.hr.views.SurveyViewSet",
    "apps.meetings.views.MeetingInvitationViewSet",
    "apps.meetings.views.MeetingPresentationViewSet",
    "apps.meetings.views.MeetingRoomViewSet",
    "apps.notifications.views.FCMDeviceViewSet",
    "apps.notifications.views.NotificationPreferenceViewSet",
    "apps.notifications.views.NotificationTemplateViewSet",
    "apps.procurements.views.ProcurementReportViewSet",
    "apps.projects.views.CashAdvanceViewSet",
    "apps.projects.views.DeliverableViewSet",
    "apps.projects.views.FundDocumentViewSet",
    "apps.projects.views.FundReportViewSet",
    "apps.projects.views.FundingOpportunityViewSet",
    "apps.projects.views.FundingProposalViewSet",
    "apps.projects.views.IndicatorRecordViewSet",
    "apps.projects.views.PaymentRequestFormViewSet",
    "apps.projects.views.ProjectDocumentViewSet",
    "apps.projects.views.ProjectFundAllocationRevisionViewSet",
    "apps.projects.views.ProjectFundAllocationViewSet",
    "apps.projects.views.ProjectFundComplianceViewSet",
    "apps.projects.views.ProjectFundTrancheViewSet",
    "apps.projects.views.ProjectFundViewSet",
    "apps.projects.views.ProjectIndicatorViewSet",
    "apps.projects.views.ProjectRiskViewSet",
    "apps.projects.views.ProjectTeamMemberViewSet",
    "apps.publications.views.AuthorViewSet",
    "apps.publications.views.ExternalPublicationViewSet",
    "apps.publications.views.PublicationCategoryViewSet",
    "apps.tools.views.FileConversionViewSet",
    "apps.tools.views.ImageToolUsageViewSet",
    "apps.tools.views.QRCodeViewSet",
    "apps.tools.views.ScribdScrapeViewSet",
    "apps.tools.views.URLViewSet",
}


def _list_viewsets():
    """{dotted path: class} for every viewset the router exposes a list route for."""
    found = {}

    def walk(patterns):
        for pattern in patterns:
            if hasattr(pattern, "url_patterns"):
                walk(pattern.url_patterns)
                continue
            callback = getattr(pattern, "callback", None)
            view_class = getattr(callback, "cls", None)
            actions = getattr(callback, "actions", {}) or {}
            if view_class and "list" in actions.values():
                found[f"{view_class.__module__}.{view_class.__name__}"] = view_class

    walk(get_resolver().url_patterns)
    return found


def test_no_new_unpaginated_list_endpoints():
    unpaginated = {
        path for path, cls in _list_viewsets().items()
        if not getattr(cls, "pagination_class", None)
    }
    new = unpaginated - UNPAGINATED_ALLOWLIST
    assert not new, (
        "New unpaginated list endpoint(s): "
        + ", ".join(sorted(new))
        + ". Set pagination_class (with a deterministic, unique-suffixed "
          "order_by) or add to UNPAGINATED_ALLOWLIST with a reason."
    )


def test_allowlist_has_no_stale_entries():
    """A paginated endpoint left in the allowlist hides the real remaining debt."""
    unpaginated = {
        path for path, cls in _list_viewsets().items()
        if not getattr(cls, "pagination_class", None)
    }
    stale = UNPAGINATED_ALLOWLIST - unpaginated
    assert not stale, (
        "Now paginated (or removed) — drop from UNPAGINATED_ALLOWLIST: "
        + ", ".join(sorted(stale))
    )


@pytest.mark.parametrize(
    "path",
    [
        "apps.events.views.EventViewSet",
        "apps.hr.views.EmployeeViewSet",
        "apps.hr.views.AttendanceViewSet",
        "apps.tasks.views.TaskViewSet",
        "apps.tasks.views.MessageViewSet",
        "apps.tasks.views.ApprovalViewSet",
    ],
)
def test_migrated_endpoints_stay_paginated(path):
    """The first rollout wave must not regress."""
    view_class = _list_viewsets().get(path)
    assert view_class is not None, f"{path} is no longer a routed list endpoint"
    assert getattr(view_class, "pagination_class", None), f"{path} lost its pagination"


def test_max_page_size_is_capped():
    """A client must not be able to opt back into an unbounded response."""
    from apps.core.pagination import StandardResultsSetPagination

    assert StandardResultsSetPagination.max_page_size <= 200
