/**
 * @fileoverview Unit tests for RolesService
 * Tests cover CRUD operations, permission management, validation, and tenant isolation
 */

import { Repository } from 'typeorm';
import { Role } from '../entities/role.entity.js';
import { Permission, PermissionAction } from '../entities/permission.entity.js';
import { User } from '../entities/user.entity.js';
import { RolesService, RoleSearchParams, CreateRoleDto, UpdateRoleDto } from '../services/roles.service.js';

describe('RolesService', () => {
  let rolesService: RolesService;
  let mockRoleRepository: Partial<Repository<Role>>;
  let mockPermissionRepository: Partial<Repository<Permission>>;

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

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

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

  const mockRole: Partial<Role> = {
    id: mockRoleId,
    tenantId: mockTenantId,
    name: 'Administrator',
    code: 'ADMIN',
    description: 'Full system access',
    isSystem: false,
    color: '#FF5733',
    permissions: [mockPermission1 as Permission, mockPermission2 as Permission],
    users: [],
    createdAt: new Date('2026-01-01'),
    createdBy: mockUserId,
    updatedAt: null,
    updatedBy: null,
    deletedAt: null,
    deletedBy: null,
  };

  const mockSystemRole: Partial<Role> = {
    id: '550e8400-e29b-41d4-a716-446655440011',
    tenantId: mockTenantId,
    name: 'Super Admin',
    code: 'SUPER_ADMIN',
    description: 'System administrator role',
    isSystem: true,
    color: '#000000',
    permissions: [mockPermission1 as Permission],
    users: [],
    createdAt: new Date('2026-01-01'),
    createdBy: null,
    updatedAt: null,
    updatedBy: null,
    deletedAt: null,
    deletedBy: null,
  };

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

    // Setup mock repositories
    mockRoleRepository = {
      create: jest.fn().mockReturnValue(mockRole),
      save: jest.fn().mockResolvedValue(mockRole),
      findOne: jest.fn().mockResolvedValue(mockRole),
      find: jest.fn().mockResolvedValue([mockRole]),
      findAndCount: jest.fn().mockResolvedValue([[mockRole], 1]),
      softDelete: jest.fn().mockResolvedValue({ affected: 1 }),
    };

    mockPermissionRepository = {
      findBy: jest.fn().mockResolvedValue([mockPermission1, mockPermission2]),
      findOne: jest.fn().mockResolvedValue(mockPermission1),
    };

    rolesService = new RolesService(
      mockRoleRepository as Repository<Role>,
      mockPermissionRepository as Repository<Permission>
    );
  });

  describe('findAll', () => {
    it('should return all roles for a tenant with pagination', async () => {
      const params: RoleSearchParams = {
        tenantId: mockTenantId,
        limit: 50,
        offset: 0,
      };

      const result = await rolesService.findAll(params);

      expect(mockRoleRepository.findAndCount).toHaveBeenCalledWith({
        where: [{ tenantId: mockTenantId }],
        relations: ['permissions'],
        take: 50,
        skip: 0,
        order: { isSystem: 'DESC', name: 'ASC' },
      });
      expect(result.data).toEqual([mockRole]);
      expect(result.total).toBe(1);
    });

    it('should filter by search term', async () => {
      const params: RoleSearchParams = {
        tenantId: mockTenantId,
        search: 'Admin',
        limit: 50,
        offset: 0,
      };

      await rolesService.findAll(params);

      expect(mockRoleRepository.findAndCount).toHaveBeenCalled();
      const callArgs = (mockRoleRepository.findAndCount as jest.Mock).mock.calls[0][0];
      expect(callArgs.where).toHaveLength(3); // name, code, description search
    });

    it('should filter by isSystem flag', async () => {
      const params: RoleSearchParams = {
        tenantId: mockTenantId,
        isSystem: true,
        limit: 50,
        offset: 0,
      };

      await rolesService.findAll(params);

      const callArgs = (mockRoleRepository.findAndCount as jest.Mock).mock.calls[0][0];
      expect(callArgs.where[0].isSystem).toBe(true);
    });

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

      await rolesService.findAll(params);

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

  describe('findOne', () => {
    it('should return role by ID with relations', async () => {
      const result = await rolesService.findOne(mockRoleId, mockTenantId);

      expect(mockRoleRepository.findOne).toHaveBeenCalledWith({
        where: { id: mockRoleId, tenantId: mockTenantId },
        relations: ['permissions', 'users'],
      });
      expect(result).toEqual(mockRole);
    });

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

      const result = await rolesService.findOne('non-existent-id', mockTenantId);

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

    it('should enforce tenant isolation', async () => {
      const differentTenantId = '550e8400-e29b-41d4-a716-446655440999';

      await rolesService.findOne(mockRoleId, differentTenantId);

      expect(mockRoleRepository.findOne).toHaveBeenCalledWith({
        where: { id: mockRoleId, tenantId: differentTenantId },
        relations: ['permissions', 'users'],
      });
    });
  });

  describe('findByCode', () => {
    it('should return role by code', async () => {
      const result = await rolesService.findByCode('ADMIN', mockTenantId);

      expect(mockRoleRepository.findOne).toHaveBeenCalledWith({
        where: { code: 'ADMIN', tenantId: mockTenantId },
        relations: ['permissions'],
      });
      expect(result).toEqual(mockRole);
    });

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

      const result = await rolesService.findByCode('NON_EXISTENT', mockTenantId);

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

  describe('create', () => {
    it('should create a new role', async () => {
      mockRoleRepository.findOne = jest.fn().mockResolvedValue(null); // No existing role with same code

      const createDto: CreateRoleDto = {
        name: 'New Role',
        code: 'NEW_ROLE',
        description: 'A new role',
        isSystem: false,
        color: '#00FF00',
      };

      const result = await rolesService.create(mockTenantId, createDto, mockUserId);

      expect(mockRoleRepository.create).toHaveBeenCalledWith({
        tenantId: mockTenantId,
        name: createDto.name,
        code: createDto.code,
        description: createDto.description,
        isSystem: false,
        color: createDto.color,
        createdBy: mockUserId,
      });
      expect(mockRoleRepository.save).toHaveBeenCalled();
      expect(result).toBeDefined();
    });

    it('should create role with permissions', async () => {
      mockRoleRepository.findOne = jest.fn().mockResolvedValue(null);

      const createDto: CreateRoleDto = {
        name: 'Role With Permissions',
        code: 'ROLE_PERMS',
        permissionIds: [mockPermissionId1, mockPermissionId2],
      };

      await rolesService.create(mockTenantId, createDto, mockUserId);

      expect(mockPermissionRepository.findBy).toHaveBeenCalledWith({
        id: expect.anything(),
      });
      expect(mockRoleRepository.save).toHaveBeenCalled();
    });

    it('should throw error when code already exists', async () => {
      mockRoleRepository.findOne = jest.fn().mockResolvedValue(mockRole);

      const createDto: CreateRoleDto = {
        name: 'Duplicate Role',
        code: 'ADMIN',
      };

      await expect(rolesService.create(mockTenantId, createDto, mockUserId)).rejects.toThrow(
        'Ya existe un rol con el codigo "ADMIN"'
      );
    });

    it('should default isSystem to false', async () => {
      mockRoleRepository.findOne = jest.fn().mockResolvedValue(null);

      const createDto: CreateRoleDto = {
        name: 'Non System Role',
        code: 'NON_SYS',
      };

      await rolesService.create(mockTenantId, createDto, mockUserId);

      expect(mockRoleRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          isSystem: false,
        })
      );
    });
  });

  describe('update', () => {
    it('should update role basic fields', async () => {
      const updateDto: UpdateRoleDto = {
        name: 'Updated Role',
        description: 'Updated description',
        color: '#0000FF',
      };

      const result = await rolesService.update(mockRoleId, mockTenantId, updateDto, mockUserId);

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

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

      const result = await rolesService.update(
        'non-existent-id',
        mockTenantId,
        { name: 'Test' },
        mockUserId
      );

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

    it('should throw error when changing code to existing code', async () => {
      const existingRole = { ...mockRole, id: 'other-id' };
      mockRoleRepository.findOne = jest
        .fn()
        .mockResolvedValueOnce(mockRole) // findOne for update
        .mockResolvedValueOnce(existingRole); // findByCode check

      const updateDto: UpdateRoleDto = {
        code: 'EXISTING_CODE',
      };

      await expect(
        rolesService.update(mockRoleId, mockTenantId, updateDto, mockUserId)
      ).rejects.toThrow('Ya existe un rol con el codigo');
    });

    it('should restrict system role modifications', async () => {
      mockRoleRepository.findOne = jest.fn().mockResolvedValue(mockSystemRole);

      const updateDto: UpdateRoleDto = {
        name: 'Cannot Change Name',
        code: 'CANNOT_CHANGE',
      };

      await expect(
        rolesService.update(mockSystemRole.id!, mockTenantId, updateDto, mockUserId)
      ).rejects.toThrow('Los roles del sistema solo permiten modificar descripcion y color');
    });

    it('should allow description and color changes on system roles', async () => {
      mockRoleRepository.findOne = jest.fn().mockResolvedValue(mockSystemRole);
      mockRoleRepository.save = jest.fn().mockResolvedValue(mockSystemRole);

      const updateDto: UpdateRoleDto = {
        description: 'Updated system description',
        color: '#FFFFFF',
      };

      const result = await rolesService.update(
        mockSystemRole.id!,
        mockTenantId,
        updateDto,
        mockUserId
      );

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

    it('should update permissions when provided', async () => {
      const updateDto: UpdateRoleDto = {
        permissionIds: [mockPermissionId1],
      };

      await rolesService.update(mockRoleId, mockTenantId, updateDto, mockUserId);

      expect(mockPermissionRepository.findBy).toHaveBeenCalled();
      expect(mockRoleRepository.save).toHaveBeenCalled();
    });
  });

  describe('assignPermissions', () => {
    it('should assign permissions to role', async () => {
      const dto = { permissionIds: [mockPermissionId1, mockPermissionId2] };

      const result = await rolesService.assignPermissions(
        mockRoleId,
        mockTenantId,
        dto,
        mockUserId
      );

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

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

      const result = await rolesService.assignPermissions(
        'non-existent-id',
        mockTenantId,
        { permissionIds: [mockPermissionId1] },
        mockUserId
      );

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

    it('should replace all existing permissions', async () => {
      const dto = { permissionIds: [mockPermissionId1] };

      await rolesService.assignPermissions(mockRoleId, mockTenantId, dto, mockUserId);

      expect(mockPermissionRepository.findBy).toHaveBeenCalledWith({
        id: expect.anything(),
      });
    });
  });

  describe('addPermission', () => {
    it('should add a single permission to role', async () => {
      const newPermissionId = '550e8400-e29b-41d4-a716-446655440030';
      const roleWithoutPermission = { ...mockRole, permissions: [] };
      mockRoleRepository.findOne = jest.fn().mockResolvedValue(roleWithoutPermission);
      mockPermissionRepository.findOne = jest.fn().mockResolvedValue({
        id: newPermissionId,
        resource: 'settings',
        action: PermissionAction.READ,
      });

      const result = await rolesService.addPermission(
        mockRoleId,
        mockTenantId,
        newPermissionId,
        mockUserId
      );

      expect(mockPermissionRepository.findOne).toHaveBeenCalledWith({
        where: { id: newPermissionId },
      });
      expect(mockRoleRepository.save).toHaveBeenCalled();
      expect(result).toBeDefined();
    });

    it('should not add duplicate permission', async () => {
      // Role already has mockPermission1
      const result = await rolesService.addPermission(
        mockRoleId,
        mockTenantId,
        mockPermissionId1,
        mockUserId
      );

      expect(mockRoleRepository.save).not.toHaveBeenCalled();
      expect(result).toEqual(mockRole);
    });

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

      await expect(
        rolesService.addPermission(mockRoleId, mockTenantId, 'non-existent', mockUserId)
      ).rejects.toThrow('Permiso no encontrado');
    });

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

      const result = await rolesService.addPermission(
        'non-existent-id',
        mockTenantId,
        mockPermissionId1,
        mockUserId
      );

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

  describe('removePermission', () => {
    it('should remove permission from role', async () => {
      const result = await rolesService.removePermission(
        mockRoleId,
        mockTenantId,
        mockPermissionId1,
        mockUserId
      );

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

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

      const result = await rolesService.removePermission(
        'non-existent-id',
        mockTenantId,
        mockPermissionId1,
        mockUserId
      );

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

  describe('getPermissions', () => {
    it('should return permissions for role', async () => {
      const result = await rolesService.getPermissions(mockRoleId, mockTenantId);

      expect(result).toEqual(mockRole.permissions);
    });

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

      const result = await rolesService.getPermissions('non-existent-id', mockTenantId);

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

  describe('getUsers', () => {
    it('should return users for role', async () => {
      const mockUser: Partial<User> = {
        id: mockUserId,
        email: 'test@example.com',
      };
      const roleWithUsers = { ...mockRole, users: [mockUser] };
      mockRoleRepository.findOne = jest.fn().mockResolvedValue(roleWithUsers);

      const result = await rolesService.getUsers(mockRoleId, mockTenantId);

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

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

      const result = await rolesService.getUsers('non-existent-id', mockTenantId);

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

  describe('getUserCount', () => {
    it('should return user count for role', async () => {
      const mockUsers = [{ id: 'user1' }, { id: 'user2' }];
      const roleWithUsers = { ...mockRole, users: mockUsers };
      mockRoleRepository.findOne = jest.fn().mockResolvedValue(roleWithUsers);

      const result = await rolesService.getUserCount(mockRoleId, mockTenantId);

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

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

      const result = await rolesService.getUserCount('non-existent-id', mockTenantId);

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

  describe('getSystemRoles', () => {
    it('should return only system roles', async () => {
      mockRoleRepository.find = jest.fn().mockResolvedValue([mockSystemRole]);

      const result = await rolesService.getSystemRoles(mockTenantId);

      expect(mockRoleRepository.find).toHaveBeenCalledWith({
        where: { tenantId: mockTenantId, isSystem: true },
        relations: ['permissions'],
        order: { name: 'ASC' },
      });
      expect(result).toEqual([mockSystemRole]);
    });
  });

  describe('getCustomRoles', () => {
    it('should return only custom (non-system) roles', async () => {
      mockRoleRepository.find = jest.fn().mockResolvedValue([mockRole]);

      const result = await rolesService.getCustomRoles(mockTenantId);

      expect(mockRoleRepository.find).toHaveBeenCalledWith({
        where: { tenantId: mockTenantId, isSystem: false },
        relations: ['permissions'],
        order: { name: 'ASC' },
      });
      expect(result).toEqual([mockRole]);
    });
  });

  describe('cloneRole', () => {
    it('should clone role with new code and name', async () => {
      mockRoleRepository.findOne = jest
        .fn()
        .mockResolvedValueOnce(mockRole) // Source role
        .mockResolvedValueOnce(null); // No existing role with new code

      const result = await rolesService.cloneRole(
        mockRoleId,
        mockTenantId,
        'CLONED_ROLE',
        'Cloned Role',
        mockUserId
      );

      expect(mockRoleRepository.create).toHaveBeenCalledWith({
        tenantId: mockTenantId,
        name: 'Cloned Role',
        code: 'CLONED_ROLE',
        description: mockRole.description,
        isSystem: false, // Cloned roles are never system roles
        color: mockRole.color,
        permissions: mockRole.permissions,
        createdBy: mockUserId,
      });
      expect(mockRoleRepository.save).toHaveBeenCalled();
      expect(result).toBeDefined();
    });

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

      const result = await rolesService.cloneRole(
        'non-existent-id',
        mockTenantId,
        'NEW_CODE',
        'New Name',
        mockUserId
      );

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

    it('should throw error when new code already exists', async () => {
      mockRoleRepository.findOne = jest
        .fn()
        .mockResolvedValueOnce(mockRole) // Source role
        .mockResolvedValueOnce(mockRole); // Existing role with same code

      await expect(
        rolesService.cloneRole(mockRoleId, mockTenantId, 'ADMIN', 'Cloned Admin', mockUserId)
      ).rejects.toThrow('Ya existe un rol con el codigo "ADMIN"');
    });
  });

  describe('delete', () => {
    it('should soft delete a role', async () => {
      const result = await rolesService.delete(mockRoleId, mockTenantId);

      expect(mockRoleRepository.softDelete).toHaveBeenCalledWith(mockRoleId);
      expect(result).toBe(true);
    });

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

      const result = await rolesService.delete('non-existent-id', mockTenantId);

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

    it('should throw error when deleting system role', async () => {
      mockRoleRepository.findOne = jest.fn().mockResolvedValue(mockSystemRole);

      await expect(rolesService.delete(mockSystemRole.id!, mockTenantId)).rejects.toThrow(
        'No se pueden eliminar roles del sistema'
      );
    });

    it('should throw error when role has assigned users', async () => {
      const roleWithUsers = {
        ...mockRole,
        users: [{ id: 'user1' }],
      };
      mockRoleRepository.findOne = jest.fn().mockResolvedValue(roleWithUsers);

      await expect(rolesService.delete(mockRoleId, mockTenantId)).rejects.toThrow(
        'No se puede eliminar un rol que tiene usuarios asignados'
      );
    });
  });

  describe('Tenant Isolation', () => {
    it('should filter by tenantId in findAll', async () => {
      const params: RoleSearchParams = {
        tenantId: mockTenantId,
      };

      await rolesService.findAll(params);

      const callArgs = (mockRoleRepository.findAndCount as jest.Mock).mock.calls[0][0];
      expect(callArgs.where[0].tenantId).toBe(mockTenantId);
    });

    it('should include tenantId in create', async () => {
      mockRoleRepository.findOne = jest.fn().mockResolvedValue(null);

      const createDto: CreateRoleDto = {
        name: 'Tenant Role',
        code: 'TENANT_ROLE',
      };

      await rolesService.create(mockTenantId, createDto, mockUserId);

      expect(mockRoleRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          tenantId: mockTenantId,
        })
      );
    });

    it('should enforce tenantId in findOne', async () => {
      await rolesService.findOne(mockRoleId, mockTenantId);

      expect(mockRoleRepository.findOne).toHaveBeenCalledWith({
        where: { id: mockRoleId, tenantId: mockTenantId },
        relations: ['permissions', 'users'],
      });
    });

    it('should not access roles from different tenant', async () => {
      mockRoleRepository.findOne = jest.fn().mockResolvedValue(null);
      const differentTenantId = '550e8400-e29b-41d4-a716-446655440002';

      const result = await rolesService.findOne(mockRoleId, differentTenantId);

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