"""Performance regression tests: RBAC permission caching + admin users endpoint."""

import pytest
from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.db import connection
from django.test.utils import CaptureQueriesContext
from rest_framework.test import APIClient

from apps.companies.models import Organization, OrganizationMembership
from apps.core.models import Permission, Role, UserRole

User = get_user_model()


@pytest.fixture(autouse=True)
def clear_cache():
    # RBAC perms now live in the Django cache; isolate tests from each other.
    cache.clear()
    yield
    cache.clear()


@pytest.fixture
def organization(db):
    org = Organization.objects.create(name="Perf Test Org")
    # Refetch: uuid7() returns a hex string with no dashes on the in-memory
    # instance right after .create(), but Django normalizes to a dashed
    # uuid.UUID on any DB fetch — string cache keys built from the two forms
    # don't match. Refetching here keeps this fixture's `.id` consistent with
    # what every view (which always fetches fresh) will see.
    return Organization.objects.get(pk=org.pk)


@pytest.fixture
def admin(db, organization):
    user = User.objects.create_superuser(email="admin@test.local", password="x")
    auto_org = user.organizations.first()
    OrganizationMembership.objects.filter(organization=auto_org, user=user).update(organization=organization)
    auto_org.delete()
    return user


@pytest.fixture
def member(db, organization):
    user = User.objects.create_user(email="member@test.local", password="x")
    OrganizationMembership.objects.create(organization=organization, user=user)
    role = Role.objects.create(name="HR Staff", slug="hr-staff")
    # Permissions are seeded by a data migration — reuse if present.
    perm, _ = Permission.objects.get_or_create(
        domain="hr", action="view", defaults={"label": "View HR"})
    role.permissions.add(perm)
    UserRole.objects.create(user=user, role=role, organization=organization)
    return user


@pytest.mark.django_db
class TestRolePermissionCaching:
    def test_codenames_resolved_from_roles(self, member, organization):
        assert member._role_permission_codenames(organization) == {"hr.view"}

    def test_superuser_short_circuits(self, admin, organization):
        with CaptureQueriesContext(connection) as ctx:
            assert admin._role_permission_codenames(organization) == {"*"}
        assert len(ctx.captured_queries) == 0

    def test_repeated_access_hits_db_once(self, member, organization):
        member._role_permission_codenames(organization)  # warm the memoized cache
        with CaptureQueriesContext(connection) as ctx:
            member._role_permission_codenames(organization)
            member.has_menu_perm("hr.view", organization)
            member.has_menu_perm("projects.view", organization)
        assert len(ctx.captured_queries) == 0

    def test_has_menu_perm(self, member, organization):
        assert member.has_menu_perm("hr.view", organization) is True
        assert member.has_menu_perm("hr.create", organization) is False
        assert member.has_menu_perm("projects.view", organization) is False

    def test_domain_wildcard(self, db, organization):
        user = User.objects.create_user(email="wild@test.local", password="x")
        role = Role.objects.create(name="HR Admin", slug="hr-admin")
        perm, _ = Permission.objects.get_or_create(
            domain="hr", action="*", defaults={"label": "All HR"})
        role.permissions.add(perm)
        UserRole.objects.create(user=user, role=role, organization=organization)
        assert user.has_menu_perm("hr.anything", organization) is True

    def test_cross_request_cache(self, member, organization):
        member._role_permission_codenames(organization)  # warms the Django cache
        fresh = User.objects.get(pk=member.pk)  # new instance = new request
        with CaptureQueriesContext(connection) as ctx:
            assert fresh._role_permission_codenames(organization) == {"hr.view"}
        assert len(ctx.captured_queries) == 0

    def test_assign_roles_invalidates_cache(self, admin, member, organization):
        member._role_permission_codenames(organization)  # warm
        client = APIClient()
        client.force_authenticate(admin)
        resp = client.post(f"/api/rbac/users/{member.pk}/roles/",
                           {"role_ids": []}, format="json")
        assert resp.status_code == 200
        fresh = User.objects.get(pk=member.pk)
        assert fresh._role_permission_codenames(organization) == set()

    def test_set_role_permissions_invalidates_cache(self, admin, member, organization):
        member._role_permission_codenames(organization)  # warm
        role = Role.objects.get(slug="hr-staff")
        client = APIClient()
        client.force_authenticate(admin)
        resp = client.post(f"/api/rbac/roles/{role.pk}/permissions/",
                           {"permission_ids": []}, format="json")
        assert resp.status_code == 200
        fresh = User.objects.get(pk=member.pk)
        assert fresh._role_permission_codenames(organization) == set()


@pytest.mark.django_db
class TestAdminUsersEndpoint:
    def test_roles_shape_and_constant_queries(self, admin, member, organization):
        client = APIClient()
        client.force_authenticate(admin)

        resp = client.get("/api/rbac/users/")
        assert resp.status_code == 200
        body = resp.json()
        rows = body["results"] if isinstance(body, dict) and "results" in body else body
        by_email = {r["email"]: r for r in rows}
        roles = by_email["member@test.local"]["roles"]
        assert roles == [{"id": roles[0]["id"], "slug": "hr-staff", "name": "HR Staff"}]
        assert by_email["admin@test.local"]["roles"] == []

        # Query count must not grow with user count (prefetched roles).
        with CaptureQueriesContext(connection) as small:
            client.get("/api/rbac/users/")
        for i in range(5):
            u = User.objects.create_user(email=f"bulk{i}@test.local", password="x")
            OrganizationMembership.objects.create(organization=organization, user=u)
            UserRole.objects.create(user=u, role=Role.objects.get(slug="hr-staff"), organization=organization)
        with CaptureQueriesContext(connection) as big:
            client.get("/api/rbac/users/")
        assert len(big.captured_queries) == len(small.captured_queries)
