"""
Permission System Tests — models, services, API, DRF classes.
"""
import pytest
from django.db import transaction
from django.utils import timezone
from datetime import timedelta

from apps.core.tenant.models import Tenant, Domain
from apps.core.auth.models import User
from apps.core.permission.models import (
    Permission, PermissionAction, Role, RolePermission,
    UserRole, UserPermission, ObjectRelation, RAICPolicy,
    RelationType,
)
from apps.core.permission.services import PermissionService, PermissionChecker


# ──────────────────────────────────────────────────────────────
# Fixtures
# ──────────────────────────────────────────────────────────────

@pytest.fixture(scope='function')
def tenant(db):
    with transaction.atomic():
        t, _ = Tenant.objects.get_or_create(
            schema_name='public',
            defaults={
                'name': 'Test Tenant',
                'slug': 'perm-test-tenant',
            },
        )
        Domain.objects.get_or_create(
            domain='localhost',
            defaults={'tenant': t, 'is_primary': True},
        )
    return t


@pytest.fixture
def make_user(tenant):
    """Factory to create users with tenant."""
    _counter = [0]
    def _make(email=None, **kwargs):
        _counter[0] += 1
        if email is None:
            email = f'user{_counter[0]}@test.com'
        return User.objects.create_user(
            email=email,
            password='testpass123',
            tenant=tenant,
            **kwargs,
        )
    return _make


@pytest.fixture
def permission_read(db):
    return Permission.objects.get_or_create(
        codename='pm.task.read',
        defaults={
            'name': 'Read Task',
            'module': 'pm',
            'resource': 'task',
            'action': PermissionAction.READ,
        },
    )[0]


@pytest.fixture
def permission_create(db):
    return Permission.objects.get_or_create(
        codename='pm.task.create',
        defaults={
            'name': 'Create Task',
            'module': 'pm',
            'resource': 'task',
            'action': PermissionAction.CREATE,
        },
    )[0]


@pytest.fixture
def permission_update(db):
    return Permission.objects.get_or_create(
        codename='pm.task.update',
        defaults={
            'name': 'Update Task',
            'module': 'pm',
            'resource': 'task',
            'action': PermissionAction.UPDATE,
        },
    )[0]


@pytest.fixture
def permission_delete(db):
    return Permission.objects.get_or_create(
        codename='pm.task.delete',
        defaults={
            'name': 'Delete Task',
            'module': 'pm',
            'resource': 'task',
            'action': PermissionAction.DELETE,
        },
    )[0]


@pytest.fixture
def role_employee(tenant):
    role, _ = Role.objects.get_or_create(
        slug='employee',
        tenant=tenant,
        defaults={
            'name': 'Employee',
            'is_system': True,
        },
    )
    return role


@pytest.fixture
def role_manager(tenant):
    role, _ = Role.objects.get_or_create(
        slug='manager',
        tenant=tenant,
        defaults={
            'name': 'Manager',
            'is_system': True,
        },
    )
    return role


@pytest.fixture
def setup_rbac(
    make_user, role_employee, role_manager,
    permission_read, permission_create, permission_update, permission_delete,
):
    """Set up a full RBAC scenario with two users and roles."""
    employee_user = make_user(email='emp@test.com')
    manager_user = make_user(email='mgr@test.com')

    # Employee: read + create
    RolePermission.objects.get_or_create(role=role_employee, permission=permission_read)
    RolePermission.objects.get_or_create(role=role_employee, permission=permission_create)
    UserRole.objects.get_or_create(user=employee_user, role=role_employee)

    # Manager: all permissions
    RolePermission.objects.get_or_create(role=role_manager, permission=permission_read)
    RolePermission.objects.get_or_create(role=role_manager, permission=permission_create)
    RolePermission.objects.get_or_create(role=role_manager, permission=permission_update)
    RolePermission.objects.get_or_create(role=role_manager, permission=permission_delete)
    UserRole.objects.get_or_create(user=manager_user, role=role_manager)

    return {
        'employee_user': employee_user,
        'manager_user': manager_user,
        'role_employee': role_employee,
        'role_manager': role_manager,
    }


# ──────────────────────────────────────────────────────────────
# Model Tests
# ──────────────────────────────────────────────────────────────

class TestPermissionModel:
    def test_auto_codename(self, db):
        perm, _ = Permission.objects.get_or_create(
            codename='test.item.create',
            defaults={
                'name': 'Test',
                'module': 'test',
                'resource': 'item',
                'action': PermissionAction.CREATE,
            },
        )
        assert perm.codename == 'test.item.create'

    def test_str(self, permission_read):
        assert str(permission_read) == 'pm.task.read'


class TestRoleModel:
    def test_create_role(self, role_employee):
        assert role_employee.slug == 'employee'
        assert role_employee.is_system is True

    def test_role_hierarchy(self, tenant, role_employee, role_manager, permission_read, permission_update):
        """Manager role inherits from employee."""
        role_manager.parent = role_employee
        role_manager.save()

        RolePermission.objects.get_or_create(role=role_employee, permission=permission_read)
        RolePermission.objects.get_or_create(role=role_manager, permission=permission_update)

        # Manager should have both inherited + own permissions
        all_perms = role_manager.get_all_permissions()
        codenames = {p.codename for p in all_perms}
        assert 'pm.task.read' in codenames
        assert 'pm.task.update' in codenames


class TestUserPermissionModel:
    def test_explicit_allow(self, make_user, permission_read):
        user = make_user()
        override = UserPermission.objects.create(
            user=user,
            permission=permission_read,
            allow=True,
            reason='Special access',
        )
        assert override.is_active() is True
        assert str(override) == f'{user} → ALLOW pm.task.read'

    def test_explicit_deny(self, make_user, permission_read):
        user = make_user()
        override = UserPermission.objects.create(
            user=user,
            permission=permission_read,
            allow=False,
            reason='Restricted',
        )
        assert 'DENY' in str(override)

    def test_time_based_validity(self, make_user, permission_read):
        user = make_user()
        # Expired override
        override = UserPermission.objects.create(
            user=user,
            permission=permission_read,
            allow=True,
            expires_at=timezone.now() - timedelta(days=1),
        )
        assert override.is_active() is False

        # Future override
        override2 = UserPermission.objects.create(
            user=make_user(email='u2@test.com'),
            permission=permission_read,
            allow=True,
            starts_at=timezone.now() + timedelta(days=1),
        )
        assert override2.is_active() is False


class TestObjectRelationModel:
    def test_create_relation(self, make_user):
        user = make_user()
        rel = ObjectRelation.objects.create(
            object_type='pm.task',
            object_id='task-123',
            user=user,
            relation_type=RelationType.ASSIGNEE,
            module='pm',
        )
        assert 'assignee' in str(rel)
        assert rel.object_type == 'pm.task'


class TestRAICPolicyModel:
    def test_create_policy(self, db, permission_update):
        policy, _ = RAICPolicy.objects.get_or_create(
            module='pm',
            object_type='pm.task',
            relation_type=RelationType.ASSIGNEE,
            permission=permission_update,
        )
        assert str(policy) == 'assignee on pm.task → pm.task.update'


# ──────────────────────────────────────────────────────────────
# Service Tests
# ──────────────────────────────────────────────────────────────

class TestPermissionServiceBasic:
    def test_has_permission_authenticated(self, setup_rbac):
        data = setup_rbac
        assert PermissionService.has_permission(
            data['employee_user'], 'pm.task.read'
        ) is True
        assert PermissionService.has_permission(
            data['employee_user'], 'pm.task.delete'
        ) is False

    def test_has_permission_superuser(self, make_user, permission_read):
        superuser = make_user(
            email='super@test.com',
            is_superuser=True,
        )
        assert PermissionService.has_permission(superuser, 'pm.task.read') is True
        assert PermissionService.has_permission(superuser, 'anything.ever') is True

    def test_has_permission_unauthenticated(self):
        assert PermissionService.has_permission(None, 'pm.task.read') is False

    def test_has_any_permission(self, setup_rbac):
        data = setup_rbac
        assert PermissionService.has_any_permission(
            data['employee_user'], ['pm.task.read', 'pm.task.delete']
        ) is True
        assert PermissionService.has_any_permission(
            data['employee_user'], ['pm.task.update', 'pm.task.delete']
        ) is False

    def test_has_all_permissions(self, setup_rbac):
        data = setup_rbac
        assert PermissionService.has_all_permissions(
            data['employee_user'], ['pm.task.read', 'pm.task.create']
        ) is True
        assert PermissionService.has_all_permissions(
            data['employee_user'], ['pm.task.read', 'pm.task.delete']
        ) is False


class TestUserPermissionOverride:
    def test_explicit_deny_overrides_role(self, setup_rbac, permission_read):
        """Explicit deny should override role-based access."""
        data = setup_rbac
        user = data['employee_user']
        
        # Employee has pm.task.read from role
        assert PermissionService.has_permission(user, 'pm.task.read') is True
        
        # Create explicit deny
        UserPermission.objects.create(
            user=user,
            permission=permission_read,
            allow=False,
            reason='Access revoked',
        )
        
        # Clear cache
        from django.core.cache import cache
        cache.clear()
        
        # Override should deny
        result = PermissionService._check_user_override(user, 'pm.task.read')
        assert result is False

    def test_explicit_allow_grants_access(self, make_user, permission_update):
        """Explicit allow grants access even without role."""
        user = make_user(email='special@test.com')
        
        UserPermission.objects.create(
            user=user,
            permission=permission_update,
            allow=True,
        )
        
        from django.core.cache import cache
        cache.clear()
        
        result = PermissionService._check_user_override(user, 'pm.task.update')
        assert result is True


class TestGetUserPermissions:
    def test_returns_role_permissions(self, setup_rbac):
        data = setup_rbac
        perms = PermissionService.get_user_permissions(data['employee_user'])
        assert 'pm.task.read' in perms
        assert 'pm.task.create' in perms
        assert 'pm.task.delete' not in perms

    def test_includes_explicit_allows(self, setup_rbac, permission_delete):
        data = setup_rbac
        user = data['employee_user']
        
        UserPermission.objects.create(
            user=user,
            permission=permission_delete,
            allow=True,
        )
        
        from django.core.cache import cache
        cache.clear()
        
        perms = PermissionService.get_user_permissions(user)
        assert 'pm.task.delete' in perms

    def test_excludes_explicit_denies(self, setup_rbac, permission_read):
        data = setup_rbac
        user = data['employee_user']
        
        UserPermission.objects.create(
            user=user,
            permission=permission_read,
            allow=False,
        )
        
        from django.core.cache import cache
        cache.clear()
        
        perms = PermissionService.get_user_permissions(user)
        assert 'pm.task.read' not in perms

    def test_superuser_gets_all(self, make_user, permission_read, permission_create):
        superuser = make_user(email='su@test.com', is_superuser=True)
        perms = PermissionService.get_user_permissions(superuser)
        assert 'pm.task.read' in perms
        assert 'pm.task.create' in perms

    def test_returns_sorted_list(self, setup_rbac):
        data = setup_rbac
        perms = PermissionService.get_user_permissions(data['manager_user'])
        assert isinstance(perms, list)
        assert perms == sorted(perms)


class TestPermissionChecker:
    def test_context_manager(self, setup_rbac):
        data = setup_rbac
        with PermissionChecker(data['employee_user']) as checker:
            assert checker.has('pm.task.read') is True
            assert checker.has('pm.task.delete') is False

    def test_has_any(self, setup_rbac):
        data = setup_rbac
        with PermissionChecker(data['employee_user']) as checker:
            assert checker.has_any(['pm.task.read', 'pm.task.delete']) is True
            assert checker.has_any(['pm.task.update', 'pm.task.delete']) is False

    def test_has_all(self, setup_rbac):
        data = setup_rbac
        with PermissionChecker(data['manager_user']) as checker:
            assert checker.has_all(['pm.task.read', 'pm.task.update']) is True


class TestRAICService:
    def test_register_relation(self, make_user):
        user = make_user()
        rel = PermissionService.register_relation(
            user=user,
            object_type='pm.task',
            object_id='task-456',
            relation_type='assignee',
            module='pm',
        )
        assert rel.relation_type == 'assignee'
        assert rel.object_id == 'task-456'

    def test_remove_relation(self, make_user):
        user = make_user()
        PermissionService.register_relation(
            user=user,
            object_type='pm.task',
            object_id='task-789',
            relation_type='assignee',
            module='pm',
        )
        deleted = PermissionService.remove_relation(
            user=user,
            object_type='pm.task',
            object_id='task-789',
            relation_type='assignee',
        )
        assert deleted is True

    def test_get_object_relations(self, make_user):
        user1 = make_user(email='u1@test.com')
        user2 = make_user(email='u2@test.com')
        PermissionService.register_relation(
            user=user1, object_type='pm.task',
            object_id='t1', relation_type='assignee', module='pm',
        )
        PermissionService.register_relation(
            user=user2, object_type='pm.task',
            object_id='t1', relation_type='watcher', module='pm',
        )
        rels = PermissionService.get_object_relations('pm.task', 't1')
        assert len(rels) == 2


# ──────────────────────────────────────────────────────────────
# API Tests
# ──────────────────────────────────────────────────────────────

@pytest.fixture
def api_client():
    from rest_framework.test import APIClient
    client = APIClient()
    client.defaults['HTTP_HOST'] = 'localhost'
    client.defaults['SERVER_NAME'] = 'localhost'
    return client


class TestMyPermissionsAPI:
    def test_my_permissions_authenticated(
        self, api_client, setup_rbac,
    ):
        data = setup_rbac
        api_client.force_authenticate(user=data['employee_user'])
        resp = api_client.get('/api/v1/access/me/')
        assert resp.status_code == 200
        body = resp.data.get('data', resp.data)
        assert 'permissions' in body
        assert 'roles' in body
        assert 'pm.task.read' in body['permissions']
        assert any(r['slug'] == 'employee' for r in body['roles'])

    @pytest.mark.django_db
    def test_my_permissions_unauthenticated(self, api_client):
        resp = api_client.get('/api/v1/access/me/')
        assert resp.status_code == 401


class TestRoleAPI:
    def test_list_roles(self, api_client, setup_rbac):
        data = setup_rbac
        api_client.force_authenticate(user=data['manager_user'])
        resp = api_client.get('/api/v1/access/roles/')
        assert resp.status_code == 200

    def test_create_role(self, api_client, make_user, tenant):
        admin = make_user(email='admin@example.com', is_superuser=True)
        api_client.force_authenticate(user=admin)
        resp = api_client.post('/api/v1/access/roles/', {
            'name': 'Custom Role',
            'slug': 'custom-role',
            'description': 'Test custom role',
        }, format='json')
        assert resp.status_code == 201
        assert resp.data['name'] == 'Custom Role'

    def test_cannot_delete_system_role(self, api_client, make_user, role_employee):
        admin = make_user(email='admin2@example.com', is_superuser=True)
        api_client.force_authenticate(user=admin)
        resp = api_client.delete(f'/api/v1/access/roles/{role_employee.id}/')
        assert resp.status_code == 400


class TestPermissionAPI:
    def test_list_permissions(self, api_client, setup_rbac, permission_read):
        data = setup_rbac
        api_client.force_authenticate(user=data['manager_user'])
        resp = api_client.get('/api/v1/access/permissions/')
        assert resp.status_code == 200

    def test_grouped_permissions(self, api_client, setup_rbac, permission_read, permission_create):
        data = setup_rbac
        api_client.force_authenticate(user=data['manager_user'])
        resp = api_client.get('/api/v1/access/permissions/grouped/')
        assert resp.status_code == 200
        body = resp.data.get('data', resp.data)
        # body is a list of {module, resources}
        modules = [item['module'] for item in body] if isinstance(body, list) else body.keys()
        assert 'pm' in modules


class TestLoginIncludesPermissions:
    """Test that login response includes permissions and roles."""
    
    def test_login_response_has_permissions(self, api_client, setup_rbac):
        data = setup_rbac
        user = data['employee_user']
        # Set password for login
        user.set_password('testpass123')
        user.save()
        
        resp = api_client.post('/api/v1/auth/login/', {
            'email': user.email,
            'password': 'testpass123',
        }, format='json')
        assert resp.status_code == 200
        user_data = resp.data.get('user', {})
        assert 'permissions' in user_data
        assert 'roles' in user_data
        assert isinstance(user_data['permissions'], list)
        assert isinstance(user_data['roles'], list)
