# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Unit tests for RBAC permission resolution.
Tests rule specificity, multiple roles, and permission combination logic.
"""

from unittest.mock import Mock

from modules.datamodels.datamodelUam import User, AccessLevel
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
from modules.security.rbac import RbacClass
from modules.connectors.connectorDbPostgre import DatabaseConnector

_TEST_ROLE_USER = "test-rid-user"
_TEST_ROLE_VIEWER = "test-rid-viewer"


def _patchRbacResolution(rbac, roleIds, rulesWithPriority):
    """Stub multi-tenant role loading and rule fetch so tests exercise merge logic only."""
    rbac._getRoleIdsForUser = Mock(return_value=roleIds)
    rbac._getRulesForRoleIds = Mock(return_value=rulesWithPriority)


class TestRbacPermissionResolution:
    """Test RBAC permission resolution logic."""

    def testSingleRoleGenericRule(self):
        """Test permission resolution with a single role and generic rule."""
        db = Mock(spec=DatabaseConnector)
        dbApp = Mock(spec=DatabaseConnector)
        rbac = RbacClass(db, dbApp=dbApp)

        user = User(
            id="user1",
            username="testuser",
            roleLabels=["user"],
            mandateId="mandate1",
        )

        rules = [
            (
                1,
                AccessRule(
                    roleId=_TEST_ROLE_USER,
                    context=AccessRuleContext.DATA,
                    item=None,
                    view=True,
                    read=AccessLevel.MY,
                    create=AccessLevel.MY,
                    update=AccessLevel.MY,
                    delete=AccessLevel.MY,
                ),
            )
        ]
        _patchRbacResolution(rbac, [_TEST_ROLE_USER], rules)

        permissions = rbac.getUserPermissions(
            user,
            AccessRuleContext.DATA,
            "SomeTable",
        )

        assert permissions.view is True
        assert permissions.read == AccessLevel.MY
        assert permissions.create == AccessLevel.MY
        assert permissions.update == AccessLevel.MY
        assert permissions.delete == AccessLevel.MY

    def testRuleSpecificityMostSpecificWins(self):
        """Test that most specific rule wins within a single role."""
        db = Mock(spec=DatabaseConnector)
        dbApp = Mock(spec=DatabaseConnector)
        rbac = RbacClass(db, dbApp=dbApp)

        user = User(
            id="user1",
            username="testuser",
            roleLabels=["user"],
            mandateId="mandate1",
        )

        rules = [
            (
                1,
                AccessRule(
                    roleId=_TEST_ROLE_USER,
                    context=AccessRuleContext.DATA,
                    item=None,
                    view=True,
                    read=AccessLevel.GROUP,
                    create=AccessLevel.GROUP,
                    update=AccessLevel.GROUP,
                    delete=AccessLevel.GROUP,
                ),
            ),
            (
                1,
                AccessRule(
                    roleId=_TEST_ROLE_USER,
                    context=AccessRuleContext.DATA,
                    item="data.uam.UserInDB",
                    view=True,
                    read=AccessLevel.MY,
                    create=AccessLevel.NONE,
                    update=AccessLevel.MY,
                    delete=AccessLevel.NONE,
                ),
            ),
        ]
        _patchRbacResolution(rbac, [_TEST_ROLE_USER], rules)

        permissions = rbac.getUserPermissions(
            user,
            AccessRuleContext.DATA,
            "data.uam.UserInDB",
        )

        assert permissions.read == AccessLevel.MY
        assert permissions.create == AccessLevel.NONE
        assert permissions.update == AccessLevel.MY
        assert permissions.delete == AccessLevel.NONE

    def testMultipleRolesUnionLogic(self):
        """Test that multiple roles use union (opening) logic for view."""
        db = Mock(spec=DatabaseConnector)
        dbApp = Mock(spec=DatabaseConnector)
        rbac = RbacClass(db, dbApp=dbApp)

        user = User(
            id="user1",
            username="testuser",
            roleLabels=["user", "viewer"],
            mandateId="mandate1",
        )

        rules = [
            (
                1,
                AccessRule(
                    roleId=_TEST_ROLE_USER,
                    context=AccessRuleContext.UI,
                    item="playground",
                    view=False,
                ),
            ),
            (
                1,
                AccessRule(
                    roleId=_TEST_ROLE_VIEWER,
                    context=AccessRuleContext.UI,
                    item="playground",
                    view=True,
                ),
            ),
        ]
        _patchRbacResolution(rbac, [_TEST_ROLE_USER, _TEST_ROLE_VIEWER], rules)

        permissions = rbac.getUserPermissions(
            user,
            AccessRuleContext.UI,
            "playground",
        )

        assert permissions.view is True

    def testViewFalseOverridesGeneric(self):
        """Test that specific view=false overrides generic view=true."""
        db = Mock(spec=DatabaseConnector)
        dbApp = Mock(spec=DatabaseConnector)
        rbac = RbacClass(db, dbApp=dbApp)

        user = User(
            id="user1",
            username="testuser",
            roleLabels=["user"],
            mandateId="mandate1",
        )

        rules = [
            (
                1,
                AccessRule(
                    roleId=_TEST_ROLE_USER,
                    context=AccessRuleContext.UI,
                    item=None,
                    view=True,
                ),
            ),
            (
                1,
                AccessRule(
                    roleId=_TEST_ROLE_USER,
                    context=AccessRuleContext.UI,
                    item="playground.voice.settings",
                    view=False,
                ),
            ),
        ]
        _patchRbacResolution(rbac, [_TEST_ROLE_USER], rules)

        permissions = rbac.getUserPermissions(
            user,
            AccessRuleContext.UI,
            "playground.voice.settings",
        )

        assert permissions.view is False

    def testNoRolesReturnsNoAccess(self):
        """Test that user with no resolved role IDs gets no access."""
        db = Mock(spec=DatabaseConnector)
        dbApp = Mock(spec=DatabaseConnector)
        rbac = RbacClass(db, dbApp=dbApp)
        dbApp.getRecordset = Mock(return_value=[])

        user = User(
            id="user1",
            username="testuser",
            roleLabels=[],
            mandateId="mandate1",
        )

        permissions = rbac.getUserPermissions(
            user,
            AccessRuleContext.DATA,
            "SomeTable",
        )

        assert permissions.view is False
        assert permissions.read == AccessLevel.NONE
        assert permissions.create == AccessLevel.NONE
        assert permissions.update == AccessLevel.NONE
        assert permissions.delete == AccessLevel.NONE

    def testFindMostSpecificRule(self):
        """Test findMostSpecificRule method."""
        db = Mock(spec=DatabaseConnector)
        dbApp = Mock(spec=DatabaseConnector)
        rbac = RbacClass(db, dbApp=dbApp)

        rules = [
            AccessRule(
                roleId=_TEST_ROLE_USER,
                context=AccessRuleContext.DATA,
                item=None,
                view=True,
                read=AccessLevel.GROUP,
            ),
            AccessRule(
                roleId=_TEST_ROLE_USER,
                context=AccessRuleContext.DATA,
                item="data.uam.UserInDB",
                view=True,
                read=AccessLevel.MY,
            ),
            AccessRule(
                roleId=_TEST_ROLE_USER,
                context=AccessRuleContext.DATA,
                item="data.uam.UserInDB.email",
                view=True,
                read=AccessLevel.NONE,
            ),
        ]

        rule = rbac.findMostSpecificRule(rules, "data.uam.UserInDB.email")
        assert rule is not None
        assert rule.item == "data.uam.UserInDB.email"
        assert rule.read == AccessLevel.NONE

        rule = rbac.findMostSpecificRule(rules, "data.uam.UserInDB")
        assert rule is not None
        assert rule.item == "data.uam.UserInDB"
        assert rule.read == AccessLevel.MY

        rule = rbac.findMostSpecificRule(rules, "OtherTable")
        assert rule is not None
        assert rule.item is None
        assert rule.read == AccessLevel.GROUP

    def testValidateAccessRuleOpeningRights(self):
        """Test that CUD permissions respect read permission level."""
        db = Mock(spec=DatabaseConnector)
        dbApp = Mock(spec=DatabaseConnector)
        rbac = RbacClass(db, dbApp=dbApp)

        rule1 = AccessRule(
            roleId=_TEST_ROLE_USER,
            context=AccessRuleContext.DATA,
            item="data.uam.UserInDB",
            view=True,
            read=AccessLevel.MY,
            create=AccessLevel.MY,
            update=AccessLevel.MY,
            delete=AccessLevel.MY,
        )
        assert rbac.validateAccessRule(rule1) is True

        rule2 = AccessRule(
            roleId=_TEST_ROLE_USER,
            context=AccessRuleContext.DATA,
            item="data.uam.UserInDB",
            view=True,
            read=AccessLevel.MY,
            create=AccessLevel.GROUP,
            update=AccessLevel.MY,
            delete=AccessLevel.MY,
        )
        assert rbac.validateAccessRule(rule2) is False

        rule3 = AccessRule(
            roleId="test-rid-admin",
            context=AccessRuleContext.DATA,
            item="data.uam.UserInDB",
            view=True,
            read=AccessLevel.GROUP,
            create=AccessLevel.GROUP,
            update=AccessLevel.GROUP,
            delete=AccessLevel.GROUP,
        )
        assert rbac.validateAccessRule(rule3) is True

        rule4 = AccessRule(
            roleId=_TEST_ROLE_USER,
            context=AccessRuleContext.DATA,
            item="data.uam.UserInDB",
            view=True,
            read=AccessLevel.NONE,
            create=AccessLevel.MY,
            update=AccessLevel.MY,
            delete=AccessLevel.MY,
        )
        assert rbac.validateAccessRule(rule4) is False

    def testUiContextOnlyViewMatters(self):
        """Test that UI context only checks view permission."""
        db = Mock(spec=DatabaseConnector)
        dbApp = Mock(spec=DatabaseConnector)
        rbac = RbacClass(db, dbApp=dbApp)

        user = User(
            id="user1",
            username="testuser",
            roleLabels=["user"],
            mandateId="mandate1",
        )

        rules = [
            (
                1,
                AccessRule(
                    roleId=_TEST_ROLE_USER,
                    context=AccessRuleContext.UI,
                    item="playground",
                    view=True,
                ),
            )
        ]
        _patchRbacResolution(rbac, [_TEST_ROLE_USER], rules)

        permissions = rbac.getUserPermissions(
            user,
            AccessRuleContext.UI,
            "playground",
        )

        assert permissions.view is True

    def testResourceContextOnlyViewMatters(self):
        """Test that RESOURCE context only checks view permission."""
        db = Mock(spec=DatabaseConnector)
        dbApp = Mock(spec=DatabaseConnector)
        rbac = RbacClass(db, dbApp=dbApp)

        user = User(
            id="user1",
            username="testuser",
            roleLabels=["user"],
            mandateId="mandate1",
        )

        rules = [
            (
                1,
                AccessRule(
                    roleId=_TEST_ROLE_USER,
                    context=AccessRuleContext.RESOURCE,
                    item="ai.model.anthropic",
                    view=True,
                ),
            )
        ]
        _patchRbacResolution(rbac, [_TEST_ROLE_USER], rules)

        permissions = rbac.getUserPermissions(
            user,
            AccessRuleContext.RESOURCE,
            "ai.model.anthropic",
        )

        assert permissions.view is True
