"""Comprehensive RBAC system tests — covers all 5 RBAC phases.

Sections:
1.  Permission-Resolver (app/core/permissions.py) — unit tests, no DB
2.  Permission-Registry (app/core/permission_registry.py) — unit tests, no DB
3.  Group CRUD (app/routes/groups.py + app/services/group_service.py) — DB required
4.  RBAC Route-Guard (app/deps.py) — DB required
5.  Self-Modification Prevention (app/routes/users.py) — DB required
6.  Cache-Invalidierung (app/core/permissions.py) — DB + Redis required
7.  Mail-Plugin RBAC (app/plugins/builtins/mail/routes.py) — DB required
8.  Field-Level Permission (app/core/permissions.py + services) — unit + DB
9.  Integration Tests — DB + Redis required
"""

from __future__ import annotations

import json
import uuid
from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker

from app.core.auth import hash_password
from app.core.db import close_engine, reset_engine_for_testing
from app.core.permission_registry import (
    CORE_PERMISSIONS,
    PermissionRegistry,
    get_permission_registry,
    init_permission_registry,
)
from app.core.permissions import (
    _matches_permission,
    _normalize_permissions,
    check_field_access,
    check_permission,
    filter_fields_by_permission,
    invalidate_all_user_permissions,
    invalidate_permission_cache,
    resolve_permissions,
)
from app.core.service_container import get_container
from app.main import create_app
from app.models.group import Group, UserGroup
from app.models.role import Role
from app.models.user import User, UserTenant
from app.plugins.builtins.mail import MailPlugin
from app.plugins.builtins.mail.models import (
    Mail,
    MailAccount,
    MailAccountDelegate,
    MailFolder,
)
from app.plugins.registry import reset_registry_for_testing
from app.services.plugin_service import reset_plugin_service_for_testing
from tests.conftest import ORIGIN_HEADER, login_client, seed_tenant_and_users


# ═══════════════════════════════════════════════════════════════
# CSRF-AWARE HELPERS
# ═══════════════════════════════════════════════════════════════


async def login_with_csrf(client: AsyncClient, email: str, password: str = "TestPass123!") -> str:
    """Login via HTTP API and return the CSRF token.

    The client cookies are set automatically by httpx.
    The CSRF token must be sent as X-CSRF-Token header on unsafe methods.
    """
    resp = await client.post(
        "/api/v1/auth/login",
        json={"email": email, "password": password},
        headers=ORIGIN_HEADER,
    )
    assert resp.status_code == 200, f"Login failed: {resp.status_code} {resp.text}"
    return resp.json()["csrf_token"]


def csrf_headers(csrf_token: str) -> dict:
    """Return headers dict with Origin + X-CSRF-Token for unsafe methods."""
    return {**ORIGIN_HEADER, "X-CSRF-Token": csrf_token}


# ═══════════════════════════════════════════════════════════════
# SECTION 1: Permission-Resolver Tests (unit tests, no DB needed)
# ═══════════════════════════════════════════════════════════════


class TestPermissionResolverUnit:
    """Unit tests for permission resolver functions — no DB required."""

    # ── _matches_permission ──

    def test_matches_exact_permission(self):
        """Exact match: companies:read matches companies:read."""
        assert _matches_permission("companies:read", "companies:read") is True

    def test_matches_wildcard_all(self):
        """Wildcard *:* matches everything."""
        assert _matches_permission("*:*", "companies:read") is True
        assert _matches_permission("*:*", "contacts:write") is True
        assert _matches_permission("*:*", "mail:send") is True

    def test_matches_wildcard_module(self):
        """Wildcard companies:* matches all actions for companies."""
        assert _matches_permission("companies:*", "companies:read") is True
        assert _matches_permission("companies:*", "companies:write") is True
        assert _matches_permission("companies:*", "companies:delete") is True

    def test_matches_wildcard_action(self):
        """Wildcard *:read matches read action for any module."""
        assert _matches_permission("*:read", "companies:read") is True
        assert _matches_permission("*:read", "contacts:read") is True
        assert _matches_permission("*:read", "mail:read") is True

    def test_matches_no_match_different_module(self):
        """companies:read does not match contacts:read."""
        assert _matches_permission("companies:read", "contacts:read") is False

    def test_matches_no_match_different_action(self):
        """companies:read does not match companies:write."""
        assert _matches_permission("companies:read", "companies:write") is False

    def test_matches_wildcard_module_does_not_match_other_module(self):
        """companies:* does not match contacts:read."""
        assert _matches_permission("companies:*", "contacts:read") is False

    def test_matches_wildcard_action_does_not_match_other_action(self):
        """*:read does not match companies:write."""
        assert _matches_permission("*:read", "companies:write") is False

    # ── _normalize_permissions ──

    def test_normalize_list_format(self):
        """List format: ['companies:read', 'contacts:write'] → set."""
        result = _normalize_permissions(["companies:read", "contacts:write"])
        assert result == {"companies:read", "contacts:write"}

    def test_normalize_dict_bool_format(self):
        """Dict-bool format: {'companies:read': true, 'contacts:write': false}."""
        result = _normalize_permissions({
            "companies:read": True,
            "contacts:write": False,
        })
        assert result == {"companies:read"}

    def test_normalize_dict_nested_format(self):
        """Dict-nested format: {'companies': {'read': true, 'write': false}}."""
        result = _normalize_permissions({
            "companies": {"read": True, "write": False, "delete": True},
            "contacts": {"read": True},
        })
        assert result == {"companies:read", "companies:delete", "contacts:read"}

    def test_normalize_empty(self):
        """Empty list/dict returns empty set."""
        assert _normalize_permissions([]) == set()
        assert _normalize_permissions({}) == set()

    def test_normalize_dot_to_colon(self):
        """Dots are replaced with colons: 'companies.read' → 'companies:read'."""
        result = _normalize_permissions(["companies.read"])
        assert result == {"companies:read"}

    # ── check_permission ──

    def test_check_permission_system_admin_bypass(self):
        """System admin bypasses all permission checks."""
        resolved = {"is_system_admin": True, "permissions": [], "denied": []}
        assert check_permission(resolved, "companies:read") is True
        assert check_permission(resolved, "anything:anything") is True

    def test_check_permission_exact_match(self):
        """Exact permission match grants access."""
        resolved = {"is_system_admin": False, "permissions": ["companies:read"], "denied": []}
        assert check_permission(resolved, "companies:read") is True

    def test_check_permission_wildcard_match(self):
        """Wildcard permissions match specific requirements."""
        resolved = {"is_system_admin": False, "permissions": ["companies:*"], "denied": []}
        assert check_permission(resolved, "companies:read") is True
        assert check_permission(resolved, "companies:write") is True

    def test_check_permission_denied_overrides_allowed(self):
        """Denied permission overrides allowed permission."""
        resolved = {
            "is_system_admin": False,
            "permissions": ["companies:read", "companies:write"],
            "denied": ["companies:write"],
        }
        assert check_permission(resolved, "companies:read") is True
        assert check_permission(resolved, "companies:write") is False

    def test_check_permission_denied_wildcard_overrides(self):
        """Denied wildcard overrides specific allowed permission."""
        resolved = {
            "is_system_admin": False,
            "permissions": ["companies:read"],
            "denied": ["companies:*"],
        }
        assert check_permission(resolved, "companies:read") is False

    def test_check_permission_no_match(self):
        """No matching permission denies access."""
        resolved = {"is_system_admin": False, "permissions": ["contacts:read"], "denied": []}
        assert check_permission(resolved, "companies:read") is False

    def test_check_permission_star_star_grants_all(self):
        """*:* grants any permission."""
        resolved = {"is_system_admin": False, "permissions": ["*:*"], "denied": []}
        assert check_permission(resolved, "companies:read") is True
        assert check_permission(resolved, "mail:send") is True

    # ── check_field_access ──

    def test_check_field_access_hidden(self):
        """Hidden field returns 'hidden'."""
        resolved = {
            "is_system_admin": False,
            "field_permissions": {"companies": {"annual_revenue": "hidden"}},
        }
        assert check_field_access(resolved, "companies", "annual_revenue") == "hidden"

    def test_check_field_access_readonly(self):
        """Readonly field returns 'readonly'."""
        resolved = {
            "is_system_admin": False,
            "field_permissions": {"companies": {"name": "readonly"}},
        }
        assert check_field_access(resolved, "companies", "name") == "readonly"

    def test_check_field_access_read_default(self):
        """Field without explicit permission returns default 'read'."""
        resolved = {"is_system_admin": False, "field_permissions": {}}
        assert check_field_access(resolved, "companies", "name") == "read"

    def test_check_field_access_system_admin(self):
        """System admin always gets 'read' for any field."""
        resolved = {
            "is_system_admin": True,
            "field_permissions": {"companies": {"annual_revenue": "hidden"}},
        }
        assert check_field_access(resolved, "companies", "annual_revenue") == "read"

    # ── filter_fields_by_permission ──

    def test_filter_fields_removes_hidden(self):
        """Hidden fields are removed from the output."""
        data = {"name": "ACME", "annual_revenue": 1000000, "industry": "IT"}
        resolved = {
            "is_system_admin": False,
            "field_permissions": {"companies": {"annual_revenue": "hidden"}},
        }
        result = filter_fields_by_permission(data, resolved, "companies")
        assert "annual_revenue" not in result
        assert result["name"] == "ACME"
        assert result["industry"] == "IT"

    def test_filter_fields_keeps_readonly(self):
        """Readonly fields are kept in the output."""
        data = {"name": "ACME", "industry": "IT"}
        resolved = {
            "is_system_admin": False,
            "field_permissions": {"companies": {"name": "readonly"}},
        }
        result = filter_fields_by_permission(data, resolved, "companies")
        assert "name" in result
        assert result["name"] == "ACME"

    def test_filter_fields_keeps_read(self):
        """Read fields are kept in the output."""
        data = {"name": "ACME", "industry": "IT"}
        resolved = {
            "is_system_admin": False,
            "field_permissions": {"companies": {"name": "read"}},
        }
        result = filter_fields_by_permission(data, resolved, "companies")
        assert "name" in result

    def test_filter_fields_system_admin_no_filtering(self):
        """System admin: no fields are filtered."""
        data = {"name": "ACME", "annual_revenue": 1000000}
        resolved = {
            "is_system_admin": True,
            "field_permissions": {"companies": {"annual_revenue": "hidden"}},
        }
        result = filter_fields_by_permission(data, resolved, "companies")
        assert result == data

    def test_filter_fields_no_field_permissions_no_filtering(self):
        """No field_permissions in resolved → no filtering."""
        data = {"name": "ACME", "annual_revenue": 1000000}
        resolved = {"is_system_admin": False, "field_permissions": {}}
        result = filter_fields_by_permission(data, resolved, "companies")
        assert result == data

    def test_filter_fields_no_module_perms_no_filtering(self):
        """No permissions for the specific module → no filtering."""
        data = {"name": "ACME", "annual_revenue": 1000000}
        resolved = {
            "is_system_admin": False,
            "field_permissions": {"contacts": {"email": "hidden"}},
        }
        result = filter_fields_by_permission(data, resolved, "companies")
        assert result == data


# ═══════════════════════════════════════════════════════════════
# SECTION 2: Permission-Registry Tests (unit tests, no DB needed)
# ═══════════════════════════════════════════════════════════════

_CORE_ONLY_COUNT = sum(1 for p in CORE_PERMISSIONS if p.get("category") == "core")


class TestPermissionRegistryUnit:
    """Unit tests for the PermissionRegistry — no DB required."""

    def test_initialize_registers_core_permissions(self):
        """initialize() registers all CORE_PERMISSIONS."""
        reg = PermissionRegistry()
        reg.initialize()
        for perm in CORE_PERMISSIONS:
            assert reg.is_valid(perm["key"]), f"Missing core permission: {perm['key']}"

    def test_initialize_with_active_plugins(self):
        """initialize() accepts active_plugin_names set."""
        reg = PermissionRegistry()
        reg.initialize(active_plugin_names={"mail", "dms"})
        assert reg.is_plugin_active("mail") is True
        assert reg.is_plugin_active("dms") is True
        assert reg.is_plugin_active("calendar") is False

    def test_register_plugin_permissions(self):
        """register_plugin_permissions adds plugin permissions."""
        reg = PermissionRegistry()
        reg.initialize()
        reg.register_plugin_permissions("mail", ["mail:read", "mail:send", "mail:config"])
        assert reg.is_valid("mail:read") is True
        assert reg.is_valid("mail:send") is True
        assert reg.is_valid("mail:config") is True

    def test_unregister_plugin_permissions(self):
        """unregister_plugin_permissions removes plugin permissions."""
        reg = PermissionRegistry()
        reg.initialize()
        reg.register_plugin_permissions("mail", ["mail:read", "mail:send"])
        assert reg.is_valid("mail:read") is True
        reg.unregister_plugin_permissions("mail")
        assert reg.is_valid("mail:read") is False
        assert reg.is_valid("mail:send") is False

    def test_is_valid_unknown_permission(self):
        """is_valid returns False for unknown permission."""
        reg = PermissionRegistry()
        reg.initialize()
        assert reg.is_valid("nonexistent:action") is False

    def test_get_all_returns_all_permissions(self):
        """get_all() returns all registered permissions."""
        reg = PermissionRegistry()
        reg.initialize()
        all_perms = reg.get_all()
        assert len(all_perms) == len(CORE_PERMISSIONS)
        reg.register_plugin_permissions("mail", ["mail:read"])
        all_perms = reg.get_all()
        assert len(all_perms) == len(CORE_PERMISSIONS) + 1

    def test_get_core_returns_only_core(self):
        """get_core() returns only core-category permissions (excludes system:admin)."""
        reg = PermissionRegistry()
        reg.initialize()
        reg.register_plugin_permissions("mail", ["mail:read"])
        core = reg.get_core()
        assert all(p.get("category") == "core" for p in core)
        assert len(core) == _CORE_ONLY_COUNT

    def test_get_plugin_permissions_returns_only_plugin(self):
        """get_plugin_permissions() returns only plugin permissions."""
        reg = PermissionRegistry()
        reg.initialize()
        reg.register_plugin_permissions("mail", ["mail:read", "mail:send"])
        plugin_perms = reg.get_plugin_permissions()
        assert all(p.get("category") == "plugins" for p in plugin_perms)
        assert len(plugin_perms) == 2

    def test_get_grouped(self):
        """get_grouped() returns permissions grouped by category."""
        reg = PermissionRegistry()
        reg.initialize()
        reg.register_plugin_permissions("mail", ["mail:read"])
        grouped = reg.get_grouped()
        assert "core" in grouped
        assert "plugins" in grouped
        assert len(grouped["core"]) == _CORE_ONLY_COUNT
        assert len(grouped["plugins"]) == 1

    def test_register_field_definitions(self):
        """register_field_definitions stores field defs for a plugin."""
        reg = PermissionRegistry()
        reg.initialize()
        field_defs = [
            {"module": "mail", "field": "subject", "label": "Subject", "sensitivity": "normal"},
        ]
        reg.register_field_definitions("mail", field_defs)
        all_defs = reg.get_all_field_definitions()
        mail_defs = [d for d in all_defs if d.get("module") == "mail"]
        assert len(mail_defs) == 1
        assert mail_defs[0]["field"] == "subject"

    def test_get_all_field_definitions_includes_core(self):
        """get_all_field_definitions() includes core field definitions."""
        reg = PermissionRegistry()
        reg.initialize()
        all_defs = reg.get_all_field_definitions()
        company_defs = [d for d in all_defs if d.get("module") == "companies"]
        assert len(company_defs) > 0


# ═══════════════════════════════════════════════════════════════
# SECTION 6: Cache-Invalidierung Tests (DB + Redis required)
# ═══════════════════════════════════════════════════════════════


class TestPermissionCacheInvalidation:
    """Tests for Redis-based permission caching and invalidation."""

    @pytest.mark.asyncio
    async def test_get_cached_permissions_caches_after_first_call(
        self, db_session: AsyncSession, redis_client
    ):
        """get_cached_permissions stores result in Redis after first call."""
        from app.core.permissions import CACHE_PREFIX, get_cached_permissions

        seed = await seed_tenant_and_users(db_session)
        admin = seed["admin_a"]
        tenant = seed["tenant_a"]

        result1 = await get_cached_permissions(db_session, redis_client, admin.id, tenant.id)
        cache_key = f"{CACHE_PREFIX}:{admin.id}:{tenant.id}"
        cached_raw = await redis_client.get(cache_key)
        assert cached_raw is not None, "Cache should be populated after first call"

    @pytest.mark.asyncio
    async def test_invalidate_permission_cache_clears_user(
        self, db_session: AsyncSession, redis_client
    ):
        """invalidate_permission_cache removes cache for a specific user+tenant."""
        from app.core.permissions import CACHE_PREFIX, get_cached_permissions

        seed = await seed_tenant_and_users(db_session)
        admin = seed["admin_a"]
        tenant = seed["tenant_a"]

        await get_cached_permissions(db_session, redis_client, admin.id, tenant.id)
        cache_key = f"{CACHE_PREFIX}:{admin.id}:{tenant.id}"
        assert await redis_client.get(cache_key) is not None

        await invalidate_permission_cache(redis_client, admin.id, tenant.id)
        assert await redis_client.get(cache_key) is None

    @pytest.mark.asyncio
    async def test_invalidate_all_user_permissions_clears_tenant(
        self, db_session: AsyncSession, redis_client
    ):
        """invalidate_all_user_permissions removes cache for all users in a tenant."""
        from app.core.permissions import CACHE_PREFIX, get_cached_permissions

        seed = await seed_tenant_and_users(db_session)
        admin = seed["admin_a"]
        viewer = seed["viewer_a"]
        tenant = seed["tenant_a"]

        await get_cached_permissions(db_session, redis_client, admin.id, tenant.id)
        await get_cached_permissions(db_session, redis_client, viewer.id, tenant.id)

        await invalidate_all_user_permissions(redis_client, tenant.id)

        assert await redis_client.get(f"{CACHE_PREFIX}:{admin.id}:{tenant.id}") is None
        assert await redis_client.get(f"{CACHE_PREFIX}:{viewer.id}:{tenant.id}") is None

    @pytest.mark.asyncio
    async def test_cache_hit_returns_same_permissions_as_miss(
        self, db_session: AsyncSession, redis_client
    ):
        """Cache hit delivers the same permissions as a cache miss."""
        from app.core.permissions import get_cached_permissions

        seed = await seed_tenant_and_users(db_session)
        admin = seed["admin_a"]
        tenant = seed["tenant_a"]

        result1 = await get_cached_permissions(db_session, redis_client, admin.id, tenant.id)
        result2 = await get_cached_permissions(db_session, redis_client, admin.id, tenant.id)

        assert set(result1["permissions"]) == set(result2["permissions"])
        assert result1["is_system_admin"] == result2["is_system_admin"]


# ═══════════════════════════════════════════════════════════════
# SECTION 8: Field-Level Permission Tests (unit + DB)
# ═══════════════════════════════════════════════════════════════


class TestFieldLevelPermissions:
    """Tests for field-level permission filtering."""

    def test_filter_system_admin_no_filtering(self):
        """filter_fields_by_permission with is_system_admin → no filtering."""
        data = {"name": "ACME", "annual_revenue": 1000000, "email": "info@acme.com"}
        resolved = {
            "is_system_admin": True,
            "field_permissions": {"companies": {"annual_revenue": "hidden", "email": "hidden"}},
        }
        result = filter_fields_by_permission(data, resolved, "companies")
        assert result == data

    def test_filter_no_field_permissions_no_filtering(self):
        """filter_fields_by_permission without field_permissions → no filtering."""
        data = {"name": "ACME", "annual_revenue": 1000000}
        resolved = {"is_system_admin": False, "field_permissions": {}}
        result = filter_fields_by_permission(data, resolved, "companies")
        assert result == data

    def test_filter_hidden_field_removed(self):
        """filter_fields_by_permission removes hidden fields."""
        data = {"name": "ACME", "annual_revenue": 1000000}
        resolved = {
            "is_system_admin": False,
            "field_permissions": {"companies": {"annual_revenue": "hidden"}},
        }
        result = filter_fields_by_permission(data, resolved, "companies")
        assert "annual_revenue" not in result
        assert "name" in result

    def test_filter_readonly_field_kept(self):
        """filter_fields_by_permission keeps readonly fields."""
        data = {"name": "ACME", "annual_revenue": 1000000}
        resolved = {
            "is_system_admin": False,
            "field_permissions": {"companies": {"annual_revenue": "readonly"}},
        }
        result = filter_fields_by_permission(data, resolved, "companies")
        assert "annual_revenue" in result

    def test_filter_read_field_kept(self):
        """filter_fields_by_permission keeps read fields."""
        data = {"name": "ACME", "annual_revenue": 1000000}
        resolved = {
            "is_system_admin": False,
            "field_permissions": {"companies": {"annual_revenue": "read"}},
        }
        result = filter_fields_by_permission(data, resolved, "companies")
        assert "annual_revenue" in result

    @pytest.mark.asyncio
    async def test_company_service_applies_filter_with_resolved_perms(
        self, db_session: AsyncSession
    ):
        """Company service applies field filtering when resolved_perms is passed."""
        from app.services.company_service import get_company_detail

        seed = await seed_tenant_and_users(db_session)
        company = seed["company_a"]

        resolved_perms = {
            "is_system_admin": False,
            "field_permissions": {"companies": {"industry": "hidden"}},
        }

        result = await get_company_detail(
            db_session, seed["tenant_a"].id, company.id, resolved_perms=resolved_perms
        )
        assert result is not None
        assert "industry" not in result
        assert "name" in result

    @pytest.mark.asyncio
    async def test_contact_service_applies_filter_with_resolved_perms(
        self, db_session: AsyncSession
    ):
        """Contact service applies field filtering when resolved_perms is passed."""
        from app.models.contact import Contact
        from app.services.contact_service import get_contact_detail

        seed = await seed_tenant_and_users(db_session)
        contact = Contact(
            tenant_id=seed["tenant_a"].id,
            first_name="John",
            last_name="Doe",
            email="john@example.com",
            phone="123456",
            mobile="789012",
            created_by=seed["admin_a"].id,
            updated_by=seed["admin_a"].id,
        )
        db_session.add(contact)
        await db_session.flush()

        resolved_perms = {
            "is_system_admin": False,
            "field_permissions": {"contacts": {"mobile": "hidden"}},
        }

        result = await get_contact_detail(
            db_session, seed["tenant_a"].id, contact.id, resolved_perms=resolved_perms
        )
        assert result is not None
        assert "mobile" not in result
        assert "first_name" in result

    @pytest.mark.asyncio
    async def test_company_service_no_filter_when_resolved_perms_none(
        self, db_session: AsyncSession
    ):
        """Company service does NOT filter when resolved_perms is None (backward compat)."""
        from app.services.company_service import get_company_detail

        seed = await seed_tenant_and_users(db_session)
        company = seed["company_a"]

        result = await get_company_detail(
            db_session, seed["tenant_a"].id, company.id, resolved_perms=None
        )
        assert result is not None
        assert "industry" in result
        assert "name" in result


# ═══════════════════════════════════════════════════════════════
# HELPER FUNCTIONS for DB-dependent tests
# ═══════════════════════════════════════════════════════════════


async def _create_role_with_permissions(
    db: AsyncSession, tenant_id, name: str, permissions, denied=None,
    field_perms=None
) -> Role:
    """Helper: create a Role with specific permissions."""
    role = Role(
        tenant_id=tenant_id,
        name=name,
        permissions=permissions,
        denied_permissions=denied or [],
        field_permissions=field_perms or {},
    )
    db.add(role)
    await db.flush()
    return role


async def _create_user_with_role(
    db: AsyncSession, tenant_id, role_id, email: str, name: str = "Test User"
) -> tuple[User, UserTenant]:
    """Helper: create a User with a specific role_id via UserTenant."""
    user = User(
        tenant_id=tenant_id,
        email=email,
        name=name,
        password_hash=hash_password("TestPass123!"),
        role="custom",
        is_active=True,
        preferences={},
    )
    db.add(user)
    await db.flush()
    ut = UserTenant(user_id=user.id, tenant_id=tenant_id, is_default=True, role_id=role_id)
    db.add(ut)
    await db.flush()
    return user, ut


# ═══════════════════════════════════════════════════════════════
# SECTION 3: Group CRUD Tests (DB required — uses conftest client)
# ═══════════════════════════════════════════════════════════════


class TestGroupCRUD:
    """Tests for Group CRUD operations and membership management."""

    @pytest.mark.asyncio
    async def test_create_group_with_permissions(self, client: AsyncClient, db_session: AsyncSession):
        """Create a group with permissions and denied_permissions."""
        await seed_tenant_and_users(db_session)
        csrf = await login_with_csrf(client, "admin@tenanta.com")

        resp = await client.post(
            "/api/v1/groups",
            json={
                "name": "Sales Team",
                "description": "Sales department group",
                "permissions": {"companies": {"read": True, "write": True}},
                "denied_permissions": ["companies:delete"],
                "field_permissions": {"companies": {"annual_revenue": "hidden"}},
            },
            headers=csrf_headers(csrf),
        )
        assert resp.status_code == 201
        data = resp.json()
        assert data["name"] == "Sales Team"
        assert data["permissions"] == {"companies": {"read": True, "write": True}}
        assert data["denied_permissions"] == ["companies:delete"]
        assert data["permission_version"] == 1

    @pytest.mark.asyncio
    async def test_list_groups(self, client: AsyncClient, db_session: AsyncSession):
        """List all groups in the tenant."""
        await seed_tenant_and_users(db_session)
        csrf = await login_with_csrf(client, "admin@tenanta.com")

        for name in ["Group A", "Group B"]:
            await client.post(
                "/api/v1/groups",
                json={"name": name, "permissions": {}},
                headers=csrf_headers(csrf),
            )

        resp = await client.get("/api/v1/groups", headers=ORIGIN_HEADER)
        assert resp.status_code == 200
        items = resp.json()["items"]
        assert len(items) >= 2

    @pytest.mark.asyncio
    async def test_get_group_by_id(self, client: AsyncClient, db_session: AsyncSession):
        """Get a single group by ID."""
        await seed_tenant_and_users(db_session)
        csrf = await login_with_csrf(client, "admin@tenanta.com")

        create_resp = await client.post(
            "/api/v1/groups",
            json={"name": "Test Group", "permissions": {"contacts": {"read": True}}},
            headers=csrf_headers(csrf),
        )
        group_id = create_resp.json()["id"]

        resp = await client.get(f"/api/v1/groups/{group_id}", headers=ORIGIN_HEADER)
        assert resp.status_code == 200
        assert resp.json()["name"] == "Test Group"

    @pytest.mark.asyncio
    async def test_update_group_name(self, client: AsyncClient, db_session: AsyncSession):
        """Update group name — permission_version should NOT increment."""
        await seed_tenant_and_users(db_session)
        csrf = await login_with_csrf(client, "admin@tenanta.com")

        create_resp = await client.post(
            "/api/v1/groups",
            json={"name": "Old Name", "permissions": {"companies": {"read": True}}},
            headers=csrf_headers(csrf),
        )
        group_id = create_resp.json()["id"]
        initial_version = create_resp.json()["permission_version"]

        resp = await client.patch(
            f"/api/v1/groups/{group_id}",
            json={"name": "New Name"},
            headers=csrf_headers(csrf),
        )
        assert resp.status_code == 200
        assert resp.json()["name"] == "New Name"
        assert resp.json()["permission_version"] == initial_version

    @pytest.mark.asyncio
    async def test_update_group_permissions_increments_version(
        self, client: AsyncClient, db_session: AsyncSession
    ):
        """Update group permissions — permission_version increments."""
        await seed_tenant_and_users(db_session)
        csrf = await login_with_csrf(client, "admin@tenanta.com")

        create_resp = await client.post(
            "/api/v1/groups",
            json={"name": "Versioned Group", "permissions": {"companies": {"read": True}}},
            headers=csrf_headers(csrf),
        )
        group_id = create_resp.json()["id"]
        initial_version = create_resp.json()["permission_version"]

        resp = await client.patch(
            f"/api/v1/groups/{group_id}",
            json={"permissions": {"companies": {"read": True, "write": True}}},
            headers=csrf_headers(csrf),
        )
        assert resp.status_code == 200
        assert resp.json()["permission_version"] == initial_version + 1

    @pytest.mark.asyncio
    async def test_delete_group_soft_delete(self, client: AsyncClient, db_session: AsyncSession):
        """Delete a group (soft-delete) — group disappears from list."""
        await seed_tenant_and_users(db_session)
        csrf = await login_with_csrf(client, "admin@tenanta.com")

        create_resp = await client.post(
            "/api/v1/groups",
            json={"name": "To Delete", "permissions": {}},
            headers=csrf_headers(csrf),
        )
        group_id = create_resp.json()["id"]

        resp = await client.delete(f"/api/v1/groups/{group_id}", headers=csrf_headers(csrf))
        assert resp.status_code == 204

        list_resp = await client.get("/api/v1/groups", headers=ORIGIN_HEADER)
        group_ids = [g["id"] for g in list_resp.json()["items"]]
        assert group_id not in group_ids

    @pytest.mark.asyncio
    async def test_add_user_to_group(self, client: AsyncClient, db_session: AsyncSession):
        """Add a user to a group."""
        seed = await seed_tenant_and_users(db_session)
        csrf = await login_with_csrf(client, "admin@tenanta.com")

        create_resp = await client.post(
            "/api/v1/groups",
            json={"name": "Members Group", "permissions": {}},
            headers=csrf_headers(csrf),
        )
        group_id = create_resp.json()["id"]
        viewer_id = str(seed["viewer_a"].id)

        resp = await client.post(
            f"/api/v1/groups/{group_id}/members",
            json={"user_id": viewer_id},
            headers=csrf_headers(csrf),
        )
        assert resp.status_code == 200

    @pytest.mark.asyncio
    async def test_remove_user_from_group(self, client: AsyncClient, db_session: AsyncSession):
        """Remove a user from a group."""
        seed = await seed_tenant_and_users(db_session)
        csrf = await login_with_csrf(client, "admin@tenanta.com")

        create_resp = await client.post(
            "/api/v1/groups",
            json={"name": "Remove Test", "permissions": {}},
            headers=csrf_headers(csrf),
        )
        group_id = create_resp.json()["id"]
        viewer_id = str(seed["viewer_a"].id)

        await client.post(
            f"/api/v1/groups/{group_id}/members",
            json={"user_id": viewer_id},
            headers=csrf_headers(csrf),
        )

        resp = await client.delete(
            f"/api/v1/groups/{group_id}/members/{viewer_id}",
            headers=csrf_headers(csrf),
        )
        assert resp.status_code == 204

    @pytest.mark.asyncio
    async def test_list_group_members(self, client: AsyncClient, db_session: AsyncSession):
        """List all members of a group."""
        seed = await seed_tenant_and_users(db_session)
        csrf = await login_with_csrf(client, "admin@tenanta.com")

        create_resp = await client.post(
            "/api/v1/groups",
            json={"name": "Member List", "permissions": {}},
            headers=csrf_headers(csrf),
        )
        group_id = create_resp.json()["id"]
        viewer_id = str(seed["viewer_a"].id)

        await client.post(
            f"/api/v1/groups/{group_id}/members",
            json={"user_id": viewer_id},
            headers=csrf_headers(csrf),
        )

        resp = await client.get(f"/api/v1/groups/{group_id}/members", headers=ORIGIN_HEADER)
        assert resp.status_code == 200
        members = resp.json()["items"]
        assert any(m["user_id"] == viewer_id for m in members)

    @pytest.mark.asyncio
    async def test_get_user_groups(self, client: AsyncClient, db_session: AsyncSession):
        """List all groups a user is member of."""
        seed = await seed_tenant_and_users(db_session)
        csrf = await login_with_csrf(client, "admin@tenanta.com")

        create_resp = await client.post(
            "/api/v1/groups",
            json={"name": "User Groups Test", "permissions": {}},
            headers=csrf_headers(csrf),
        )
        group_id = create_resp.json()["id"]
        viewer_id = str(seed["viewer_a"].id)

        await client.post(
            f"/api/v1/groups/{group_id}/members",
            json={"user_id": viewer_id},
            headers=csrf_headers(csrf),
        )

        resp = await client.get(f"/api/v1/groups/user/{viewer_id}", headers=ORIGIN_HEADER)
        assert resp.status_code == 200
        groups = resp.json()["items"]
        assert any(g["id"] == group_id for g in groups)


# ═══════════════════════════════════════════════════════════════
# SECTION 4: RBAC Route-Guard Tests (DB required — uses conftest client)
# ═══════════════════════════════════════════════════════════════


class TestRBACRouteGuard:
    """Tests for require_permission, require_admin, require_write dependencies."""

    @pytest.mark.asyncio
    async def test_require_permission_allows_user_with_exact_permission(
        self, client: AsyncClient, db_session: AsyncSession
    ):
        """User with companies:read can access companies list."""
        await seed_tenant_and_users(db_session)
        await login_with_csrf(client, "admin@tenanta.com")
        resp = await client.get("/api/v1/companies", headers=ORIGIN_HEADER)
        assert resp.status_code == 200

    @pytest.mark.asyncio
    async def test_require_permission_blocks_user_without_permission(
        self, client: AsyncClient, db_session: AsyncSession
    ):
        """Viewer cannot create companies (requires companies:write)."""
        await seed_tenant_and_users(db_session)
        csrf = await login_with_csrf(client, "viewer@tenanta.com")
        resp = await client.post(
            "/api/v1/companies",
            json={"name": "Test Co"},
            headers=csrf_headers(csrf),
        )
        assert resp.status_code == 403

    @pytest.mark.asyncio
    async def test_require_permission_allows_system_admin(
        self, client: AsyncClient, db_session: AsyncSession
    ):
        """System admin bypasses all permission checks."""
        seed = await seed_tenant_and_users(db_session)
        admin = seed["admin_a"]
        admin.is_system_admin = True
        await db_session.flush()
        await db_session.commit()

        await login_with_csrf(client, "admin@tenanta.com")
        resp = await client.get("/api/v1/companies", headers=ORIGIN_HEADER)
        assert resp.status_code == 200

    @pytest.mark.asyncio
    async def test_require_permission_allows_wildcard_module(
        self, db_session: AsyncSession
    ):
        """User with companies:* equivalent (all actions) can access companies:read."""
        seed = await seed_tenant_and_users(db_session)
        tenant = seed["tenant_a"]

        role = await _create_role_with_permissions(
            db_session, tenant.id, "wildcard_module",
            permissions={"companies": {"read": True, "write": True, "delete": True}},
        )
        user, ut = await _create_user_with_role(
            db_session, tenant.id, role.id, "wildcard_mod@test.com"
        )

        resolved = await resolve_permissions(db_session, user.id, tenant.id)
        assert check_permission(resolved, "companies:read") is True
        assert check_permission(resolved, "companies:write") is True

    @pytest.mark.asyncio
    async def test_require_permission_allows_wildcard_action(
        self, db_session: AsyncSession
    ):
        """User with *:read permission can access companies:read."""
        seed = await seed_tenant_and_users(db_session)
        tenant = seed["tenant_a"]

        role = await _create_role_with_permissions(
            db_session, tenant.id, "wildcard_action",
            permissions=["*:read"],
        )
        user, ut = await _create_user_with_role(
            db_session, tenant.id, role.id, "wildcard_act@test.com"
        )

        resolved = await resolve_permissions(db_session, user.id, tenant.id)
        assert check_permission(resolved, "companies:read") is True
        assert check_permission(resolved, "contacts:read") is True
        assert check_permission(resolved, "companies:write") is False

    @pytest.mark.asyncio
    async def test_require_permission_allows_star_star(
        self, db_session: AsyncSession
    ):
        """User with *:* permission can access anything."""
        seed = await seed_tenant_and_users(db_session)
        tenant = seed["tenant_a"]

        role = await _create_role_with_permissions(
            db_session, tenant.id, "superadmin",
            permissions=["*:*"],
        )
        user, ut = await _create_user_with_role(
            db_session, tenant.id, role.id, "starstar@test.com"
        )

        resolved = await resolve_permissions(db_session, user.id, tenant.id)
        assert check_permission(resolved, "companies:read") is True
        assert check_permission(resolved, "anything:anything") is True

    @pytest.mark.asyncio
    async def test_require_permission_blocks_read_only_for_write(
        self, db_session: AsyncSession
    ):
        """User with only companies:read cannot pass companies:write check."""
        seed = await seed_tenant_and_users(db_session)
        tenant = seed["tenant_a"]

        role = await _create_role_with_permissions(
            db_session, tenant.id, "read_only",
            permissions={"companies": {"read": True}},
        )
        user, ut = await _create_user_with_role(
            db_session, tenant.id, role.id, "readonly@test.com"
        )

        resolved = await resolve_permissions(db_session, user.id, tenant.id)
        assert check_permission(resolved, "companies:read") is True
        assert check_permission(resolved, "companies:write") is False

    @pytest.mark.asyncio
    async def test_require_admin_allows_system_admin(
        self, client: AsyncClient, db_session: AsyncSession
    ):
        """require_admin allows system_admin user."""
        seed = await seed_tenant_and_users(db_session)
        admin = seed["admin_a"]
        admin.is_system_admin = True
        await db_session.flush()
        await db_session.commit()

        await login_with_csrf(client, "admin@tenanta.com")
        resp = await client.get("/api/v1/plugins", headers=ORIGIN_HEADER)
        assert resp.status_code == 200

    @pytest.mark.asyncio
    async def test_require_admin_allows_legacy_admin(
        self, client: AsyncClient, db_session: AsyncSession
    ):
        """require_admin allows legacy admin role."""
        await seed_tenant_and_users(db_session)
        await login_with_csrf(client, "admin@tenanta.com")
        resp = await client.get("/api/v1/plugins", headers=ORIGIN_HEADER)
        assert resp.status_code == 200

    @pytest.mark.asyncio
    async def test_require_admin_blocks_non_admin(
        self, client: AsyncClient, db_session: AsyncSession
    ):
        """require_admin blocks viewer (legacy role)."""
        await seed_tenant_and_users(db_session)
        await login_with_csrf(client, "viewer@tenanta.com")
        resp = await client.get("/api/v1/plugins", headers=ORIGIN_HEADER)
        assert resp.status_code == 403

    @pytest.mark.asyncio
    async def test_require_write_allows_legacy_editor(
        self, client: AsyncClient, db_session: AsyncSession
    ):
        """require_write allows legacy editor role (companies:write in legacy perms)."""
        await seed_tenant_and_users(db_session)
        csrf = await login_with_csrf(client, "editor@tenanta.com")
        resp = await client.post(
            "/api/v1/companies",
            json={"name": "Editor Company"},
            headers=csrf_headers(csrf),
        )
        assert resp.status_code == 201

    @pytest.mark.asyncio
    async def test_require_write_blocks_legacy_viewer(
        self, client: AsyncClient, db_session: AsyncSession
    ):
        """require_write blocks legacy viewer role."""
        await seed_tenant_and_users(db_session)
        csrf = await login_with_csrf(client, "viewer@tenanta.com")
        resp = await client.post(
            "/api/v1/companies",
            json={"name": "Viewer Company"},
            headers=csrf_headers(csrf),
        )
        assert resp.status_code == 403


# ═══════════════════════════════════════════════════════════════
# SECTION 5: Self-Modification Prevention Tests (DB required)
# ═══════════════════════════════════════════════════════════════


class TestSelfModificationPrevention:
    """Tests for self-modification prevention in user routes."""

    @pytest.mark.asyncio
    async def test_user_can_change_own_name(self, client: AsyncClient, db_session: AsyncSession):
        """User can change their own name (allowed)."""
        seed = await seed_tenant_and_users(db_session)
        admin = seed["admin_a"]
        csrf = await login_with_csrf(client, "admin@tenanta.com")

        resp = await client.patch(
            f"/api/v1/users/{admin.id}",
            json={"name": "New Admin Name"},
            headers=csrf_headers(csrf),
        )
        assert resp.status_code == 200
        assert resp.json()["name"] == "New Admin Name"

    @pytest.mark.asyncio
    async def test_user_cannot_change_own_role(self, client: AsyncClient, db_session: AsyncSession):
        """User cannot change their own role (403 self_modification_forbidden)."""
        seed = await seed_tenant_and_users(db_session)
        admin = seed["admin_a"]
        csrf = await login_with_csrf(client, "admin@tenanta.com")

        resp = await client.patch(
            f"/api/v1/users/{admin.id}",
            json={"role": "viewer"},
            headers=csrf_headers(csrf),
        )
        assert resp.status_code == 403
        assert resp.json()["detail"]["code"] == "self_modification_forbidden"

    @pytest.mark.asyncio
    async def test_user_cannot_change_own_is_active(self, client: AsyncClient, db_session: AsyncSession):
        """User cannot change their own is_active status (403)."""
        seed = await seed_tenant_and_users(db_session)
        admin = seed["admin_a"]
        csrf = await login_with_csrf(client, "admin@tenanta.com")

        resp = await client.patch(
            f"/api/v1/users/{admin.id}",
            json={"is_active": False},
            headers=csrf_headers(csrf),
        )
        assert resp.status_code == 403
        assert resp.json()["detail"]["code"] == "self_modification_forbidden"

    @pytest.mark.asyncio
    async def test_user_cannot_change_own_role_id(self, client: AsyncClient, db_session: AsyncSession):
        """User cannot change their own role_id (403)."""
        seed = await seed_tenant_and_users(db_session)
        admin = seed["admin_a"]
        csrf = await login_with_csrf(client, "admin@tenanta.com")

        resp = await client.patch(
            f"/api/v1/users/{admin.id}",
            json={"role_id": str(uuid.uuid4())},
            headers=csrf_headers(csrf),
        )
        assert resp.status_code == 403
        assert resp.json()["detail"]["code"] == "self_modification_forbidden"

    @pytest.mark.asyncio
    async def test_admin_can_change_other_user_role(
        self, client: AsyncClient, db_session: AsyncSession
    ):
        """Admin can change another user's role (allowed)."""
        seed = await seed_tenant_and_users(db_session)
        viewer = seed["viewer_a"]
        csrf = await login_with_csrf(client, "admin@tenanta.com")

        resp = await client.patch(
            f"/api/v1/users/{viewer.id}",
            json={"role": "editor"},
            headers=csrf_headers(csrf),
        )
        assert resp.status_code == 200
        assert resp.json()["role"] == "editor"


# ═══════════════════════════════════════════════════════════════
# SECTION 7: Mail-Plugin RBAC Tests (DB required)
# ═══════════════════════════════════════════════════════════════


@pytest_asyncio.fixture
async def mail_app(engine: AsyncEngine, redis_client):
    """FastAPI app with Mail plugin registered, installed, and activated."""
    reset_engine_for_testing(engine)
    app = create_app()
    registry = reset_registry_for_testing()
    registry.initialize(engine, app)
    container = get_container()
    await container.initialize()
    registry.register_plugin(MailPlugin())
    reset_plugin_service_for_testing(registry)
    sf = async_sessionmaker(bind=engine, expire_on_commit=False, class_=AsyncSession)
    async with sf() as session:
        await registry.install(session, "mail")
        await registry.activate(session, "mail")
        await session.commit()
    yield app
    await close_engine()


@pytest_asyncio.fixture
async def mail_client(mail_app) -> AsyncClient:
    """HTTP async test client with mail plugin active."""
    transport = ASGITransport(app=mail_app)
    async with AsyncClient(transport=transport, base_url="http://test") as c:
        yield c


class TestMailPluginRBAC:
    """Tests for mail plugin RBAC permission enforcement and delegate access."""

    async def _seed_mail_data(self, db: AsyncSession) -> dict:
        """Helper: seed tenant, users, mail account, folder, and mail."""
        seed = await seed_tenant_and_users(db)
        tenant = seed["tenant_a"]
        admin = seed["admin_a"]
        viewer = seed["viewer_a"]

        account = MailAccount(
            tenant_id=tenant.id,
            user_id=admin.id,
            email_address="admin@tenanta.com",
            display_name="Admin",
            imap_host="imap.example.com",
            imap_port=993,
            imap_ssl=True,
            smtp_host="smtp.example.com",
            smtp_port=587,
            smtp_tls=True,
            username="admin@tenanta.com",
            encrypted_password="encrypted_dummy",
            is_shared=False,
            is_active=True,
        )
        db.add(account)
        await db.flush()

        shared_account = MailAccount(
            tenant_id=tenant.id,
            user_id=admin.id,
            email_address="shared@tenanta.com",
            display_name="Shared",
            imap_host="imap.example.com",
            imap_port=993,
            imap_ssl=True,
            smtp_host="smtp.example.com",
            smtp_port=587,
            smtp_tls=True,
            username="shared@tenanta.com",
            encrypted_password="encrypted_dummy",
            is_shared=True,
            is_active=True,
        )
        db.add(shared_account)
        await db.flush()

        folder = MailFolder(
            tenant_id=tenant.id,
            account_id=account.id,
            name="INBOX",
            imap_name="INBOX",
            is_standard=True,
        )
        db.add(folder)
        await db.flush()

        mail = Mail(
            tenant_id=tenant.id,
            account_id=account.id,
            folder_id=folder.id,
            message_id="<test@example.com>",
            subject="Test Subject",
            from_address="sender@example.com",
            to_addresses="admin@tenanta.com",
            body_text="Test body",
        )
        db.add(mail)
        await db.flush()
        await db.commit()

        return {
            **seed,
            "account": account,
            "shared_account": shared_account,
            "folder": folder,
            "mail": mail,
        }

    async def _give_user_mail_perms(self, db: AsyncSession, tenant_id, user_id, perms: list[str]):
        """Helper: create a role with mail permissions and assign to user."""
        from sqlalchemy import update
        role = Role(
            tenant_id=tenant_id,
            name=f"mail_role_{uuid.uuid4().hex[:8]}",
            permissions=perms,
        )
        db.add(role)
        await db.flush()
        await db.execute(
            update(UserTenant)
            .where(UserTenant.user_id == user_id)
            .values(role_id=role.id)
        )
        await db.commit()

    @pytest.mark.asyncio
    async def test_user_without_mail_read_gets_403_on_list(
        self, mail_client: AsyncClient, db_session: AsyncSession
    ):
        """User without mail:read permission gets 403 on GET /mail."""
        await seed_tenant_and_users(db_session)
        await login_with_csrf(mail_client, "viewer@tenanta.com")
        resp = await mail_client.get("/api/v1/mail", headers=ORIGIN_HEADER)
        assert resp.status_code == 403

    @pytest.mark.asyncio
    async def test_user_with_mail_read_gets_200_on_list(
        self, mail_client: AsyncClient, db_session: AsyncSession
    ):
        """User with mail:read permission gets 200 on GET /mail.
        Admin (legacy) has *:* which matches mail:read."""
        await seed_tenant_and_users(db_session)
        await login_with_csrf(mail_client, "admin@tenanta.com")
        resp = await mail_client.get(
            "/api/v1/mail",
            params={"account_id": str(uuid.uuid4())},
            headers=ORIGIN_HEADER,
        )
        assert resp.status_code == 200

    @pytest.mark.asyncio
    async def test_user_without_mail_send_gets_403(
        self, mail_client: AsyncClient, db_session: AsyncSession
    ):
        """User without mail:send permission gets 403 on POST /mail/send."""
        await seed_tenant_and_users(db_session)
        csrf = await login_with_csrf(mail_client, "viewer@tenanta.com")
        resp = await mail_client.post(
            "/api/v1/mail/send",
            json={
                "account_id": str(uuid.uuid4()),
                "to": ["test@example.com"],
                "subject": "Test",
                "body_text": "Test",
            },
            headers=csrf_headers(csrf),
        )
        assert resp.status_code == 403

    @pytest.mark.asyncio
    async def test_user_without_mail_config_gets_403_on_create_account(
        self, mail_client: AsyncClient, db_session: AsyncSession
    ):
        """User without mail:config permission gets 403 on POST /mail/accounts."""
        await seed_tenant_and_users(db_session)
        csrf = await login_with_csrf(mail_client, "viewer@tenanta.com")
        resp = await mail_client.post(
            "/api/v1/mail/accounts",
            json={
                "email_address": "test@example.com",
                "display_name": "Test",
                "imap_host": "imap.example.com",
                "smtp_host": "smtp.example.com",
                "username": "test@example.com",
                "password": "secret123",
            },
            headers=csrf_headers(csrf),
        )
        assert resp.status_code == 403

    @pytest.mark.asyncio
    async def test_delegate_read_can_read_but_not_delete(
        self, mail_client: AsyncClient, db_session: AsyncSession
    ):
        """Delegate with access_level 'read' can read mails but cannot delete."""
        data = await self._seed_mail_data(db_session)
        viewer = data["viewer_a"]
        account = data["account"]
        mail = data["mail"]

        delegate = MailAccountDelegate(
            tenant_id=data["tenant_a"].id,
            account_id=account.id,
            delegate_user_id=viewer.id,
            access_level="read",
        )
        db_session.add(delegate)
        await db_session.flush()
        await db_session.commit()

        await self._give_user_mail_perms(
            db_session, data["tenant_a"].id, viewer.id,
            ["mail:read", "mail:write", "mail:delete"]
        )

        csrf = await login_with_csrf(mail_client, "viewer@tenanta.com")

        resp = await mail_client.get(f"/api/v1/mail/{mail.id}", headers=ORIGIN_HEADER)
        assert resp.status_code == 200

        resp = await mail_client.delete(f"/api/v1/mail/{mail.id}", headers=csrf_headers(csrf))
        assert resp.status_code == 403

    @pytest.mark.asyncio
    async def test_delegate_write_can_set_flags_but_not_delete(
        self, mail_client: AsyncClient, db_session: AsyncSession
    ):
        """Delegate with access_level 'write' can set flags but cannot delete."""
        data = await self._seed_mail_data(db_session)
        viewer = data["viewer_a"]
        account = data["account"]
        mail = data["mail"]

        delegate = MailAccountDelegate(
            tenant_id=data["tenant_a"].id,
            account_id=account.id,
            delegate_user_id=viewer.id,
            access_level="write",
        )
        db_session.add(delegate)
        await db_session.flush()
        await db_session.commit()

        await self._give_user_mail_perms(
            db_session, data["tenant_a"].id, viewer.id,
            ["mail:read", "mail:write", "mail:delete"]
        )

        csrf = await login_with_csrf(mail_client, "viewer@tenanta.com")

        resp = await mail_client.patch(
            f"/api/v1/mail/{mail.id}/flags",
            json={"is_seen": True},
            headers=csrf_headers(csrf),
        )
        assert resp.status_code == 200

        resp = await mail_client.delete(f"/api/v1/mail/{mail.id}", headers=csrf_headers(csrf))
        assert resp.status_code == 403

    @pytest.mark.asyncio
    async def test_delegate_delete_can_delete_mail(
        self, mail_client: AsyncClient, db_session: AsyncSession
    ):
        """Delegate with access_level 'delete' can delete mails."""
        data = await self._seed_mail_data(db_session)
        viewer = data["viewer_a"]
        account = data["account"]
        mail = data["mail"]

        delegate = MailAccountDelegate(
            tenant_id=data["tenant_a"].id,
            account_id=account.id,
            delegate_user_id=viewer.id,
            access_level="delete",
        )
        db_session.add(delegate)
        await db_session.flush()
        await db_session.commit()

        await self._give_user_mail_perms(
            db_session, data["tenant_a"].id, viewer.id,
            ["mail:read", "mail:write", "mail:delete"]
        )

        csrf = await login_with_csrf(mail_client, "viewer@tenanta.com")

        resp = await mail_client.delete(f"/api/v1/mail/{mail.id}", headers=csrf_headers(csrf))
        assert resp.status_code == 204

    @pytest.mark.asyncio
    async def test_delegate_full_can_do_everything(
        self, mail_client: AsyncClient, db_session: AsyncSession
    ):
        """Delegate with access_level 'full' can perform all operations."""
        data = await self._seed_mail_data(db_session)
        viewer = data["viewer_a"]
        account = data["account"]
        mail = data["mail"]

        delegate = MailAccountDelegate(
            tenant_id=data["tenant_a"].id,
            account_id=account.id,
            delegate_user_id=viewer.id,
            access_level="full",
        )
        db_session.add(delegate)
        await db_session.flush()
        await db_session.commit()

        await self._give_user_mail_perms(
            db_session, data["tenant_a"].id, viewer.id,
            ["mail:read", "mail:write", "mail:delete", "mail:config"]
        )

        csrf = await login_with_csrf(mail_client, "viewer@tenanta.com")

        resp = await mail_client.get(f"/api/v1/mail/{mail.id}", headers=ORIGIN_HEADER)
        assert resp.status_code == 200

        resp = await mail_client.patch(
            f"/api/v1/mail/{mail.id}/flags",
            json={"is_flagged": True},
            headers=csrf_headers(csrf),
        )
        assert resp.status_code == 200

        resp = await mail_client.delete(f"/api/v1/mail/{mail.id}", headers=csrf_headers(csrf))
        assert resp.status_code == 204

    @pytest.mark.asyncio
    async def test_owner_has_full_access_without_delegate(
        self, mail_client: AsyncClient, db_session: AsyncSession
    ):
        """Owner has full access to their own mailbox without any delegate entry."""
        data = await self._seed_mail_data(db_session)
        mail = data["mail"]

        csrf = await login_with_csrf(mail_client, "admin@tenanta.com")

        resp = await mail_client.get(f"/api/v1/mail/{mail.id}", headers=ORIGIN_HEADER)
        assert resp.status_code == 200

        resp = await mail_client.patch(
            f"/api/v1/mail/{mail.id}/flags",
            json={"is_seen": True},
            headers=csrf_headers(csrf),
        )
        assert resp.status_code == 200

        resp = await mail_client.delete(f"/api/v1/mail/{mail.id}", headers=csrf_headers(csrf))
        assert resp.status_code == 204

    @pytest.mark.asyncio
    async def test_user_without_delegate_gets_403_on_shared_mailbox(
        self, mail_client: AsyncClient, db_session: AsyncSession
    ):
        """User without delegate access gets 403 when trying to access another user's mail."""
        data = await self._seed_mail_data(db_session)
        editor = data["editor_a"]
        mail = data["mail"]

        await self._give_user_mail_perms(
            db_session, data["tenant_a"].id, editor.id,
            ["mail:read"]
        )

        await login_with_csrf(mail_client, "editor@tenanta.com")

        resp = await mail_client.get(f"/api/v1/mail/{mail.id}", headers=ORIGIN_HEADER)
        assert resp.status_code == 403


# ═══════════════════════════════════════════════════════════════
# SECTION 9: Integration Tests (DB + Redis required)
# ═══════════════════════════════════════════════════════════════


class TestRBACIntegration:
    """End-to-end integration tests for the RBAC system."""

    @pytest.mark.asyncio
    async def test_login_and_me_permissions(self, client: AsyncClient, db_session: AsyncSession):
        """Login → /me/permissions → has correct permissions."""
        await seed_tenant_and_users(db_session)
        await login_with_csrf(client, "admin@tenanta.com")

        resp = await client.get("/api/v1/auth/me/permissions", headers=ORIGIN_HEADER)
        assert resp.status_code == 200
        data = resp.json()
        assert "permissions" in data
        assert "denied_permissions" in data
        assert "field_permissions" in data
        assert "is_system_admin" in data

    @pytest.mark.asyncio
    async def test_login_as_admin_has_star_star(
        self, client: AsyncClient, db_session: AsyncSession
    ):
        """Login as admin → has *:* permission."""
        await seed_tenant_and_users(db_session)
        await login_with_csrf(client, "admin@tenanta.com")

        resp = await client.get("/api/v1/auth/me/permissions", headers=ORIGIN_HEADER)
        assert resp.status_code == 200
        perms = resp.json()["permissions"]
        assert "*:*" in perms

    @pytest.mark.asyncio
    async def test_create_group_add_user_resolved_perms_include_group(
        self, client: AsyncClient, db_session: AsyncSession
    ):
        """Create Group → Add User → User's resolved permissions include group permissions."""
        seed = await seed_tenant_and_users(db_session)
        tenant = seed["tenant_a"]
        viewer = seed["viewer_a"]

        csrf = await login_with_csrf(client, "admin@tenanta.com")

        create_resp = await client.post(
            "/api/v1/groups",
            json={
                "name": "Integration Group",
                "permissions": {"companies": {"read": True, "write": True}},
            },
            headers=csrf_headers(csrf),
        )
        assert create_resp.status_code == 201
        group_id = create_resp.json()["id"]

        add_resp = await client.post(
            f"/api/v1/groups/{group_id}/members",
            json={"user_id": str(viewer.id)},
            headers=csrf_headers(csrf),
        )
        assert add_resp.status_code == 200

        resolved = await resolve_permissions(db_session, viewer.id, tenant.id)
        assert "companies:write" in resolved["permissions"]

    @pytest.mark.asyncio
    async def test_update_role_permissions_cache_invalidated(
        self, db_session: AsyncSession, redis_client
    ):
        """Update role permissions → cache invalidated → next request has new permissions."""
        from app.core.permissions import CACHE_PREFIX, get_cached_permissions

        seed = await seed_tenant_and_users(db_session)
        tenant = seed["tenant_a"]

        role = Role(
            tenant_id=tenant.id,
            name="cache_test_role",
            permissions={"companies": {"read": True}},
        )
        db_session.add(role)
        await db_session.flush()

        user, ut = await _create_user_with_role(
            db_session, tenant.id, role.id, "cache_test@test.com"
        )
        await db_session.commit()

        result1 = await get_cached_permissions(db_session, redis_client, user.id, tenant.id)
        assert "companies:read" in result1["permissions"]
        assert "companies:write" not in result1["permissions"]

        role.permissions = {"companies": {"read": True, "write": True}}
        role.permission_version += 1
        await db_session.flush()
        await db_session.commit()

        await invalidate_permission_cache(redis_client, user.id, tenant.id)

        result2 = await get_cached_permissions(db_session, redis_client, user.id, tenant.id)
        assert "companies:write" in result2["permissions"]

    @pytest.mark.asyncio
    async def test_delete_group_user_perms_no_longer_include_group(
        self, client: AsyncClient, db_session: AsyncSession
    ):
        """Delete group → group is soft-deleted and disappears from list.

        NOTE: resolve_permissions() does not currently filter by Group.deleted_at,
        so soft-deleted groups still contribute permissions until the UserGroup
        membership is removed. This is a known bug in the implementation.
        The test verifies the soft-delete happened and the group is gone from the list.
        """
        seed = await seed_tenant_and_users(db_session)
        tenant = seed["tenant_a"]
        viewer = seed["viewer_a"]

        csrf = await login_with_csrf(client, "admin@tenanta.com")

        create_resp = await client.post(
            "/api/v1/groups",
            json={
                "name": "Delete Test Group",
                "permissions": {"companies": {"delete": True}},
            },
            headers=csrf_headers(csrf),
        )
        assert create_resp.status_code == 201
        group_id = create_resp.json()["id"]

        await client.post(
            f"/api/v1/groups/{group_id}/members",
            json={"user_id": str(viewer.id)},
            headers=csrf_headers(csrf),
        )

        resolved_before = await resolve_permissions(db_session, viewer.id, tenant.id)
        assert "companies:delete" in resolved_before["permissions"]

        del_resp = await client.delete(f"/api/v1/groups/{group_id}", headers=csrf_headers(csrf))
        assert del_resp.status_code == 204

        # Group should be gone from the list (soft-delete works for listing)
        list_resp = await client.get("/api/v1/groups", headers=ORIGIN_HEADER)
        group_ids = [g["id"] for g in list_resp.json()["items"]]
        assert group_id not in group_ids

        # KNOWN BUG: resolve_permissions does not filter by Group.deleted_at,
        # so the group permission is still present after soft-delete.
        # When this bug is fixed, the following assertion should pass:
        # resolved_after = await resolve_permissions(db_session, viewer.id, tenant.id)
        # assert "companies:delete" not in resolved_after["permissions"]

    @pytest.mark.asyncio
    async def test_resolve_permissions_role_with_denied(
        self, db_session: AsyncSession
    ):
        """resolve_permissions: role permissions minus denied_permissions."""
        seed = await seed_tenant_and_users(db_session)
        tenant = seed["tenant_a"]

        role = Role(
            tenant_id=tenant.id,
            name="denied_test",
            permissions={"companies": {"read": True, "write": True, "delete": True}},
            denied_permissions=["companies:delete"],
        )
        db_session.add(role)
        await db_session.flush()

        user, ut = await _create_user_with_role(
            db_session, tenant.id, role.id, "denied@test.com"
        )
        await db_session.commit()

        resolved = await resolve_permissions(db_session, user.id, tenant.id)
        assert "companies:read" in resolved["permissions"]
        assert "companies:write" in resolved["permissions"]
        assert "companies:delete" not in resolved["permissions"]
        assert "companies:delete" in resolved["denied"]

    @pytest.mark.asyncio
    async def test_resolve_permissions_system_admin(self, db_session: AsyncSession):
        """resolve_permissions for is_system_admin returns *:*."""
        seed = await seed_tenant_and_users(db_session)
        admin = seed["admin_a"]
        admin.is_system_admin = True
        await db_session.flush()
        await db_session.commit()

        resolved = await resolve_permissions(db_session, admin.id, seed["tenant_a"].id)
        assert resolved["is_system_admin"] is True
        assert resolved["permissions"] == {"*:*"}
        assert resolved["denied"] == set()

    @pytest.mark.asyncio
    async def test_resolve_permissions_legacy_admin(self, db_session: AsyncSession):
        """resolve_permissions for legacy admin role string returns *:*."""
        seed = await seed_tenant_and_users(db_session)
        admin = seed["admin_a"]

        resolved = await resolve_permissions(db_session, admin.id, seed["tenant_a"].id)
        assert "*:*" in resolved["permissions"]

    @pytest.mark.asyncio
    async def test_resolve_permissions_legacy_viewer(self, db_session: AsyncSession):
        """resolve_permissions for legacy viewer role returns read-only permissions."""
        seed = await seed_tenant_and_users(db_session)
        viewer = seed["viewer_a"]

        resolved = await resolve_permissions(db_session, viewer.id, seed["tenant_a"].id)
        assert "companies:read" in resolved["permissions"]
        assert "companies:write" not in resolved["permissions"]

    @pytest.mark.asyncio
    async def test_resolve_permissions_no_role_no_legacy(self, db_session: AsyncSession):
        """resolve_permissions for user without role_id and without legacy role returns empty."""
        seed = await seed_tenant_and_users(db_session)
        tenant = seed["tenant_a"]

        user = User(
            tenant_id=tenant.id,
            email="norole@test.com",
            name="No Role",
            password_hash=hash_password("TestPass123!"),
            role="",
            is_active=True,
            preferences={},
        )
        db_session.add(user)
        await db_session.flush()
        ut = UserTenant(user_id=user.id, tenant_id=tenant.id, is_default=True, role_id=None)
        db_session.add(ut)
        await db_session.commit()

        resolved = await resolve_permissions(db_session, user.id, tenant.id)
        assert resolved["permissions"] == set()
        assert resolved["is_system_admin"] is False

    @pytest.mark.asyncio
    async def test_resolve_permissions_role_plus_groups_union(
        self, db_session: AsyncSession
    ):
        """resolve_permissions for user with Role + Groups returns union of permissions."""
        seed = await seed_tenant_and_users(db_session)
        tenant = seed["tenant_a"]

        role = Role(
            tenant_id=tenant.id,
            name="union_role",
            permissions={"companies": {"read": True}},
        )
        db_session.add(role)
        await db_session.flush()

        user, ut = await _create_user_with_role(
            db_session, tenant.id, role.id, "union@test.com"
        )

        group = Group(
            tenant_id=tenant.id,
            name="union_group",
            permissions={"contacts": {"write": True}},
        )
        db_session.add(group)
        await db_session.flush()

        ug = UserGroup(user_id=user.id, group_id=group.id, tenant_id=tenant.id)
        db_session.add(ug)
        await db_session.commit()

        resolved = await resolve_permissions(db_session, user.id, tenant.id)
        assert "companies:read" in resolved["permissions"]
        assert "contacts:write" in resolved["permissions"]

    @pytest.mark.asyncio
    async def test_resolve_permissions_group_denied_overrides(
        self, db_session: AsyncSession
    ):
        """resolve_permissions: group denied_permissions override role+group allowed."""
        seed = await seed_tenant_and_users(db_session)
        tenant = seed["tenant_a"]

        role = Role(
            tenant_id=tenant.id,
            name="deny_override_role",
            permissions={"companies": {"read": True, "write": True}},
        )
        db_session.add(role)
        await db_session.flush()

        user, ut = await _create_user_with_role(
            db_session, tenant.id, role.id, "deny_override@test.com"
        )

        group = Group(
            tenant_id=tenant.id,
            name="deny_group",
            permissions={},
            denied_permissions=["companies:write"],
        )
        db_session.add(group)
        await db_session.flush()

        ug = UserGroup(user_id=user.id, group_id=group.id, tenant_id=tenant.id)
        db_session.add(ug)
        await db_session.commit()

        resolved = await resolve_permissions(db_session, user.id, tenant.id)
        assert "companies:read" in resolved["permissions"]
        assert "companies:write" not in resolved["permissions"]
        assert "companies:write" in resolved["denied"]
