"""
Performance benchmarks for the RBAC system.
"""

import sys
import time
from collections import deque

import pytest
from django.contrib.auth.models import Permission
from django.db import connection, reset_queries
from django.test import override_settings

from topdownrbac.models import (
    RestrictedObject,
    Role,
    RoleBinding,
    UserPermission,
    UserSubject,
)


def get_db_size():
    if connection.vendor == "postgresql":
        with connection.cursor() as cursor:
            cursor.execute(
                "SELECT pg_size_pretty(pg_database_size(current_database()));"
            )
            size = cursor.fetchone()[0]
        return size
    elif connection.vendor == "sqlite":
        with connection.cursor() as cursor:
            cursor.execute("PRAGMA page_count;")
            page_count = cursor.fetchone()[0]
            cursor.execute("PRAGMA page_size;")
            page_size = cursor.fetchone()[0]
        size_bytes = page_count * page_size
        if size_bytes < 1024:
            return f"{size_bytes} bytes"
        elif size_bytes < 1024 * 1024:
            return f"{size_bytes / 1024:.1f} kB"
        else:
            return f"{size_bytes / (1024 * 1024):.1f} MB"
    else:
        raise NotImplementedError(
            f"get_db_size() does not support the '{connection.vendor}' database backend"
        )


@pytest.mark.django_db(transaction=True)
class TestPerformanceBench:
    @pytest.fixture(autouse=True)
    def _setup(self, db):
        # Use treebeard's load_bulk for efficient bulk tree creation
        # Build a 5-level tree: 10 roots, each with 10 children, etc.
        # Total: 10 + 100 + 1000 + 10000 + 100000 = 111110 ... actually 10^1+10^2+...+10^5
        # But original test uses 10*10*10*10*10 = 100000 leaf nodes

        # Build bulk data structure for load_bulk
        # Structure: list of dicts with 'data' and optional 'children'
        def build_level(depth_remaining):
            if depth_remaining == 0:
                return None
            nodes = []
            for _ in range(10):
                node = {"data": {}}
                children = build_level(depth_remaining - 1)
                if children:
                    node["children"] = children
                nodes.append(node)
            return nodes

        bulk_data = build_level(5)
        RestrictedObject.load_bulk(bulk_data)

        # Retrieve specific nodes for testing
        # Get first root and its descendants at various depths
        all_roots = list(RestrictedObject.get_root_nodes())
        self.root_obj = all_roots[0]

        # Get a level-4 descendant of root_obj
        level2 = list(self.root_obj.get_children())
        level3 = list(level2[0].get_children())
        level4 = list(level3[0].get_children())
        self.descendant_of_root = level4[0]

        # Get a node that is NOT a descendant of root_obj (last leaf of last root)
        last_root = all_roots[-1]
        l2 = list(last_root.get_children())
        l3 = list(l2[-1].get_children())
        l4 = list(l3[-1].get_children())
        l5 = list(l4[-1].get_children())
        self.non_descendant_obj = l5[-1]

        self.role_manager = Role.objects.create(name="manager")
        self.role_manager.add_permission(
            Permission.objects.get(codename="view_restrictedobject")
        )
        self.role_manager.add_permission(
            Permission.objects.get(codename="change_restrictedobject")
        )
        self.role_manager.add_permission(
            Permission.objects.get(codename="delete_restrictedobject")
        )

        self.role_viewer = Role.objects.create(name="viewer")
        self.role_viewer.add_permission(
            Permission.objects.get(codename="view_restrictedobject")
        )

        for i in range(10):
            UserSubject.objects.create_user(username="user" + str(i), password="")

    @override_settings(DEBUG=True)
    def test_simple_config_benchmark(self):

        reset_queries()
        print("===================================================")
        print(f"Test name : {sys._getframe().f_code.co_name}s")
        print(
            "Stage : Create role binding for 1 top level manager, which propagates to ~10000 instances."
        )
        connection.queries_log = deque(maxlen=500000)
        start_time = time.time()

        RoleBinding.objects.create(
            subject=UserSubject.objects.get(username="user2"),
            role=self.role_manager,
            restricted_object=self.root_obj,
            propagate=True,
        )

        queries = connection.queries
        total_time = sum(float(q["time"]) for q in queries)
        print(f"Raw ORM Time: {total_time:.5f}s")
        print("Actual test execution time: %s s" % (time.time() - start_time))
        print(f"Queries executed: {len(queries)}")
        print(
            f"UserPermission table entry count: {UserPermission.objects.all().count()}"
        )

        reset_queries()

        print("===================================================")
        print(f"Current db size: {get_db_size()}")
        print("===================================================")
        print(f"Test name : {sys._getframe().f_code.co_name}s")
        print(
            "Stage : Check permission for user 0 on a restricted object in their hierarchy"
        )
        start_time = time.time()

        user = UserSubject.objects.get(username="user2")
        assert user.has_perm(
            "view_restrictedobject",
            obj=self.descendant_of_root,
        )
        assert not user.has_perm(
            "view_restrictedobject",
            obj=self.non_descendant_obj,
        )
        queries = connection.queries
        total_time = sum(float(q["time"]) for q in queries)
        print(f"Raw ORM Time: {total_time:.5f}s")
        print("Actual test execution time: %s s" % (time.time() - start_time))
        print(f"Queries executed: {len(queries)}")
        print(
            f"UserPermission table entry count: {UserPermission.objects.all().count()}"
        )

        reset_queries()
        print("===================================================")
        print(f"Current db size: {get_db_size()}")
        print("===================================================")
        print(f"Test name : {sys._getframe().f_code.co_name}s")
        print("Stage : Remove role manager from user2, restricted object id 1")
        start_time = time.time()

        rb = RoleBinding.objects.get(
            subject=user,
            role=self.role_manager,
            restricted_object=self.root_obj,
        )
        rb.delete()
        assert not user.has_perm(
            "view_restrictedobject",
            obj=self.descendant_of_root,
        )

        queries = connection.queries
        total_time = sum(float(q["time"]) for q in queries)
        print(f"Raw ORM Time: {total_time:.5f}s")
        print("Actual test execution time: %s s" % (time.time() - start_time))
        print(f"Queries executed: {len(queries)}")
        print(
            f"UserPermission table entry count: {UserPermission.objects.all().count()}"
        )

        reset_queries()
        print("===================================================")
        print(f"Current db size: {get_db_size()}")
        print("===================================================")
        print(f"Test name : {sys._getframe().f_code.co_name}s")
        print(
            "Stage : Add 3 more perms to the role to see length and db size of this operation"
        )
        start_time = time.time()
        self.role_manager.add_permission(
            Permission.objects.get(codename="add_restrictedobject")
        )
        self.role_manager.add_permission(
            Permission.objects.get(codename="add_usersubject")
        )
        self.role_manager.add_permission(
            Permission.objects.get(codename="change_usersubject")
        )

        RoleBinding.objects.create(
            subject=UserSubject.objects.get(username="user2"),
            role=self.role_manager,
            restricted_object=self.root_obj,
            propagate=True,
        )

        queries = connection.queries
        total_time = sum(float(q["time"]) for q in queries)
        print(f"Raw ORM Time: {total_time:.5f}s")
        print("Actual test execution time: %s s" % (time.time() - start_time))
        print(f"Queries executed: {len(queries)}")
        print(
            f"UserPermission table entry count: {UserPermission.objects.all().count()}"
        )

        reset_queries()
        print("===================================================")
        print(f"Current db size: {get_db_size()}")
        print("===================================================")
        print(f"Test name : {sys._getframe().f_code.co_name}s")
        print("Stage : Check access time for a queryset")
        start_time = time.time()
        assert (
            RestrictedObject.get_queryset_for_user(
                user, "view_restrictedobject"
            ).count()
            == 11111
        )
        print(f"Queryset length : {len(RestrictedObject.get_queryset_for_user(user, "view_restrictedobject"))}")
        print("Query count is correct Stat recap...")
        queries = connection.queries
        total_time = sum(float(q["time"]) for q in queries)
        print(f"Raw ORM Time: {total_time:.5f}s")
        print("Actual test execution time: %s s" % (time.time() - start_time))
        print(f"Queries executed: {len(queries)}")
        print(
            f"UserPermission table entry count: {UserPermission.objects.all().count()}"
        )

        print("===================================================")
        print(f"Current db size: {get_db_size()}")
