import { Test, TestingModule } from '@nestjs/testing';
import {
  ConflictException,
  NotFoundException,
  BadRequestException,
  ForbiddenException,
} from '@nestjs/common';
import { getRepositoryToken } from '@nestjs/typeorm';
import { ClsService } from 'nestjs-cls';
import { RoleService } from './role.service';
import { RoleRepository } from './repositories/role.repository';
import { RolePermissionEntity } from '../../entities/role-permission.entity';
import { UserEntity } from '../../entities/user.entity';
import { RedisService } from '../../redis/redis.service';
import { CLS_TENANT_ID, CLS_USER_ID } from '../../common/cls/cls.constants';

describe('RoleService', () => {
  let service: RoleService;

  const mockRoleRepo = {
    findAllWithPermissions: jest.fn(),
    findByIdWithPermissions: jest.fn(),
    findByName: jest.fn(),
    save: jest.fn(),
    update: jest.fn(),
    softDelete: jest.fn(),
  };

  const mockRolePermRepo = {
    createQueryBuilder: jest.fn(),
    save: jest.fn(),
  };

  const mockUserRepo = {
    findOne: jest.fn(),
  };

  const mockRedisService = {
    invalidateByPattern: jest.fn(),
  };

  const mockCls = {
    get: jest.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        RoleService,
        { provide: RoleRepository, useValue: mockRoleRepo },
        { provide: getRepositoryToken(RolePermissionEntity), useValue: mockRolePermRepo },
        { provide: getRepositoryToken(UserEntity), useValue: mockUserRepo },
        { provide: RedisService, useValue: mockRedisService },
        { provide: ClsService, useValue: mockCls },
      ],
    }).compile();

    service = module.get<RoleService>(RoleService);
    mockCls.get.mockImplementation((key: string) => {
      if (key === CLS_TENANT_ID) return 'tenant-1';
      if (key === CLS_USER_ID) return 'user-1';
      return null;
    });
  });

  afterEach(() => jest.clearAllMocks());

  describe('findAll', () => {
    it('should return all roles mapped to response', async () => {
      const roles = [
        { id: 'r1', name: 'Admin', description: 'Full access', is_system: true, created_at: new Date(), updated_at: new Date(), permissions: [] },
      ];
      mockRoleRepo.findAllWithPermissions.mockResolvedValue(roles);

      const result = await service.findAll();

      expect(result).toHaveLength(1);
      expect(result[0].name).toBe('Admin');
    });

    it('should return empty array when no roles exist', async () => {
      mockRoleRepo.findAllWithPermissions.mockResolvedValue([]);
      const result = await service.findAll();
      expect(result).toEqual([]);
    });
  });

  describe('findById', () => {
    it('should return role response when found', async () => {
      const role = { id: 'r1', name: 'Admin', description: null, is_system: true, created_at: new Date(), updated_at: new Date(), permissions: [] };
      mockRoleRepo.findByIdWithPermissions.mockResolvedValue(role);

      const result = await service.findById('r1');

      expect(result.id).toBe('r1');
    });

    it('should throw NotFoundException when role not found', async () => {
      mockRoleRepo.findByIdWithPermissions.mockResolvedValue(null);

      await expect(service.findById('999')).rejects.toThrow(NotFoundException);
    });
  });

  describe('create', () => {
    const createDto = { name: 'Custom Role', description: 'Custom' };

    it('should create role and return success', async () => {
      mockRoleRepo.findByName.mockResolvedValue(null);
      mockRoleRepo.save.mockResolvedValue({ id: 'new-r1' });

      const result = await service.create(createDto as any);

      expect(result).toEqual({ id: 'new-r1', message: 'Role created successfully' });
      expect(mockRoleRepo.save).toHaveBeenCalledWith(
        expect.objectContaining({ name: 'Custom Role', is_system: false }),
      );
    });

    it('should throw ConflictException when role name already exists', async () => {
      mockRoleRepo.findByName.mockResolvedValue({ id: 'existing' });

      await expect(service.create(createDto as any)).rejects.toThrow(ConflictException);
    });
  });

  describe('update', () => {
    const existingRole = { id: 'r1', name: 'Custom', permissions: [] };

    it('should update and return success response', async () => {
      mockRoleRepo.findByIdWithPermissions.mockResolvedValue(existingRole);
      mockRoleRepo.update.mockResolvedValue(undefined);

      const result = await service.update('r1', { description: 'Updated' } as any);

      expect(result).toEqual({ id: 'r1', message: 'Role updated successfully' });
    });

    it('should throw NotFoundException when role not found', async () => {
      mockRoleRepo.findByIdWithPermissions.mockResolvedValue(null);

      await expect(service.update('999', {} as any)).rejects.toThrow(NotFoundException);
    });

    it('should throw ConflictException when new name conflicts', async () => {
      mockRoleRepo.findByIdWithPermissions.mockResolvedValue(existingRole);
      mockRoleRepo.findByName.mockResolvedValue({ id: 'other-id' });

      await expect(
        service.update('r1', { name: 'Taken Name' } as any),
      ).rejects.toThrow(ConflictException);
    });
  });

  describe('remove', () => {
    it('should soft delete role and invalidate cache', async () => {
      mockUserRepo.findOne.mockResolvedValue({ role_id: 'different-role' });
      mockRoleRepo.findByIdWithPermissions.mockResolvedValue({ id: 'r1', is_system: false });
      mockRoleRepo.softDelete.mockResolvedValue(undefined);
      mockRedisService.invalidateByPattern.mockResolvedValue(undefined);

      const result = await service.remove('r1');

      expect(result).toEqual({ message: 'Role deleted successfully' });
      expect(mockRedisService.invalidateByPattern).toHaveBeenCalledWith('perm:role:tenant-1:r1');
    });

    it('should throw NotFoundException when role not found', async () => {
      mockUserRepo.findOne.mockResolvedValue({ role_id: 'different-role' });
      mockRoleRepo.findByIdWithPermissions.mockResolvedValue(null);

      await expect(service.remove('999')).rejects.toThrow(NotFoundException);
    });

    it('should throw BadRequestException when deleting a system role', async () => {
      mockUserRepo.findOne.mockResolvedValue({ role_id: 'different-role' });
      mockRoleRepo.findByIdWithPermissions.mockResolvedValue({ id: 'r1', is_system: true });

      await expect(service.remove('r1')).rejects.toThrow(BadRequestException);
    });

    it('should throw ForbiddenException when deleting own role', async () => {
      mockUserRepo.findOne.mockResolvedValue({ role_id: 'r1' });

      await expect(service.remove('r1')).rejects.toThrow(ForbiddenException);
    });
  });

  describe('assignPermissions', () => {
    it('should replace permissions and invalidate cache', async () => {
      mockUserRepo.findOne.mockResolvedValue({ role_id: 'different-role' });
      mockRoleRepo.findByIdWithPermissions.mockResolvedValue({ id: 'r1', permissions: [] });

      const mockQb = { delete: jest.fn().mockReturnThis(), where: jest.fn().mockReturnThis(), execute: jest.fn().mockResolvedValue(undefined) };
      mockRolePermRepo.createQueryBuilder.mockReturnValue(mockQb);
      mockRolePermRepo.save.mockResolvedValue([]);
      mockRedisService.invalidateByPattern.mockResolvedValue(undefined);

      const dto = {
        permissions: [
          { module: 'users', access_level: 'view' },
        ],
      };

      const result = await service.assignPermissions('r1', dto as any);

      expect(result).toEqual({ message: 'Permissions assigned successfully' });
      expect(mockRedisService.invalidateByPattern).toHaveBeenCalled();
    });

    it('should throw ForbiddenException when modifying own role permissions', async () => {
      mockUserRepo.findOne.mockResolvedValue({ role_id: 'r1' });

      await expect(
        service.assignPermissions('r1', { permissions: [] } as any),
      ).rejects.toThrow(ForbiddenException);
    });

    it('should throw NotFoundException when role not found', async () => {
      mockUserRepo.findOne.mockResolvedValue({ role_id: 'different-role' });
      mockRoleRepo.findByIdWithPermissions.mockResolvedValue(null);

      await expect(
        service.assignPermissions('999', { permissions: [] } as any),
      ).rejects.toThrow(NotFoundException);
    });

    it('should throw BadRequestException for invalid module name', async () => {
      mockUserRepo.findOne.mockResolvedValue({ role_id: 'different-role' });
      mockRoleRepo.findByIdWithPermissions.mockResolvedValue({ id: 'r1' });

      const dto = {
        permissions: [{ module: 'invalid_module', access_level: 'view' }],
      };

      await expect(
        service.assignPermissions('r1', dto as any),
      ).rejects.toThrow(BadRequestException);
    });
  });

  describe('getPermissionsMatrix', () => {
    it('should return full module matrix with existing permissions filled', async () => {
      const role = {
        id: 'r1',
        permissions: [{ module: 'users', access_level: 'edit' }],
      };
      mockRoleRepo.findByIdWithPermissions.mockResolvedValue(role);

      const result = await service.getPermissionsMatrix('r1');

      expect(Array.isArray(result)).toBe(true);
      // Each module should have an entry
      const usersEntry = result.find((r: any) => r.module === 'users');
      expect(usersEntry?.access_level).toBe('edit');
    });

    it('should throw NotFoundException when role not found', async () => {
      mockRoleRepo.findByIdWithPermissions.mockResolvedValue(null);

      await expect(service.getPermissionsMatrix('999')).rejects.toThrow(NotFoundException);
    });
  });

  describe('invalidateAllTenantPermissions', () => {
    it('should call redis invalidateByPattern with tenant wildcard', async () => {
      mockRedisService.invalidateByPattern.mockResolvedValue(undefined);

      await service.invalidateAllTenantPermissions();

      expect(mockRedisService.invalidateByPattern).toHaveBeenCalledWith('perm:role:tenant-1:*');
    });
  });
});
