/**
 * @fileoverview Unit tests for PermissionsService
 * Tests cover CRUD operations, bulk creation, grouping, validation, and role relationships
 */

import { Repository, SelectQueryBuilder } from 'typeorm';
import { Permission, PermissionAction } from '../entities/permission.entity.js';
import { Role } from '../entities/role.entity.js';
import {
  PermissionsService,
  PermissionSearchParams,
  CreatePermissionDto,
  UpdatePermissionDto,
  BulkCreatePermissionsDto,
} from '../services/permissions.service.js';

describe('PermissionsService', () => {
  let permissionsService: PermissionsService;
  let mockPermissionRepository: Partial<Repository<Permission>>;
  let mockRoleRepository: Partial<Repository<Role>>;
  let mockQueryBuilder: Partial<SelectQueryBuilder<Permission>>;

  const mockPermissionId = '550e8400-e29b-41d4-a716-446655440020';
  const mockPermissionId2 = '550e8400-e29b-41d4-a716-446655440021';
  const mockRoleId = '550e8400-e29b-41d4-a716-446655440010';
  const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';

  const mockPermission: Partial<Permission> = {
    id: mockPermissionId,
    resource: 'users',
    action: PermissionAction.READ,
    description: 'Read users',
    module: 'auth',
    createdAt: new Date('2026-01-01'),
    roles: [],
  };

  const mockPermission2: Partial<Permission> = {
    id: mockPermissionId2,
    resource: 'users',
    action: PermissionAction.CREATE,
    description: 'Create users',
    module: 'auth',
    createdAt: new Date('2026-01-01'),
    roles: [],
  };

  const mockRole: Partial<Role> = {
    id: mockRoleId,
    tenantId: mockTenantId,
    name: 'Administrator',
    code: 'ADMIN',
    permissions: [mockPermission as Permission],
  };

  beforeEach(() => {
    jest.clearAllMocks();

    // Setup mock query builder
    mockQueryBuilder = {
      select: jest.fn().mockReturnThis(),
      where: jest.fn().mockReturnThis(),
      orderBy: jest.fn().mockReturnThis(),
      getRawMany: jest.fn().mockResolvedValue([{ resource: 'users' }, { resource: 'roles' }]),
    };

    // Setup mock repositories
    mockPermissionRepository = {
      create: jest.fn().mockReturnValue(mockPermission),
      save: jest.fn().mockResolvedValue(mockPermission),
      findOne: jest.fn().mockResolvedValue(mockPermission),
      find: jest.fn().mockResolvedValue([mockPermission, mockPermission2]),
      findAndCount: jest.fn().mockResolvedValue([[mockPermission], 1]),
      delete: jest.fn().mockResolvedValue({ affected: 1 }),
      createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder),
    };

    mockRoleRepository = {
      findOne: jest.fn().mockResolvedValue(mockRole),
    };

    permissionsService = new PermissionsService(
      mockPermissionRepository as Repository<Permission>,
      mockRoleRepository as Repository<Role>
    );
  });

  describe('findAll', () => {
    it('should return all permissions with pagination', async () => {
      const params: PermissionSearchParams = {
        limit: 100,
        offset: 0,
      };

      const result = await permissionsService.findAll(params);

      expect(mockPermissionRepository.findAndCount).toHaveBeenCalledWith({
        where: [{}],
        take: 100,
        skip: 0,
        order: { module: 'ASC', resource: 'ASC', action: 'ASC' },
      });
      expect(result.data).toEqual([mockPermission]);
      expect(result.total).toBe(1);
    });

    it('should filter by resource', async () => {
      const params: PermissionSearchParams = {
        resource: 'users',
        limit: 100,
        offset: 0,
      };

      await permissionsService.findAll(params);

      const callArgs = (mockPermissionRepository.findAndCount as jest.Mock).mock.calls[0][0];
      expect(callArgs.where[0].resource).toBe('users');
    });

    it('should filter by action', async () => {
      const params: PermissionSearchParams = {
        action: PermissionAction.READ,
        limit: 100,
        offset: 0,
      };

      await permissionsService.findAll(params);

      const callArgs = (mockPermissionRepository.findAndCount as jest.Mock).mock.calls[0][0];
      expect(callArgs.where[0].action).toBe(PermissionAction.READ);
    });

    it('should filter by module', async () => {
      const params: PermissionSearchParams = {
        module: 'auth',
        limit: 100,
        offset: 0,
      };

      await permissionsService.findAll(params);

      const callArgs = (mockPermissionRepository.findAndCount as jest.Mock).mock.calls[0][0];
      expect(callArgs.where[0].module).toBe('auth');
    });

    it('should filter by search term', async () => {
      const params: PermissionSearchParams = {
        search: 'user',
        limit: 100,
        offset: 0,
      };

      await permissionsService.findAll(params);

      const callArgs = (mockPermissionRepository.findAndCount as jest.Mock).mock.calls[0][0];
      expect(callArgs.where).toHaveLength(3); // resource, description, module search
    });

    it('should use default pagination values', async () => {
      const params: PermissionSearchParams = {};

      await permissionsService.findAll(params);

      const callArgs = (mockPermissionRepository.findAndCount as jest.Mock).mock.calls[0][0];
      expect(callArgs.take).toBe(100);
      expect(callArgs.skip).toBe(0);
    });

    it('should combine multiple filters', async () => {
      const params: PermissionSearchParams = {
        resource: 'users',
        action: PermissionAction.READ,
        module: 'auth',
        limit: 50,
        offset: 10,
      };

      await permissionsService.findAll(params);

      const callArgs = (mockPermissionRepository.findAndCount as jest.Mock).mock.calls[0][0];
      expect(callArgs.where[0].resource).toBe('users');
      expect(callArgs.where[0].action).toBe(PermissionAction.READ);
      expect(callArgs.where[0].module).toBe('auth');
      expect(callArgs.take).toBe(50);
      expect(callArgs.skip).toBe(10);
    });
  });

  describe('findOne', () => {
    it('should return permission by ID with relations', async () => {
      const result = await permissionsService.findOne(mockPermissionId);

      expect(mockPermissionRepository.findOne).toHaveBeenCalledWith({
        where: { id: mockPermissionId },
        relations: ['roles'],
      });
      expect(result).toEqual(mockPermission);
    });

    it('should return null when permission not found', async () => {
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue(null);

      const result = await permissionsService.findOne('non-existent-id');

      expect(result).toBeNull();
    });
  });

  describe('findByResourceAction', () => {
    it('should return permission by resource and action', async () => {
      const result = await permissionsService.findByResourceAction('users', PermissionAction.READ);

      expect(mockPermissionRepository.findOne).toHaveBeenCalledWith({
        where: { resource: 'users', action: PermissionAction.READ },
      });
      expect(result).toEqual(mockPermission);
    });

    it('should return null when not found', async () => {
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue(null);

      const result = await permissionsService.findByResourceAction(
        'nonexistent',
        PermissionAction.DELETE
      );

      expect(result).toBeNull();
    });
  });

  describe('findByModule', () => {
    it('should return permissions for a module', async () => {
      const result = await permissionsService.findByModule('auth');

      expect(mockPermissionRepository.find).toHaveBeenCalledWith({
        where: { module: 'auth' },
        order: { resource: 'ASC', action: 'ASC' },
      });
      expect(result).toHaveLength(2);
    });
  });

  describe('findByResource', () => {
    it('should return permissions for a resource', async () => {
      const result = await permissionsService.findByResource('users');

      expect(mockPermissionRepository.find).toHaveBeenCalledWith({
        where: { resource: 'users' },
        order: { action: 'ASC' },
      });
      expect(result).toHaveLength(2);
    });
  });

  describe('create', () => {
    it('should create a new permission', async () => {
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue(null); // No existing permission

      const createDto: CreatePermissionDto = {
        resource: 'products',
        action: PermissionAction.CREATE,
        description: 'Create products',
        module: 'inventory',
      };

      const result = await permissionsService.create(createDto);

      expect(mockPermissionRepository.create).toHaveBeenCalledWith({
        resource: createDto.resource,
        action: createDto.action,
        description: createDto.description,
        module: createDto.module,
      });
      expect(mockPermissionRepository.save).toHaveBeenCalled();
      expect(result).toBeDefined();
    });

    it('should throw error when permission already exists', async () => {
      // Permission already exists
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue(mockPermission);

      const createDto: CreatePermissionDto = {
        resource: 'users',
        action: PermissionAction.READ,
      };

      await expect(permissionsService.create(createDto)).rejects.toThrow(
        'Ya existe el permiso read para el recurso users'
      );
    });

    it('should handle null description and module', async () => {
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue(null);

      const createDto: CreatePermissionDto = {
        resource: 'reports',
        action: PermissionAction.EXPORT,
      };

      await permissionsService.create(createDto);

      expect(mockPermissionRepository.create).toHaveBeenCalledWith({
        resource: 'reports',
        action: PermissionAction.EXPORT,
        description: null,
        module: null,
      });
    });
  });

  describe('update', () => {
    it('should update permission description', async () => {
      const updateDto: UpdatePermissionDto = {
        description: 'Updated description',
      };

      const result = await permissionsService.update(mockPermissionId, updateDto);

      expect(mockPermissionRepository.save).toHaveBeenCalled();
      expect(result).toBeDefined();
    });

    it('should update permission module', async () => {
      const updateDto: UpdatePermissionDto = {
        module: 'new-module',
      };

      const result = await permissionsService.update(mockPermissionId, updateDto);

      expect(mockPermissionRepository.save).toHaveBeenCalled();
      expect(result).toBeDefined();
    });

    it('should return null when permission not found', async () => {
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue(null);

      const result = await permissionsService.update('non-existent-id', {
        description: 'Test',
      });

      expect(result).toBeNull();
    });

    it('should handle setting values to null', async () => {
      const updateDto: UpdatePermissionDto = {
        description: undefined,
        module: undefined,
      };

      const result = await permissionsService.update(mockPermissionId, updateDto);

      expect(mockPermissionRepository.save).toHaveBeenCalled();
      expect(result).toBeDefined();
    });
  });

  describe('bulkCreateForResource', () => {
    it('should create multiple permissions for a resource', async () => {
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue(null); // None exist

      const dto: BulkCreatePermissionsDto = {
        resource: 'products',
        actions: [PermissionAction.CREATE, PermissionAction.READ, PermissionAction.UPDATE],
        module: 'inventory',
      };

      const result = await permissionsService.bulkCreateForResource(dto);

      expect(mockPermissionRepository.create).toHaveBeenCalledTimes(3);
      expect(mockPermissionRepository.save).toHaveBeenCalledTimes(3);
      expect(result).toHaveLength(3);
    });

    it('should skip existing permissions', async () => {
      // First action exists, second and third do not
      mockPermissionRepository.findOne = jest
        .fn()
        .mockResolvedValueOnce(mockPermission) // Exists
        .mockResolvedValueOnce(null) // Does not exist
        .mockResolvedValueOnce(null); // Does not exist

      const dto: BulkCreatePermissionsDto = {
        resource: 'users',
        actions: [PermissionAction.READ, PermissionAction.UPDATE, PermissionAction.DELETE],
        module: 'auth',
      };

      const result = await permissionsService.bulkCreateForResource(dto);

      expect(mockPermissionRepository.create).toHaveBeenCalledTimes(2);
      expect(mockPermissionRepository.save).toHaveBeenCalledTimes(2);
      expect(result).toHaveLength(2);
    });

    it('should return empty array when all permissions exist', async () => {
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue(mockPermission);

      const dto: BulkCreatePermissionsDto = {
        resource: 'users',
        actions: [PermissionAction.READ, PermissionAction.CREATE],
      };

      const result = await permissionsService.bulkCreateForResource(dto);

      expect(mockPermissionRepository.create).not.toHaveBeenCalled();
      expect(result).toHaveLength(0);
    });

    it('should auto-generate description for bulk created permissions', async () => {
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue(null);

      const dto: BulkCreatePermissionsDto = {
        resource: 'orders',
        actions: [PermissionAction.CREATE],
        module: 'sales',
      };

      await permissionsService.bulkCreateForResource(dto);

      expect(mockPermissionRepository.create).toHaveBeenCalledWith({
        resource: 'orders',
        action: PermissionAction.CREATE,
        description: 'create orders',
        module: 'sales',
      });
    });
  });

  describe('createCrudPermissions', () => {
    it('should create CRUD permissions for a resource', async () => {
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue(null);

      const result = await permissionsService.createCrudPermissions('customers', 'crm');

      expect(mockPermissionRepository.create).toHaveBeenCalledTimes(4); // CREATE, READ, UPDATE, DELETE
      expect(result).toHaveLength(4);
    });

    it('should work without module parameter', async () => {
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue(null);

      await permissionsService.createCrudPermissions('settings');

      expect(mockPermissionRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          module: null,
        })
      );
    });
  });

  describe('getResources', () => {
    it('should return list of unique resources', async () => {
      const result = await permissionsService.getResources();

      expect(mockPermissionRepository.createQueryBuilder).toHaveBeenCalledWith('p');
      expect(mockQueryBuilder.select).toHaveBeenCalledWith('DISTINCT p.resource', 'resource');
      expect(mockQueryBuilder.orderBy).toHaveBeenCalledWith('p.resource', 'ASC');
      expect(result).toEqual(['users', 'roles']);
    });
  });

  describe('getModules', () => {
    it('should return list of unique modules', async () => {
      mockQueryBuilder.getRawMany = jest
        .fn()
        .mockResolvedValue([{ module: 'auth' }, { module: 'inventory' }]);

      const result = await permissionsService.getModules();

      expect(mockQueryBuilder.select).toHaveBeenCalledWith('DISTINCT p.module', 'module');
      expect(mockQueryBuilder.where).toHaveBeenCalledWith('p.module IS NOT NULL');
      expect(result).toEqual(['auth', 'inventory']);
    });
  });

  describe('getPermissionsByModule', () => {
    it('should return permissions grouped by module', async () => {
      const permissions = [
        { ...mockPermission, module: 'auth' },
        { ...mockPermission2, module: 'auth' },
        { id: '3', resource: 'products', action: PermissionAction.READ, module: 'inventory' },
      ];
      mockPermissionRepository.find = jest.fn().mockResolvedValue(permissions);

      const result = await permissionsService.getPermissionsByModule();

      expect(mockPermissionRepository.find).toHaveBeenCalledWith({
        order: { module: 'ASC', resource: 'ASC', action: 'ASC' },
      });
      expect(result).toBeInstanceOf(Map);
      expect(result.get('auth')).toHaveLength(2);
      expect(result.get('inventory')).toHaveLength(1);
    });

    it('should group null modules under "other"', async () => {
      const permissions = [{ ...mockPermission, module: null }];
      mockPermissionRepository.find = jest.fn().mockResolvedValue(permissions);

      const result = await permissionsService.getPermissionsByModule();

      expect(result.get('other')).toHaveLength(1);
    });
  });

  describe('getPermissionsByResource', () => {
    it('should return permissions grouped by resource', async () => {
      const permissions = [
        { ...mockPermission, resource: 'users' },
        { ...mockPermission2, resource: 'users' },
        { id: '3', resource: 'roles', action: PermissionAction.READ },
      ];
      mockPermissionRepository.find = jest.fn().mockResolvedValue(permissions);

      const result = await permissionsService.getPermissionsByResource();

      expect(mockPermissionRepository.find).toHaveBeenCalledWith({
        order: { resource: 'ASC', action: 'ASC' },
      });
      expect(result).toBeInstanceOf(Map);
      expect(result.get('users')).toHaveLength(2);
      expect(result.get('roles')).toHaveLength(1);
    });
  });

  describe('roleHasPermission', () => {
    it('should return true when role has permission', async () => {
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue(mockPermission);
      mockRoleRepository.findOne = jest.fn().mockResolvedValue({
        ...mockRole,
        permissions: [mockPermission],
      });

      const result = await permissionsService.roleHasPermission(
        mockRoleId,
        'users',
        PermissionAction.READ
      );

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

    it('should return false when role does not have permission', async () => {
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue(mockPermission);
      mockRoleRepository.findOne = jest.fn().mockResolvedValue({
        ...mockRole,
        permissions: [],
      });

      const result = await permissionsService.roleHasPermission(
        mockRoleId,
        'users',
        PermissionAction.READ
      );

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

    it('should return false when permission does not exist', async () => {
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue(null);

      const result = await permissionsService.roleHasPermission(
        mockRoleId,
        'nonexistent',
        PermissionAction.READ
      );

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

    it('should return false when role does not exist', async () => {
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue(mockPermission);
      mockRoleRepository.findOne = jest.fn().mockResolvedValue(null);

      const result = await permissionsService.roleHasPermission(
        'non-existent-role',
        'users',
        PermissionAction.READ
      );

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

  describe('getRolesWithPermission', () => {
    it('should return roles that have the permission', async () => {
      const permissionWithRoles = {
        ...mockPermission,
        roles: [mockRole],
      };
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue(permissionWithRoles);

      const result = await permissionsService.getRolesWithPermission(mockPermissionId);

      expect(result).toEqual([mockRole]);
    });

    it('should return empty array when permission not found', async () => {
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue(null);

      const result = await permissionsService.getRolesWithPermission('non-existent');

      expect(result).toEqual([]);
    });

    it('should return empty array when permission has no roles', async () => {
      const permissionWithoutRoles = {
        ...mockPermission,
        roles: null,
      };
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue(permissionWithoutRoles);

      const result = await permissionsService.getRolesWithPermission(mockPermissionId);

      expect(result).toEqual([]);
    });
  });

  describe('delete', () => {
    it('should delete permission', async () => {
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue({
        ...mockPermission,
        roles: [],
      });

      const result = await permissionsService.delete(mockPermissionId);

      expect(mockPermissionRepository.delete).toHaveBeenCalledWith(mockPermissionId);
      expect(result).toBe(true);
    });

    it('should return false when permission not found', async () => {
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue(null);

      const result = await permissionsService.delete('non-existent-id');

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

    it('should throw error when permission is assigned to roles', async () => {
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue({
        ...mockPermission,
        roles: [mockRole],
      });

      await expect(permissionsService.delete(mockPermissionId)).rejects.toThrow(
        'No se puede eliminar un permiso asignado a roles'
      );
    });
  });

  describe('deleteByResource', () => {
    it('should delete all permissions for a resource', async () => {
      mockPermissionRepository.find = jest.fn().mockResolvedValue([
        { ...mockPermission, roles: [] },
        { ...mockPermission2, roles: [] },
      ]);
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue({
        ...mockPermission,
        roles: [],
      });
      mockPermissionRepository.delete = jest.fn().mockResolvedValue({ affected: 2 });

      const result = await permissionsService.deleteByResource('users');

      expect(mockPermissionRepository.delete).toHaveBeenCalledWith({ resource: 'users' });
      expect(result).toBe(2);
    });

    it('should throw error when any permission is assigned to roles', async () => {
      mockPermissionRepository.find = jest.fn().mockResolvedValue([
        { ...mockPermission, id: 'id1' },
        { ...mockPermission2, id: 'id2' },
      ]);
      // First permission is not assigned, second is
      mockPermissionRepository.findOne = jest
        .fn()
        .mockResolvedValueOnce({ ...mockPermission, roles: [] })
        .mockResolvedValueOnce({ ...mockPermission2, roles: [mockRole] });

      await expect(permissionsService.deleteByResource('users')).rejects.toThrow(
        'El permiso create de users esta asignado a roles'
      );
    });
  });

  describe('Permission Actions', () => {
    it('should support all permission actions', async () => {
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue(null);

      const allActions = [
        PermissionAction.CREATE,
        PermissionAction.READ,
        PermissionAction.UPDATE,
        PermissionAction.DELETE,
        PermissionAction.APPROVE,
        PermissionAction.CANCEL,
        PermissionAction.EXPORT,
      ];

      for (const action of allActions) {
        const createDto: CreatePermissionDto = {
          resource: 'test',
          action,
        };

        await permissionsService.create(createDto);

        expect(mockPermissionRepository.create).toHaveBeenCalledWith(
          expect.objectContaining({
            action,
          })
        );
      }
    });
  });

  describe('Global Permissions (No Tenant)', () => {
    it('should not include tenantId in permission operations', async () => {
      // Permissions are global (no tenantId), verify operations don't include tenant filtering
      await permissionsService.findOne(mockPermissionId);

      expect(mockPermissionRepository.findOne).toHaveBeenCalledWith({
        where: { id: mockPermissionId },
        relations: ['roles'],
      });
      // Should NOT include tenantId
      const callArgs = (mockPermissionRepository.findOne as jest.Mock).mock.calls[0][0];
      expect(callArgs.where.tenantId).toBeUndefined();
    });

    it('should create permissions without tenantId', async () => {
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue(null);

      const createDto: CreatePermissionDto = {
        resource: 'global-resource',
        action: PermissionAction.READ,
      };

      await permissionsService.create(createDto);

      const callArgs = (mockPermissionRepository.create as jest.Mock).mock.calls[0][0];
      expect(callArgs.tenantId).toBeUndefined();
    });
  });
});
