"""
Unit tests for permission-based authorization (authorization.py).

Tests the FastAPI dependencies that validate user permissions within organizations.
"""

from unittest.mock import AsyncMock, MagicMock, patch
from uuid import uuid4

import pytest
from fastapi import HTTPException
from server.auth.authorization import (
    ROLE_PERMISSIONS,
    Permission,
    RoleName,
    get_api_key_org_id_from_request,
    get_role_permissions,
    get_user_org_role,
    has_permission,
    require_permission,
)

# =============================================================================
# Tests for Permission enum
# =============================================================================


class TestPermission:
    """Tests for Permission enum."""

    def test_permission_values(self):
        """
        GIVEN: Permission enum
        WHEN: Accessing permission values
        THEN: All expected permissions exist with correct string values
        """
        assert Permission.MANAGE_SECRETS.value == 'manage_secrets'
        assert Permission.MANAGE_MCP.value == 'manage_mcp'
        assert Permission.MANAGE_INTEGRATIONS.value == 'manage_integrations'
        assert (
            Permission.MANAGE_APPLICATION_SETTINGS.value
            == 'manage_application_settings'
        )
        assert Permission.MANAGE_API_KEYS.value == 'manage_api_keys'
        assert Permission.VIEW_LLM_SETTINGS.value == 'view_llm_settings'
        assert Permission.EDIT_LLM_SETTINGS.value == 'edit_llm_settings'
        assert Permission.VIEW_BILLING.value == 'view_billing'
        assert Permission.ADD_CREDITS.value == 'add_credits'
        assert (
            Permission.INVITE_USER_TO_ORGANIZATION.value
            == 'invite_user_to_organization'
        )
        assert Permission.CHANGE_USER_ROLE_MEMBER.value == 'change_user_role:member'
        assert Permission.CHANGE_USER_ROLE_ADMIN.value == 'change_user_role:admin'
        assert Permission.CHANGE_USER_ROLE_OWNER.value == 'change_user_role:owner'
        assert Permission.VIEW_ORG_SETTINGS.value == 'view_org_settings'
        assert Permission.CHANGE_ORGANIZATION_NAME.value == 'change_organization_name'
        assert Permission.DELETE_ORGANIZATION.value == 'delete_organization'
        assert Permission.MANAGE_AUTOMATIONS.value == 'manage_automations'

    def test_permission_from_string(self):
        """
        GIVEN: Valid permission string
        WHEN: Creating Permission from string
        THEN: Correct enum value is returned
        """
        assert Permission('manage_secrets') == Permission.MANAGE_SECRETS
        assert Permission('view_llm_settings') == Permission.VIEW_LLM_SETTINGS
        assert Permission('delete_organization') == Permission.DELETE_ORGANIZATION

    def test_permission_invalid_string(self):
        """
        GIVEN: Invalid permission string
        WHEN: Creating Permission from string
        THEN: ValueError is raised
        """
        with pytest.raises(ValueError):
            Permission('invalid_permission')


# =============================================================================
# Tests for RoleName enum
# =============================================================================


class TestRoleName:
    """Tests for RoleName enum."""

    def test_role_name_values(self):
        """
        GIVEN: RoleName enum
        WHEN: Accessing role name values
        THEN: All expected roles exist with correct string values
        """
        assert RoleName.OWNER.value == 'owner'
        assert RoleName.ADMIN.value == 'admin'
        assert RoleName.MEMBER.value == 'member'

    def test_role_name_from_string(self):
        """
        GIVEN: Valid role name string
        WHEN: Creating RoleName from string
        THEN: Correct enum value is returned
        """
        assert RoleName('owner') == RoleName.OWNER
        assert RoleName('admin') == RoleName.ADMIN
        assert RoleName('member') == RoleName.MEMBER

    def test_role_name_invalid_string(self):
        """
        GIVEN: Invalid role name string
        WHEN: Creating RoleName from string
        THEN: ValueError is raised
        """
        with pytest.raises(ValueError):
            RoleName('invalid_role')


# =============================================================================
# Tests for ROLE_PERMISSIONS mapping
# =============================================================================


class TestRolePermissions:
    """Tests for role permission mappings."""

    def test_owner_has_all_permissions(self):
        """
        GIVEN: ROLE_PERMISSIONS mapping
        WHEN: Checking owner permissions
        THEN: Owner has all permissions including owner-only permissions
        """
        owner_perms = ROLE_PERMISSIONS[RoleName.OWNER]
        assert Permission.MANAGE_SECRETS in owner_perms
        assert Permission.MANAGE_MCP in owner_perms
        assert Permission.VIEW_LLM_SETTINGS in owner_perms
        assert Permission.EDIT_LLM_SETTINGS in owner_perms
        assert Permission.VIEW_BILLING in owner_perms
        assert Permission.ADD_CREDITS in owner_perms
        assert Permission.INVITE_USER_TO_ORGANIZATION in owner_perms
        assert Permission.CHANGE_USER_ROLE_MEMBER in owner_perms
        assert Permission.CHANGE_USER_ROLE_ADMIN in owner_perms
        assert Permission.CHANGE_USER_ROLE_OWNER in owner_perms
        assert Permission.CHANGE_ORGANIZATION_NAME in owner_perms
        assert Permission.DELETE_ORGANIZATION in owner_perms
        assert Permission.MANAGE_AUTOMATIONS in owner_perms

    def test_admin_has_admin_permissions(self):
        """
        GIVEN: ROLE_PERMISSIONS mapping
        WHEN: Checking admin permissions
        THEN: Admin has admin permissions but not owner-only permissions
        """
        admin_perms = ROLE_PERMISSIONS[RoleName.ADMIN]
        assert Permission.MANAGE_SECRETS in admin_perms
        assert Permission.MANAGE_MCP in admin_perms
        assert Permission.VIEW_LLM_SETTINGS in admin_perms
        assert Permission.EDIT_LLM_SETTINGS in admin_perms
        assert Permission.VIEW_BILLING in admin_perms
        assert Permission.ADD_CREDITS in admin_perms
        assert Permission.INVITE_USER_TO_ORGANIZATION in admin_perms
        assert Permission.CHANGE_USER_ROLE_MEMBER in admin_perms
        assert Permission.CHANGE_USER_ROLE_ADMIN in admin_perms
        assert Permission.MANAGE_AUTOMATIONS in admin_perms
        # Admin should NOT have owner-only permissions
        assert Permission.CHANGE_USER_ROLE_OWNER not in admin_perms
        assert Permission.CHANGE_ORGANIZATION_NAME not in admin_perms
        assert Permission.DELETE_ORGANIZATION not in admin_perms

    def test_member_has_limited_permissions(self):
        """
        GIVEN: ROLE_PERMISSIONS mapping
        WHEN: Checking member permissions
        THEN: Member has limited permissions
        """
        member_perms = ROLE_PERMISSIONS[RoleName.MEMBER]
        # Member has basic settings permissions
        assert Permission.MANAGE_SECRETS in member_perms
        assert Permission.MANAGE_MCP in member_perms
        assert Permission.MANAGE_INTEGRATIONS in member_perms
        assert Permission.MANAGE_APPLICATION_SETTINGS in member_perms
        assert Permission.MANAGE_API_KEYS in member_perms
        assert Permission.MANAGE_AUTOMATIONS in member_perms
        assert Permission.VIEW_LLM_SETTINGS in member_perms
        assert Permission.VIEW_ORG_SETTINGS in member_perms
        # Member should NOT have admin/owner permissions
        assert Permission.EDIT_LLM_SETTINGS not in member_perms
        assert Permission.VIEW_BILLING not in member_perms
        assert Permission.ADD_CREDITS not in member_perms
        assert Permission.INVITE_USER_TO_ORGANIZATION not in member_perms
        assert Permission.CHANGE_USER_ROLE_MEMBER not in member_perms
        assert Permission.CHANGE_USER_ROLE_ADMIN not in member_perms
        assert Permission.CHANGE_USER_ROLE_OWNER not in member_perms
        assert Permission.CHANGE_ORGANIZATION_NAME not in member_perms
        assert Permission.DELETE_ORGANIZATION not in member_perms


# =============================================================================
# Tests for get_role_permissions function
# =============================================================================


class TestGetRolePermissions:
    """Tests for get_role_permissions function."""

    def test_get_owner_permissions(self):
        """
        GIVEN: Role name 'owner'
        WHEN: get_role_permissions is called
        THEN: Owner permissions are returned
        """
        perms = get_role_permissions('owner')
        assert Permission.DELETE_ORGANIZATION in perms
        assert Permission.CHANGE_ORGANIZATION_NAME in perms

    def test_get_admin_permissions(self):
        """
        GIVEN: Role name 'admin'
        WHEN: get_role_permissions is called
        THEN: Admin permissions are returned
        """
        perms = get_role_permissions('admin')
        assert Permission.EDIT_LLM_SETTINGS in perms
        assert Permission.DELETE_ORGANIZATION not in perms

    def test_get_member_permissions(self):
        """
        GIVEN: Role name 'member'
        WHEN: get_role_permissions is called
        THEN: Member permissions are returned
        """
        perms = get_role_permissions('member')
        assert Permission.VIEW_LLM_SETTINGS in perms
        assert Permission.EDIT_LLM_SETTINGS not in perms

    def test_get_invalid_role_permissions(self):
        """
        GIVEN: Invalid role name
        WHEN: get_role_permissions is called
        THEN: Empty frozenset is returned
        """
        perms = get_role_permissions('invalid_role')
        assert perms == frozenset()


# =============================================================================
# Tests for has_permission function
# =============================================================================


class TestHasPermission:
    """Tests for has_permission function."""

    def test_owner_has_delete_organization_permission(self):
        """
        GIVEN: User with owner role
        WHEN: Checking for DELETE_ORGANIZATION permission
        THEN: Returns True
        """
        mock_role = MagicMock()
        mock_role.name = 'owner'
        assert has_permission(mock_role, Permission.DELETE_ORGANIZATION) is True

    def test_owner_has_view_llm_settings_permission(self):
        """
        GIVEN: User with owner role
        WHEN: Checking for VIEW_LLM_SETTINGS permission
        THEN: Returns True
        """
        mock_role = MagicMock()
        mock_role.name = 'owner'
        assert has_permission(mock_role, Permission.VIEW_LLM_SETTINGS) is True

    def test_admin_has_edit_llm_settings_permission(self):
        """
        GIVEN: User with admin role
        WHEN: Checking for EDIT_LLM_SETTINGS permission
        THEN: Returns True
        """
        mock_role = MagicMock()
        mock_role.name = 'admin'
        assert has_permission(mock_role, Permission.EDIT_LLM_SETTINGS) is True

    def test_admin_lacks_delete_organization_permission(self):
        """
        GIVEN: User with admin role
        WHEN: Checking for DELETE_ORGANIZATION permission
        THEN: Returns False
        """
        mock_role = MagicMock()
        mock_role.name = 'admin'
        assert has_permission(mock_role, Permission.DELETE_ORGANIZATION) is False

    def test_member_has_view_llm_settings_permission(self):
        """
        GIVEN: User with member role
        WHEN: Checking for VIEW_LLM_SETTINGS permission
        THEN: Returns True
        """
        mock_role = MagicMock()
        mock_role.name = 'member'
        assert has_permission(mock_role, Permission.VIEW_LLM_SETTINGS) is True

    def test_member_lacks_edit_llm_settings_permission(self):
        """
        GIVEN: User with member role
        WHEN: Checking for EDIT_LLM_SETTINGS permission
        THEN: Returns False
        """
        mock_role = MagicMock()
        mock_role.name = 'member'
        assert has_permission(mock_role, Permission.EDIT_LLM_SETTINGS) is False

    def test_member_lacks_delete_organization_permission(self):
        """
        GIVEN: User with member role
        WHEN: Checking for DELETE_ORGANIZATION permission
        THEN: Returns False
        """
        mock_role = MagicMock()
        mock_role.name = 'member'
        assert has_permission(mock_role, Permission.DELETE_ORGANIZATION) is False

    def test_invalid_role_has_no_permissions(self):
        """
        GIVEN: User with invalid role
        WHEN: Checking for any permission
        THEN: Returns False
        """
        mock_role = MagicMock()
        mock_role.name = 'invalid_role'
        assert has_permission(mock_role, Permission.VIEW_LLM_SETTINGS) is False
        assert has_permission(mock_role, Permission.DELETE_ORGANIZATION) is False


# =============================================================================
# Tests for get_user_org_role function
# =============================================================================


class TestGetUserOrgRole:
    """Tests for get_user_org_role function."""

    @pytest.mark.asyncio
    async def test_returns_role_when_member_exists(self):
        """
        GIVEN: User is a member of organization with role
        WHEN: get_user_org_role is called
        THEN: Role object is returned
        """
        user_id = str(uuid4())
        org_id = uuid4()

        mock_org_member = MagicMock()
        mock_org_member.role_id = 1

        mock_role = MagicMock()
        mock_role.name = 'admin'

        with (
            patch(
                'server.auth.authorization.OrgMemberStore.get_org_member',
                new_callable=AsyncMock,
                return_value=mock_org_member,
            ),
            patch(
                'server.auth.authorization.RoleStore.get_role_by_id',
                new_callable=AsyncMock,
                return_value=mock_role,
            ),
        ):
            result = await get_user_org_role(user_id, org_id)
            assert result == mock_role

    @pytest.mark.asyncio
    async def test_returns_none_when_not_member(self):
        """
        GIVEN: User is not a member of organization
        WHEN: get_user_org_role is called
        THEN: None is returned
        """
        user_id = str(uuid4())
        org_id = uuid4()

        with patch(
            'server.auth.authorization.OrgMemberStore.get_org_member',
            new_callable=AsyncMock,
            return_value=None,
        ):
            result = await get_user_org_role(user_id, org_id)
            assert result is None

    @pytest.mark.asyncio
    async def test_returns_role_when_org_id_is_none(self):
        """
        GIVEN: User with a current organization
        WHEN: get_user_org_role is called with org_id=None
        THEN: Role object is returned using get_org_member_for_current_org
        """
        user_id = str(uuid4())

        mock_org_member = MagicMock()
        mock_org_member.role_id = 1

        mock_role = MagicMock()
        mock_role.name = 'admin'

        with (
            patch(
                'server.auth.authorization.OrgMemberStore.get_org_member_for_current_org',
                new_callable=AsyncMock,
                return_value=mock_org_member,
            ) as mock_get_current,
            patch(
                'server.auth.authorization.OrgMemberStore.get_org_member',
                new_callable=AsyncMock,
            ) as mock_get_org_member,
            patch(
                'server.auth.authorization.RoleStore.get_role_by_id',
                new_callable=AsyncMock,
                return_value=mock_role,
            ),
        ):
            result = await get_user_org_role(user_id, None)
            assert result == mock_role
            mock_get_current.assert_called_once()
            mock_get_org_member.assert_not_called()

    @pytest.mark.asyncio
    async def test_returns_none_when_org_id_is_none_and_no_current_org(self):
        """
        GIVEN: User with no current organization membership
        WHEN: get_user_org_role is called with org_id=None
        THEN: None is returned
        """
        user_id = str(uuid4())

        with patch(
            'server.auth.authorization.OrgMemberStore.get_org_member_for_current_org',
            new_callable=AsyncMock,
            return_value=None,
        ):
            result = await get_user_org_role(user_id, None)
            assert result is None


# =============================================================================
# Tests for require_permission dependency
# =============================================================================


def _create_mock_request(api_key_org_id=None):
    """Helper to create a mock request with optional api_key_org_id."""
    mock_request = MagicMock()
    mock_user_auth = MagicMock()
    mock_user_auth.get_api_key_org_id.return_value = api_key_org_id
    mock_request.state.user_auth = mock_user_auth
    return mock_request


class TestRequirePermission:
    """Tests for require_permission dependency factory."""

    @pytest.mark.asyncio
    async def test_returns_user_id_when_authorized(self):
        """
        GIVEN: User with required permission
        WHEN: Permission checker is called
        THEN: User ID is returned
        """
        user_id = str(uuid4())
        org_id = uuid4()
        mock_request = _create_mock_request()

        mock_role = MagicMock()
        mock_role.name = 'admin'

        with patch(
            'server.auth.authorization.get_user_org_role',
            AsyncMock(return_value=mock_role),
        ):
            permission_checker = require_permission(Permission.VIEW_LLM_SETTINGS)
            result = await permission_checker(
                request=mock_request, org_id=org_id, user_id=user_id
            )
            assert result == user_id

    @pytest.mark.asyncio
    async def test_raises_401_when_not_authenticated(self):
        """
        GIVEN: No user ID (not authenticated)
        WHEN: Permission checker is called
        THEN: 401 Unauthorized is raised
        """
        org_id = uuid4()
        mock_request = _create_mock_request()

        permission_checker = require_permission(Permission.VIEW_LLM_SETTINGS)
        with pytest.raises(HTTPException) as exc_info:
            await permission_checker(request=mock_request, org_id=org_id, user_id=None)

        assert exc_info.value.status_code == 401
        assert 'not authenticated' in exc_info.value.detail.lower()

    @pytest.mark.asyncio
    async def test_raises_403_when_not_member(self):
        """
        GIVEN: User is not a member of organization
        WHEN: Permission checker is called
        THEN: 403 Forbidden is raised
        """
        user_id = str(uuid4())
        org_id = uuid4()
        mock_request = _create_mock_request()

        with patch(
            'server.auth.authorization.get_user_org_role',
            AsyncMock(return_value=None),
        ):
            permission_checker = require_permission(Permission.VIEW_LLM_SETTINGS)
            with pytest.raises(HTTPException) as exc_info:
                await permission_checker(
                    request=mock_request, org_id=org_id, user_id=user_id
                )

            assert exc_info.value.status_code == 403
            assert 'not a member' in exc_info.value.detail.lower()

    @pytest.mark.asyncio
    async def test_raises_403_when_insufficient_permission(self):
        """
        GIVEN: User without required permission
        WHEN: Permission checker is called
        THEN: 403 Forbidden is raised
        """
        user_id = str(uuid4())
        org_id = uuid4()
        mock_request = _create_mock_request()

        mock_role = MagicMock()
        mock_role.name = 'member'

        with patch(
            'server.auth.authorization.get_user_org_role',
            AsyncMock(return_value=mock_role),
        ):
            permission_checker = require_permission(Permission.DELETE_ORGANIZATION)
            with pytest.raises(HTTPException) as exc_info:
                await permission_checker(
                    request=mock_request, org_id=org_id, user_id=user_id
                )

            assert exc_info.value.status_code == 403
            assert 'delete_organization' in exc_info.value.detail.lower()

    @pytest.mark.asyncio
    async def test_owner_can_delete_organization(self):
        """
        GIVEN: User with owner role
        WHEN: DELETE_ORGANIZATION permission is required
        THEN: User ID is returned
        """
        user_id = str(uuid4())
        org_id = uuid4()
        mock_request = _create_mock_request()

        mock_role = MagicMock()
        mock_role.name = 'owner'

        with patch(
            'server.auth.authorization.get_user_org_role',
            AsyncMock(return_value=mock_role),
        ):
            permission_checker = require_permission(Permission.DELETE_ORGANIZATION)
            result = await permission_checker(
                request=mock_request, org_id=org_id, user_id=user_id
            )
            assert result == user_id

    @pytest.mark.asyncio
    async def test_admin_cannot_delete_organization(self):
        """
        GIVEN: User with admin role
        WHEN: DELETE_ORGANIZATION permission is required
        THEN: 403 Forbidden is raised
        """
        user_id = str(uuid4())
        org_id = uuid4()
        mock_request = _create_mock_request()

        mock_role = MagicMock()
        mock_role.name = 'admin'

        with patch(
            'server.auth.authorization.get_user_org_role',
            AsyncMock(return_value=mock_role),
        ):
            permission_checker = require_permission(Permission.DELETE_ORGANIZATION)
            with pytest.raises(HTTPException) as exc_info:
                await permission_checker(
                    request=mock_request, org_id=org_id, user_id=user_id
                )

            assert exc_info.value.status_code == 403

    @pytest.mark.asyncio
    async def test_logs_warning_on_insufficient_permission(self):
        """
        GIVEN: User without required permission
        WHEN: Permission checker is called
        THEN: Warning is logged with details
        """
        user_id = str(uuid4())
        org_id = uuid4()
        mock_request = _create_mock_request()

        mock_role = MagicMock()
        mock_role.name = 'member'

        with (
            patch(
                'server.auth.authorization.get_user_org_role',
                AsyncMock(return_value=mock_role),
            ),
            patch('server.auth.authorization.logger') as mock_logger,
        ):
            permission_checker = require_permission(Permission.DELETE_ORGANIZATION)
            with pytest.raises(HTTPException):
                await permission_checker(
                    request=mock_request, org_id=org_id, user_id=user_id
                )

            mock_logger.warning.assert_called()
            call_args = mock_logger.warning.call_args
            assert call_args[1]['extra']['user_id'] == user_id
            assert call_args[1]['extra']['user_role'] == 'member'
            assert call_args[1]['extra']['required_permission'] == 'delete_organization'

    @pytest.mark.asyncio
    async def test_returns_user_id_when_org_id_is_none(self):
        """
        GIVEN: User with required permission in their current org
        WHEN: Permission checker is called with org_id=None
        THEN: User ID is returned
        """
        user_id = str(uuid4())
        mock_request = _create_mock_request()

        mock_role = MagicMock()
        mock_role.name = 'admin'

        with patch(
            'server.auth.authorization.get_user_org_role',
            AsyncMock(return_value=mock_role),
        ) as mock_get_role:
            permission_checker = require_permission(Permission.VIEW_LLM_SETTINGS)
            result = await permission_checker(
                request=mock_request, org_id=None, user_id=user_id
            )
            assert result == user_id
            mock_get_role.assert_called_once_with(user_id, None)

    @pytest.mark.asyncio
    async def test_raises_403_when_org_id_is_none_and_not_member(self):
        """
        GIVEN: User not a member of their current organization
        WHEN: Permission checker is called with org_id=None
        THEN: HTTPException with 403 status is raised
        """
        user_id = str(uuid4())
        mock_request = _create_mock_request()

        with patch(
            'server.auth.authorization.get_user_org_role',
            AsyncMock(return_value=None),
        ):
            permission_checker = require_permission(Permission.VIEW_LLM_SETTINGS)
            with pytest.raises(HTTPException) as exc_info:
                await permission_checker(
                    request=mock_request, org_id=None, user_id=user_id
                )

            assert exc_info.value.status_code == 403
            assert 'not a member' in exc_info.value.detail


# =============================================================================
# Tests for permission-based access control scenarios
# =============================================================================


class TestPermissionScenarios:
    """Tests for real-world permission scenarios."""

    @pytest.mark.asyncio
    async def test_member_can_manage_secrets(self):
        """
        GIVEN: User with member role
        WHEN: MANAGE_SECRETS permission is required
        THEN: User ID is returned
        """
        user_id = str(uuid4())
        org_id = uuid4()
        mock_request = _create_mock_request()

        mock_role = MagicMock()
        mock_role.name = 'member'

        with patch(
            'server.auth.authorization.get_user_org_role',
            AsyncMock(return_value=mock_role),
        ):
            permission_checker = require_permission(Permission.MANAGE_SECRETS)
            result = await permission_checker(
                request=mock_request, org_id=org_id, user_id=user_id
            )
            assert result == user_id

    @pytest.mark.asyncio
    async def test_member_cannot_invite_users(self):
        """
        GIVEN: User with member role
        WHEN: INVITE_USER_TO_ORGANIZATION permission is required
        THEN: 403 Forbidden is raised
        """
        user_id = str(uuid4())
        org_id = uuid4()
        mock_request = _create_mock_request()

        mock_role = MagicMock()
        mock_role.name = 'member'

        with patch(
            'server.auth.authorization.get_user_org_role',
            AsyncMock(return_value=mock_role),
        ):
            permission_checker = require_permission(
                Permission.INVITE_USER_TO_ORGANIZATION
            )
            with pytest.raises(HTTPException) as exc_info:
                await permission_checker(
                    request=mock_request, org_id=org_id, user_id=user_id
                )

            assert exc_info.value.status_code == 403

    @pytest.mark.asyncio
    async def test_admin_can_invite_users(self):
        """
        GIVEN: User with admin role
        WHEN: INVITE_USER_TO_ORGANIZATION permission is required
        THEN: User ID is returned
        """
        user_id = str(uuid4())
        org_id = uuid4()
        mock_request = _create_mock_request()

        mock_role = MagicMock()
        mock_role.name = 'admin'

        with patch(
            'server.auth.authorization.get_user_org_role',
            AsyncMock(return_value=mock_role),
        ):
            permission_checker = require_permission(
                Permission.INVITE_USER_TO_ORGANIZATION
            )
            result = await permission_checker(
                request=mock_request, org_id=org_id, user_id=user_id
            )
            assert result == user_id

    @pytest.mark.asyncio
    async def test_admin_cannot_change_owner_role(self):
        """
        GIVEN: User with admin role
        WHEN: CHANGE_USER_ROLE_OWNER permission is required
        THEN: 403 Forbidden is raised
        """
        user_id = str(uuid4())
        org_id = uuid4()
        mock_request = _create_mock_request()

        mock_role = MagicMock()
        mock_role.name = 'admin'

        with patch(
            'server.auth.authorization.get_user_org_role',
            AsyncMock(return_value=mock_role),
        ):
            permission_checker = require_permission(Permission.CHANGE_USER_ROLE_OWNER)
            with pytest.raises(HTTPException) as exc_info:
                await permission_checker(
                    request=mock_request, org_id=org_id, user_id=user_id
                )

            assert exc_info.value.status_code == 403

    @pytest.mark.asyncio
    async def test_owner_can_change_owner_role(self):
        """
        GIVEN: User with owner role
        WHEN: CHANGE_USER_ROLE_OWNER permission is required
        THEN: User ID is returned
        """
        user_id = str(uuid4())
        org_id = uuid4()
        mock_request = _create_mock_request()

        mock_role = MagicMock()
        mock_role.name = 'owner'

        with patch(
            'server.auth.authorization.get_user_org_role',
            AsyncMock(return_value=mock_role),
        ):
            permission_checker = require_permission(Permission.CHANGE_USER_ROLE_OWNER)
            result = await permission_checker(
                request=mock_request, org_id=org_id, user_id=user_id
            )
            assert result == user_id


# =============================================================================
# Tests for API key organization validation
# =============================================================================


class TestApiKeyOrgValidation:
    """Tests for API key organization binding validation in require_permission."""

    @pytest.mark.asyncio
    async def test_allows_access_when_api_key_org_matches_target_org(self):
        """
        GIVEN: API key with org_id that matches the target org_id in the request
        WHEN: Permission checker is called
        THEN: User ID is returned (access allowed)
        """
        # Arrange
        user_id = str(uuid4())
        org_id = uuid4()
        mock_request = _create_mock_request(api_key_org_id=org_id)

        mock_role = MagicMock()
        mock_role.name = 'admin'

        # Act & Assert
        with patch(
            'server.auth.authorization.get_user_org_role',
            AsyncMock(return_value=mock_role),
        ):
            permission_checker = require_permission(Permission.VIEW_LLM_SETTINGS)
            result = await permission_checker(
                request=mock_request, org_id=org_id, user_id=user_id
            )
            assert result == user_id

    @pytest.mark.asyncio
    async def test_denies_access_when_api_key_org_mismatches_target_org(self):
        """
        GIVEN: API key created for Org A, but user tries to access Org B
        WHEN: Permission checker is called
        THEN: 403 Forbidden is raised with org mismatch message
        """
        # Arrange
        user_id = str(uuid4())
        api_key_org_id = uuid4()  # Org A - where API key was created
        target_org_id = uuid4()  # Org B - where user is trying to access
        mock_request = _create_mock_request(api_key_org_id=api_key_org_id)

        # Act & Assert
        permission_checker = require_permission(Permission.VIEW_LLM_SETTINGS)
        with pytest.raises(HTTPException) as exc_info:
            await permission_checker(
                request=mock_request, org_id=target_org_id, user_id=user_id
            )

        assert exc_info.value.status_code == 403
        assert (
            'API key is not authorized for this organization' in exc_info.value.detail
        )

    @pytest.mark.asyncio
    async def test_allows_access_for_legacy_api_key_without_org_binding(self):
        """
        GIVEN: Legacy API key without org_id binding (org_id is None)
        WHEN: Permission checker is called
        THEN: Falls through to normal permission check (backward compatible)
        """
        # Arrange
        user_id = str(uuid4())
        org_id = uuid4()
        mock_request = _create_mock_request(api_key_org_id=None)

        mock_role = MagicMock()
        mock_role.name = 'admin'

        # Act & Assert
        with patch(
            'server.auth.authorization.get_user_org_role',
            AsyncMock(return_value=mock_role),
        ):
            permission_checker = require_permission(Permission.VIEW_LLM_SETTINGS)
            result = await permission_checker(
                request=mock_request, org_id=org_id, user_id=user_id
            )
            assert result == user_id

    @pytest.mark.asyncio
    async def test_allows_access_for_cookie_auth_without_api_key_org_id(self):
        """
        GIVEN: Cookie-based authentication (no api_key_org_id in user_auth)
        WHEN: Permission checker is called
        THEN: Falls through to normal permission check
        """
        # Arrange
        user_id = str(uuid4())
        org_id = uuid4()
        mock_request = _create_mock_request(api_key_org_id=None)

        mock_role = MagicMock()
        mock_role.name = 'admin'

        # Act & Assert
        with patch(
            'server.auth.authorization.get_user_org_role',
            AsyncMock(return_value=mock_role),
        ):
            permission_checker = require_permission(Permission.VIEW_LLM_SETTINGS)
            result = await permission_checker(
                request=mock_request, org_id=org_id, user_id=user_id
            )
            assert result == user_id

    @pytest.mark.asyncio
    async def test_logs_warning_on_api_key_org_mismatch(self):
        """
        GIVEN: API key org_id doesn't match target org_id
        WHEN: Permission checker is called
        THEN: Warning is logged with org mismatch details
        """
        # Arrange
        user_id = str(uuid4())
        api_key_org_id = uuid4()
        target_org_id = uuid4()
        mock_request = _create_mock_request(api_key_org_id=api_key_org_id)

        # Act & Assert
        with patch('server.auth.authorization.logger') as mock_logger:
            permission_checker = require_permission(Permission.VIEW_LLM_SETTINGS)
            with pytest.raises(HTTPException):
                await permission_checker(
                    request=mock_request, org_id=target_org_id, user_id=user_id
                )

            mock_logger.warning.assert_called()
            call_args = mock_logger.warning.call_args
            assert call_args[1]['extra']['user_id'] == user_id
            assert call_args[1]['extra']['api_key_org_id'] == str(api_key_org_id)
            assert call_args[1]['extra']['target_org_id'] == str(target_org_id)


class TestGetApiKeyOrgIdFromRequest:
    """Tests for get_api_key_org_id_from_request helper function."""

    @pytest.mark.asyncio
    async def test_returns_org_id_when_user_auth_has_api_key_org_id(self):
        """
        GIVEN: Request with user_auth that has api_key_org_id
        WHEN: get_api_key_org_id_from_request is called
        THEN: Returns the api_key_org_id
        """
        # Arrange
        org_id = uuid4()
        mock_request = _create_mock_request(api_key_org_id=org_id)

        # Act
        result = await get_api_key_org_id_from_request(mock_request)

        # Assert
        assert result == org_id

    @pytest.mark.asyncio
    async def test_returns_none_when_user_auth_has_no_api_key_org_id(self):
        """
        GIVEN: Request with user_auth that has no api_key_org_id (cookie auth)
        WHEN: get_api_key_org_id_from_request is called
        THEN: Returns None
        """
        # Arrange
        mock_request = _create_mock_request(api_key_org_id=None)

        # Act
        result = await get_api_key_org_id_from_request(mock_request)

        # Assert
        assert result is None

    @pytest.mark.asyncio
    async def test_returns_none_when_no_user_auth_in_request(self):
        """
        GIVEN: Request without user_auth in state
        WHEN: get_api_key_org_id_from_request is called
        THEN: Returns None
        """
        # Arrange
        mock_request = MagicMock()
        mock_request.state.user_auth = None

        # Act
        result = await get_api_key_org_id_from_request(mock_request)

        # Assert
        assert result is None


# =============================================================================
# Tests for require_financial_data_access dependency
# =============================================================================


def _create_mock_request_with_email(api_key_org_id=None, user_email='user@example.com'):
    """Helper to create a mock request with optional api_key_org_id and email."""
    mock_request = MagicMock()
    mock_user_auth = MagicMock()
    # get_api_key_org_id is sync, not async
    mock_user_auth.get_api_key_org_id.return_value = api_key_org_id
    # get_user_email is async
    mock_user_auth.get_user_email = AsyncMock(return_value=user_email)
    mock_request.state.user_auth = mock_user_auth
    return mock_request


class TestRequireFinancialDataAccess:
    """Tests for require_financial_data_access compound authorization dependency."""

    @pytest.mark.asyncio
    async def test_grants_access_for_openhands_email(self):
        """
        GIVEN: User with @openhands.dev email
        WHEN: require_financial_data_access is called
        THEN: Returns user_id (access granted)
        """
        from server.auth.authorization import require_financial_data_access

        # Arrange
        user_id = str(uuid4())
        org_id = uuid4()
        mock_request = _create_mock_request_with_email(user_email='admin@openhands.dev')

        with patch(
            'server.auth.authorization.get_user_auth',
            AsyncMock(return_value=mock_request.state.user_auth),
        ):
            # Act
            result = await require_financial_data_access(
                request=mock_request, org_id=org_id, user_id=user_id
            )

            # Assert
            assert result == user_id

    @pytest.mark.asyncio
    async def test_grants_access_for_owner_role(self):
        """
        GIVEN: User with owner role in organization (non-@openhands.dev email)
        WHEN: require_financial_data_access is called
        THEN: Returns user_id (access granted)
        """
        from server.auth.authorization import require_financial_data_access

        # Arrange
        user_id = str(uuid4())
        org_id = uuid4()
        mock_request = _create_mock_request_with_email(user_email='user@company.com')
        mock_role = MagicMock()
        mock_role.name = 'owner'

        with (
            patch(
                'server.auth.authorization.get_user_auth',
                AsyncMock(return_value=mock_request.state.user_auth),
            ),
            patch(
                'server.auth.authorization.get_user_org_role',
                AsyncMock(return_value=mock_role),
            ),
        ):
            # Act
            result = await require_financial_data_access(
                request=mock_request, org_id=org_id, user_id=user_id
            )

            # Assert
            assert result == user_id

    @pytest.mark.asyncio
    async def test_grants_access_for_admin_role(self):
        """
        GIVEN: User with admin role in organization (non-@openhands.dev email)
        WHEN: require_financial_data_access is called
        THEN: Returns user_id (access granted)
        """
        from server.auth.authorization import require_financial_data_access

        # Arrange
        user_id = str(uuid4())
        org_id = uuid4()
        mock_request = _create_mock_request_with_email(user_email='user@company.com')
        mock_role = MagicMock()
        mock_role.name = 'admin'

        with (
            patch(
                'server.auth.authorization.get_user_auth',
                AsyncMock(return_value=mock_request.state.user_auth),
            ),
            patch(
                'server.auth.authorization.get_user_org_role',
                AsyncMock(return_value=mock_role),
            ),
        ):
            # Act
            result = await require_financial_data_access(
                request=mock_request, org_id=org_id, user_id=user_id
            )

            # Assert
            assert result == user_id

    @pytest.mark.asyncio
    async def test_denies_access_for_member_role_without_openhands_email(self):
        """
        GIVEN: User with member role (not admin/owner) and non-@openhands.dev email
        WHEN: require_financial_data_access is called
        THEN: Raises 403 Forbidden
        """
        from server.auth.authorization import require_financial_data_access

        # Arrange
        user_id = str(uuid4())
        org_id = uuid4()
        mock_request = _create_mock_request_with_email(user_email='user@company.com')
        mock_role = MagicMock()
        mock_role.name = 'member'

        with (
            patch(
                'server.auth.authorization.get_user_auth',
                AsyncMock(return_value=mock_request.state.user_auth),
            ),
            patch(
                'server.auth.authorization.get_user_org_role',
                AsyncMock(return_value=mock_role),
            ),
        ):
            # Act & Assert
            with pytest.raises(HTTPException) as exc_info:
                await require_financial_data_access(
                    request=mock_request, org_id=org_id, user_id=user_id
                )

            assert exc_info.value.status_code == 403
            assert 'admins, owners, or OpenHands' in exc_info.value.detail

    @pytest.mark.asyncio
    async def test_denies_access_for_non_member(self):
        """
        GIVEN: User who is not a member of the organization
        WHEN: require_financial_data_access is called
        THEN: Raises 403 Forbidden
        """
        from server.auth.authorization import require_financial_data_access

        # Arrange
        user_id = str(uuid4())
        org_id = uuid4()
        mock_request = _create_mock_request_with_email(user_email='user@company.com')

        with (
            patch(
                'server.auth.authorization.get_user_auth',
                AsyncMock(return_value=mock_request.state.user_auth),
            ),
            patch(
                'server.auth.authorization.get_user_org_role',
                AsyncMock(return_value=None),
            ),
        ):
            # Act & Assert
            with pytest.raises(HTTPException) as exc_info:
                await require_financial_data_access(
                    request=mock_request, org_id=org_id, user_id=user_id
                )

            assert exc_info.value.status_code == 403
            assert 'not a member' in exc_info.value.detail

    @pytest.mark.asyncio
    async def test_denies_access_when_not_authenticated(self):
        """
        GIVEN: No user_id (not authenticated)
        WHEN: require_financial_data_access is called
        THEN: Raises 401 Unauthorized
        """
        from server.auth.authorization import require_financial_data_access

        # Arrange
        org_id = uuid4()
        mock_request = _create_mock_request_with_email()

        # Act & Assert
        with pytest.raises(HTTPException) as exc_info:
            await require_financial_data_access(
                request=mock_request, org_id=org_id, user_id=None
            )

        assert exc_info.value.status_code == 401
        assert 'not authenticated' in exc_info.value.detail

    @pytest.mark.asyncio
    async def test_denies_access_when_api_key_org_mismatch(self):
        """
        GIVEN: API key created for Org A, but user tries to access Org B
        WHEN: require_financial_data_access is called
        THEN: Raises 403 Forbidden with org mismatch message
        """
        from server.auth.authorization import require_financial_data_access

        # Arrange
        user_id = str(uuid4())
        api_key_org_id = uuid4()  # Org A
        target_org_id = uuid4()  # Org B
        mock_request = _create_mock_request_with_email(
            api_key_org_id=api_key_org_id, user_email='admin@openhands.dev'
        )

        # Act & Assert
        with pytest.raises(HTTPException) as exc_info:
            await require_financial_data_access(
                request=mock_request, org_id=target_org_id, user_id=user_id
            )

        assert exc_info.value.status_code == 403
        assert 'API key is not authorized' in exc_info.value.detail
