from uuid import uuid4
import pytest
from app_new.extensions import db
from app_new.core.models import User, Role, Permission, RolePermission, UserPermission, UserRole
from app_new.core.authorization import has_permission
from app_new.core.services.rbac import replace_user_roles, ensure_rbac_catalog
from app_new.core.services.rbac import ensure_super_admin_survives, LastSuperAdminError


def test_rbac_catalog_bootstrap_is_complete_and_idempotent(app):
    """Le wizard doit pouvoir amorcer une base RBAC vierge sans doublons."""
    with app.app_context():
        role_map, permission_map = ensure_rbac_catalog()
        db.session.commit()

        expected_roles = {
            'super_admin', 'admin', 'responsable_gmao', 'technicien',
            'assistant_prevention', 'demandeur', 'lecture',
        }
        assert expected_roles.issubset(role_map)
        assert '*' in permission_map
        assert RolePermission.query.filter_by(
            role_id=role_map['super_admin'].id,
            permission_id=permission_map['*'].id,
        ).first() is not None
        assert RolePermission.query.filter_by(
            role_id=role_map['admin'].id,
            permission_id=permission_map['*'].id,
        ).first() is None

        counts_before = {
            'roles': Role.query.filter(Role.slug.in_(expected_roles)).count(),
            'permissions': Permission.query.count(),
            'links': RolePermission.query.count(),
        }
        ensure_rbac_catalog()
        db.session.commit()
        assert Role.query.filter(Role.slug.in_(expected_roles)).count() == counts_before['roles']
        assert Permission.query.count() == counts_before['permissions']
        assert RolePermission.query.count() == counts_before['links']


def _role_with_permissions(slug, permissions):
    role = Role(slug=slug, name=slug, is_system=False, is_active=True)
    db.session.add(role)
    db.session.flush()
    for code in permissions:
        permission = Permission.query.filter_by(code=code).first()
        if permission is None:
            permission = Permission(code=code, name=code, module=code.split('.', 1)[0], action=code.split('.', 1)[1])
            db.session.add(permission)
            db.session.flush()
        db.session.add(RolePermission(role_id=role.id, permission_id=permission.id, effect='allow'))
    return role


def _user():
    user = User(username='multi_' + uuid4().hex[:10], email=uuid4().hex + '@test.local', full_name='Multi', role='demandeur')
    user.set_password('test-password-strong')
    db.session.add(user)
    db.session.flush()
    return user


def test_multiple_roles_union_and_removal(app):
    with app.app_context():
        role_a = _role_with_permissions('role_a_' + uuid4().hex[:8], ['patrimoine.view'])
        role_b = _role_with_permissions('role_b_' + uuid4().hex[:8], ['prevention.view'])
        role_c = _role_with_permissions('role_c_' + uuid4().hex[:8], ['stock.manage'])
        user = _user()
        replace_user_roles(user, [role_a.id, role_b.id, role_c.id])
        db.session.commit()
        assert has_permission('patrimoine.view', user)
        assert has_permission('prevention.view', user)
        assert has_permission('stock.manage', user)
        replace_user_roles(user, [role_b.id, role_c.id])
        db.session.commit()
        assert not has_permission('patrimoine.view', user)
        assert has_permission('prevention.view', user)
        assert has_permission('stock.manage', user)


def test_admin_removed_keeps_other_roles_without_wildcard(app):
    with app.app_context():
        admin = Role.query.filter_by(slug='admin', is_active=True).first()
        tech = Role.query.filter_by(slug='technicien', is_active=True).first()
        prevention = Role.query.filter_by(slug='assistant_prevention', is_active=True).first()
        user = _user()
        replace_user_roles(user, [admin.id, tech.id, prevention.id])
        db.session.commit()
        # admin est désormais un rôle explicite : seul super_admin possède '*'.
        assert not has_permission('*', user)
        assert has_permission('dashboard.view', user)
        replace_user_roles(user, [tech.id, prevention.id])
        db.session.commit()
        assert not has_permission('*', user)
        assert has_permission('patrimoine.view', user)
        assert has_permission('prevention.view', user)


def test_individual_deny_overrides_all_roles_and_allow_adds(app):
    with app.app_context():
        role_a = _role_with_permissions('deny_a_' + uuid4().hex[:8], ['stock.manage'])
        role_b = _role_with_permissions('deny_b_' + uuid4().hex[:8], ['stock.view'])
        user = _user()
        replace_user_roles(user, [role_a.id, role_b.id])
        db.session.commit()
        permission = Permission.query.filter_by(code='stock.manage').first()
        db.session.add(UserPermission(user_id=user.id, permission_id=permission.id, effect='deny'))
        extra = Permission(code='audit.test.view_' + uuid4().hex[:6], name='audit test', module='audit', action='view')
        db.session.add(extra)
        db.session.flush()
        db.session.add(UserPermission(user_id=user.id, permission_id=extra.id, effect='allow'))
        db.session.commit()
        assert not has_permission('stock.manage', user)
        assert has_permission(extra.code, user)


def test_inactive_role_is_ignored_and_no_duplicate_links(app):
    with app.app_context():
        code = 'system.test.view_' + uuid4().hex[:6]
        role = _role_with_permissions('inactive_' + uuid4().hex[:8], [code])
        role.is_active = False
        user = _user()
        replace_user_roles(user, [role.id, role.id])
        replace_user_roles(user, [role.id])
        db.session.commit()
        assert not has_permission(code, user)
        assert UserRole.query.filter_by(user_id=user.id, role_id=role.id).count() == 0


def test_rbac_errors_fail_closed(app, monkeypatch):
    with app.app_context():
        class BrokenUser:
            is_authenticated = True
            id = 999999
            @property
            def role_links(self):
                raise RuntimeError('database unavailable')
            @property
            def permission_links(self):
                return []
        assert not has_permission('dashboard.view', BrokenUser())

def test_custom_role_permission_and_override(app):
    with app.app_context():
        slug = "custom_" + uuid4().hex[:8]
        role = Role(slug=slug, name="Gestionnaire test", is_system=False)
        permission = Permission(code="stock.test.view", name="Stock test", module="stock", action="view")
        db.session.add_all([role, permission]); db.session.flush()
        db.session.add(RolePermission(role_id=role.id, permission_id=permission.id, effect="allow"))
        user = User(username="rbac_" + uuid4().hex[:8], email=uuid4().hex + "@test.local", full_name="RBAC", role=slug)
        user.set_password("test-password-strong")
        db.session.add(user); db.session.flush()
        db.session.add(UserRole(user_id=user.id, role_id=role.id)); db.session.commit()
        assert has_permission("stock.test.view", user)
        assert not has_permission("stock.test.edit", user)
        db.session.add(UserPermission(user_id=user.id, permission_id=permission.id, effect="deny"))
        db.session.commit()
        assert not has_permission("stock.test.view", user)

def test_admin_rbac_page_requires_authentication(client):
    response = client.get("/admin/roles")
    assert response.status_code == 302
    assert "/auth/login" in response.location


def test_super_admin_wildcard_and_deny_cannot_reduce_access(app):
    with app.app_context():
        super_role = Role.query.filter_by(slug='super_admin', is_active=True).first()
        user = _user()
        replace_user_roles(user, [super_role.id])
        db.session.commit()
        assert has_permission('*', user)
        assert has_permission('stock.destroy', user)
        permission = Permission.query.filter_by(code='stock.destroy').first()
        db.session.add(UserPermission(user_id=user.id, permission_id=permission.id, effect='deny'))
        db.session.commit()
        assert has_permission('stock.destroy', user)


def test_admin_is_explicit_and_loses_removed_permission(app):
    with app.app_context():
        admin = Role.query.filter_by(slug='admin', is_active=True).first()
        user = _user()
        replace_user_roles(user, [admin.id])
        db.session.commit()
        assert not has_permission('*', user)
        assert has_permission('stock.view', user)
        stock = Permission.query.filter_by(code='stock.view').first()
        link = RolePermission.query.filter_by(
            role_id=admin.id, permission_id=stock.id
        ).one()
        try:
            db.session.delete(link)
            db.session.commit()
            assert not has_permission('stock.view', user)
        finally:
            # Ce test vérifie un retrait temporaire ; il ne doit pas laisser
            # le rôle système admin dans un état modifié pour les tests
            # suivants (notamment les smoke tests stock).
            if not RolePermission.query.filter_by(
                role_id=admin.id, permission_id=stock.id
            ).first():
                db.session.add(RolePermission(
                    role_id=admin.id, permission_id=stock.id, effect='allow'
                ))
                db.session.commit()


def test_last_super_admin_cannot_be_removed_or_deactivated(app):
    with app.app_context():
        super_role = Role.query.filter_by(slug='super_admin', is_active=True).first()
        # Les tests précédents peuvent avoir créé des comptes temporaires.
        for link in list(UserRole.query.filter_by(role_id=super_role.id).all()):
            db.session.delete(link)
        db.session.flush()
        user = _user()
        replace_user_roles(user, [super_role.id])
        db.session.commit()
        with pytest.raises(LastSuperAdminError):
            replace_user_roles(user, [])
        with pytest.raises(LastSuperAdminError):
            ensure_super_admin_survives(user, will_be_active=False)
