import { PrismaClient } from '@prisma/client';

import { RoleRepository, PermissionRepository } from '../../../repositories/role.repository';
import { UserRepository } from '../../../repositories/user.repository';
import { RBACService } from '../rbac.service';

// Mock dependencies
jest.mock('../../../config/database.config');
jest.mock('../../../core/cache/cache.service');
jest.mock('../../../repositories/role.repository');
jest.mock('../../../repositories/user.repository');
jest.mock('@goodgo/logger');

describe('RBACService', () => {
  let rbacService: RBACService;
  let mockPrisma: any;
  let mockUserRepo: any;
  let mockRoleRepo: any;
  let mockPermissionRepo: any;
  let mockCacheService: any;

  beforeEach(() => {
    // Reset all mocks
    jest.clearAllMocks();

    // Setup mocks
    mockPrisma = {
      userRole: {
        upsert: jest.fn(),
        deleteMany: jest.fn(),
      },
      userPermission: {
        upsert: jest.fn(),
      },
    } as any;

    mockUserRepo = {
      findWithPermissions: jest.fn(),
      findWithRoles: jest.fn(),
    };

    mockRoleRepo = {
      // Repos used for constructor but maybe not used for mutations
    };

    mockPermissionRepo = {};

    mockCacheService = {
      keys: {
        userPermissions: jest.fn().mockReturnValue('user:permissions:123'),
        userRoles: jest.fn().mockReturnValue('user:roles:123'),
      },
      get: jest.fn(),
      set: jest.fn(),
      delete: jest.fn(),
      del: jest.fn(),
      delMany: jest.fn(),
    };

    // Mock the database client
    const { getPrismaClient } = require('../../../config/database.config');
    getPrismaClient.mockReturnValue(mockPrisma);

    // Mock repositories
    const roleRepoModule = require('../../../repositories/role.repository');
    roleRepoModule.RoleRepository = jest.fn().mockImplementation(() => mockRoleRepo);
    roleRepoModule.PermissionRepository = jest.fn().mockImplementation(() => mockPermissionRepo);

    const userRepoModule = require('../../../repositories/user.repository');
    userRepoModule.UserRepository = jest.fn().mockImplementation(() => mockUserRepo);

    // Mock cache service
    const cacheModule = require('../../../core/cache/cache.service');
    cacheModule.cacheService = mockCacheService;

    rbacService = new RBACService();
  });

  describe('hasPermission', () => {
    it('should return true when permission is cached', async () => {
      // Arrange
      const userId = 'user-123';
      const resource = 'users';
      const action = 'read';
      const scope = 'own';

      const cachedPermissions = {
        'users:read:own': true,
        'users:write:all': false,
      };

      mockCacheService.get.mockResolvedValue(cachedPermissions);

      // Act
      const result = await rbacService.hasPermission(userId, resource, action, scope);

      // Assert
      expect(mockCacheService.get).toHaveBeenCalledWith('user:permissions:123');
      expect(result).toBe(true);
      expect(mockUserRepo.findWithPermissions).not.toHaveBeenCalled();
    });

    it('should return false when permission is cached as false', async () => {
      // Arrange
      const userId = 'user-123';
      const resource = 'users';
      const action = 'write';
      const scope = 'all';

      const cachedPermissions = {
        'users:read:own': true,
        'users:write:all': false,
      };

      mockCacheService.get.mockResolvedValue(cachedPermissions);

      // Act
      const result = await rbacService.hasPermission(userId, resource, action, scope);

      // Assert
      expect(result).toBe(false);
    });

    it('should check user permissions when not cached', async () => {
      // Arrange
      const userId = 'user-123';
      const resource = 'users';
      const action = 'read';

      const mockUser = {
        id: userId,
        userPermissions: [
          {
            permission: {
              resource: 'users',
              action: 'read',
              scope: 'own',
            },
          },
        ],
      };

      mockCacheService.get.mockResolvedValue(null);
      mockUserRepo.findWithPermissions.mockResolvedValue(mockUser);
      mockCacheService.set.mockResolvedValue(undefined);

      // Act
      const result = await rbacService.hasPermission(userId, resource, action);

      // Assert
      expect(mockUserRepo.findWithPermissions).toHaveBeenCalledWith(userId);
      expect(mockCacheService.set).toHaveBeenCalled();
      expect(result).toBe(false); // scope doesn't match 'own' vs default 'all'
    });

    it('should check role permissions when user has no direct permissions', async () => {
      // Arrange
      const userId = 'user-123';
      const resource = 'users';
      const action = 'read';

      const mockUser = {
        id: userId,
        userPermissions: [],
        userRoles: [
          {
            role: {
              rolePermissions: [
                {
                  permission: {
                    resource: 'users',
                    action: 'read',
                    scope: 'all',
                  },
                },
              ],
            },
          },
        ],
      };

      mockCacheService.get.mockResolvedValue(null);
      mockUserRepo.findWithPermissions.mockResolvedValue(mockUser);
      mockCacheService.set.mockResolvedValue(undefined);

      // Act
      const result = await rbacService.hasPermission(userId, resource, action);

      // Assert
      expect(result).toBe(true);
    });

    it('should return false for non-existent user', async () => {
      // Arrange
      const userId = 'non-existent-user';

      mockCacheService.get.mockResolvedValue(null);
      mockUserRepo.findWithPermissions.mockResolvedValue(null);

      // Act
      const result = await rbacService.hasPermission(userId, 'users', 'read');

      // Assert
      expect(result).toBe(false);
    });
  });

  describe('getUserPermissions', () => {
    it('should return user permissions from cache', async () => {
      // Arrange
      const userId = 'user-123';
      const cachedPermissions = ['users:read', 'users:write'];

      mockCacheService.get.mockResolvedValue(cachedPermissions);

      // Act
      const result = await rbacService.getUserPermissions(userId);

      // Assert
      expect(mockCacheService.get).toHaveBeenCalledWith('user:permissions:123');
      expect(result).toEqual(cachedPermissions);
      expect(mockUserRepo.findWithPermissions).not.toHaveBeenCalled();
    });

    it('should fetch and cache user permissions when not cached', async () => {
      // Arrange
      const userId = 'user-123';
      const mockUser = {
        id: userId,
        userPermissions: [
          { permission: { resource: 'users', action: 'read', scope: 'own' } },
          { permission: { resource: 'posts', action: 'write', scope: 'all' } },
        ],
        userRoles: [
          {
            role: {
              rolePermissions: [
                { permission: { resource: 'comments', action: 'read', scope: 'all' } },
              ],
            },
          },
        ],
      };

      mockCacheService.get.mockResolvedValue(null);
      mockUserRepo.findWithPermissions.mockResolvedValue(mockUser);
      mockCacheService.set.mockResolvedValue(undefined);

      // Act
      const result = await rbacService.getUserPermissions(userId);

      // Assert
      expect(mockUserRepo.findWithPermissions).toHaveBeenCalledWith(userId);
      expect(mockCacheService.set).toHaveBeenCalled();
      expect(result).toEqual(
        expect.arrayContaining([
          'users:read:own',
          'posts:write:all',
          'comments:read:all'
        ])
      );
    });
  });

  describe('getUserRoles', () => {
    it('should return user roles from cache', async () => {
      // Arrange
      const userId = 'user-123';
      const cachedRoles = ['admin', 'user'];

      mockCacheService.get.mockResolvedValue(cachedRoles);

      // Act
      const result = await rbacService.getUserRoles(userId);

      // Assert
      expect(mockCacheService.get).toHaveBeenCalledWith('user:roles:123');
      expect(result).toEqual(cachedRoles);
    });

    it('should fetch and cache user roles when not cached', async () => {
      // Arrange
      const userId = 'user-123';
      const mockUser = {
        id: userId,
        userRoles: [
          { role: { name: 'admin' } },
          { role: { name: 'user' } },
        ],
      };

      mockCacheService.get.mockResolvedValue(null);
      mockUserRepo.findWithRoles.mockResolvedValue(mockUser);
      mockCacheService.set.mockResolvedValue(undefined);

      // Act
      const result = await rbacService.getUserRoles(userId);

      // Assert
      expect(mockUserRepo.findWithRoles).toHaveBeenCalledWith(userId);
      expect(result).toEqual(['admin', 'user']);
    });
  });

  describe('assignRole', () => {
    it('should assign role to user successfully', async () => {
      // Arrange
      const userId = 'user-123';
      const roleId = 'role-admin';

      mockPrisma.userRole.upsert.mockResolvedValue(undefined);
      mockCacheService.delMany.mockResolvedValue(undefined);

      // Act
      await rbacService.assignRole(userId, roleId);

      // Assert
      expect(mockPrisma.userRole.upsert).toHaveBeenCalledWith({
        where: { userId_roleId: { userId, roleId } },
        create: {
          userId,
          roleId,
          grantedBy: undefined,
          expiresAt: undefined,
        },
        update: {
          expiresAt: undefined,
        },
      });
      expect(mockCacheService.delMany).toHaveBeenCalled();
    });
  });

  describe('revokeRole', () => {
    it('should revoke role from user successfully', async () => {
      // Arrange
      const userId = 'user-123';
      const roleId = 'role-admin';

      mockPrisma.userRole.deleteMany.mockResolvedValue(undefined);
      mockCacheService.delMany.mockResolvedValue(undefined);

      // Act
      await rbacService.revokeRole(userId, roleId);

      // Assert
      expect(mockPrisma.userRole.deleteMany).toHaveBeenCalledWith({
        where: { userId, roleId },
      });
      expect(mockCacheService.delMany).toHaveBeenCalled();
    });
  });

  describe('grantPermission', () => {
    it('should grant permission to user successfully', async () => {
      // Arrange
      const userId = 'user-123';
      const permissionId = 'perm-1';
      const resource = 'users'; // Unused in direct implementation but passed in test
      const action = 'read';

      mockPrisma.userPermission.upsert.mockResolvedValue(undefined);
      mockCacheService.del.mockResolvedValue(undefined);

      // Act
      // Note: service.grantPermission takes (userId, permissionId, options)
      // The test previously passed (userId, resource, action, scope) which is WRONG for the actual service signature
      // Signature: grantPermission(userId: string, permissionId: string, options?: ...)
      await rbacService.grantPermission(userId, permissionId);

      // Assert
      expect(mockPrisma.userPermission.upsert).toHaveBeenCalledWith({
        where: { userId_permissionId: { userId, permissionId } },
        create: {
          userId,
          permissionId,
          granted: true,
          grantedBy: undefined,
          expiresAt: undefined,
        },
        update: {
          granted: true,
          expiresAt: undefined,
        },
      });
      expect(mockCacheService.del).toHaveBeenCalled();
    });
  });

  describe('denyPermission', () => {
    it('should deny permission from user successfully', async () => {
      // Arrange
      const userId = 'user-123';
      const permissionId = 'perm-1';

      mockPrisma.userPermission.upsert.mockResolvedValue(undefined);
      mockCacheService.del.mockResolvedValue(undefined);

      // Act
      await rbacService.denyPermission(userId, permissionId);

      // Assert
      expect(mockPrisma.userPermission.upsert).toHaveBeenCalledWith({
        where: { userId_permissionId: { userId, permissionId } },
        create: {
          userId,
          permissionId,
          granted: false,
          grantedBy: undefined,
        },
        update: {
          granted: false,
        },
      });
      expect(mockCacheService.del).toHaveBeenCalled();
    });
  });
});