import { Test, TestingModule } from '@nestjs/testing';
import { ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { RolesGuard } from './roles.guard';
import { UserService } from './user.service';
import { ROLES_KEY } from './roles.decorator';
import { ForbiddenException } from '@nestjs/common';

describe('RolesGuard', () => {
  let guard: RolesGuard;
  let reflector: Reflector;
  let userService: UserService;

  // Mock user service
  const mockUserService = {
    findByUserId: jest.fn(),
  };

  // Mock reflector
  const mockReflector = {
    getAllAndOverride: jest.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        RolesGuard,
        { provide: Reflector, useValue: mockReflector },
        { provide: UserService, useValue: mockUserService },
      ],
    }).compile();

    guard = module.get<RolesGuard>(RolesGuard);
    reflector = module.get<Reflector>(Reflector);
    userService = module.get<UserService>(UserService);

    // Reset mocks before each test
    jest.clearAllMocks();
  });

  // Helper function to create a mock execution context
  function createMockExecutionContext(userId?: string): ExecutionContext {
    const mockExecutionContext = {
      getHandler: jest.fn(),
      getClass: jest.fn(),
      switchToHttp: jest.fn().mockReturnValue({
        getRequest: jest.fn().mockReturnValue({
          user: userId ? { userId } : undefined,
        }),
      }),
    } as unknown as ExecutionContext;

    return mockExecutionContext;
  }

  it('should be defined', () => {
    expect(guard).toBeDefined();
  });

  describe('canActivate', () => {
    it('should allow access when no roles are required', async () => {
      // Arrange
      mockReflector.getAllAndOverride.mockReturnValue(undefined);
      const context = createMockExecutionContext('user-123');

      // Act
      const result = await guard.canActivate(context);

      // Assert
      expect(result).toBe(true);
      expect(mockReflector.getAllAndOverride).toHaveBeenCalledWith(ROLES_KEY, [
        context.getHandler(),
        context.getClass(),
      ]);
      expect(mockUserService.findByUserId).not.toHaveBeenCalled();
    });

    it('should allow access when user has required role', async () => {
      // Arrange
      mockReflector.getAllAndOverride.mockReturnValue(['Manager', 'Team Lead']);
      const context = createMockExecutionContext('user-123');
      mockUserService.findByUserId.mockResolvedValue({
        id: 'user-123',
        role: 'Manager',
      });

      // Act
      const result = await guard.canActivate(context);

      // Assert
      expect(result).toBe(true);
      expect(mockReflector.getAllAndOverride).toHaveBeenCalledWith(ROLES_KEY, [
        context.getHandler(),
        context.getClass(),
      ]);
      expect(mockUserService.findByUserId).toHaveBeenCalledWith('user-123');
    });

    it('should handle case-insensitive role matching', async () => {
      // Arrange
      mockReflector.getAllAndOverride.mockReturnValue(['MANAGER', 'TEAM LEAD']);
      const context = createMockExecutionContext('user-123');
      mockUserService.findByUserId.mockResolvedValue({
        id: 'user-123',
        role: 'manager', // lowercase in DB
      });

      // Act
      const result = await guard.canActivate(context);

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

    it('should handle roles with extra whitespace', async () => {
      // Arrange
      mockReflector.getAllAndOverride.mockReturnValue([
        'Manager ',
        ' Team Lead',
      ]);
      const context = createMockExecutionContext('user-123');
      mockUserService.findByUserId.mockResolvedValue({
        id: 'user-123',
        role: 'Manager',
      });

      // Act
      const result = await guard.canActivate(context);

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

    it('should deny access when user does not have required role', async () => {
      // Arrange
      mockReflector.getAllAndOverride.mockReturnValue(['Manager', 'Team Lead']);
      const context = createMockExecutionContext('user-123');
      mockUserService.findByUserId.mockResolvedValue({
        id: 'user-123',
        role: 'Researcher',
      });

      // Act & Assert
      await expect(guard.canActivate(context)).rejects.toThrow(
        ForbiddenException,
      );
      expect(mockUserService.findByUserId).toHaveBeenCalledWith('user-123');
    });

    it('should throw ForbiddenException when no user ID is found in request', async () => {
      // Arrange
      mockReflector.getAllAndOverride.mockReturnValue(['Manager']);
      const context = createMockExecutionContext(); // No user ID

      // Act & Assert
      await expect(guard.canActivate(context)).rejects.toThrow(
        new ForbiddenException('No user ID found in request'),
      );
      expect(mockUserService.findByUserId).not.toHaveBeenCalled();
    });

    it('should throw ForbiddenException when user is not found', async () => {
      // Arrange
      mockReflector.getAllAndOverride.mockReturnValue(['Manager']);
      const context = createMockExecutionContext('user-123');
      mockUserService.findByUserId.mockResolvedValue(null); // User not found

      // Act & Assert
      await expect(guard.canActivate(context)).rejects.toThrow(
        new ForbiddenException('User not found'),
      );
      expect(mockUserService.findByUserId).toHaveBeenCalledWith('user-123');
    });

    it('should throw ForbiddenException with detailed error when role check fails', async () => {
      // Arrange
      mockReflector.getAllAndOverride.mockReturnValue(['Manager', 'Team Lead']);
      const context = createMockExecutionContext('user-123');
      mockUserService.findByUserId.mockResolvedValue({
        id: 'user-123',
        role: 'Researcher',
      });

      // Act & Assert
      await expect(guard.canActivate(context)).rejects.toThrow(
        new ForbiddenException(
          'Access denied. User role (researcher) is not in required roles: [Manager, Team Lead]',
        ),
      );
    });

    it('should handle undefined user role gracefully', async () => {
      // Arrange
      mockReflector.getAllAndOverride.mockReturnValue(['Manager']);
      const context = createMockExecutionContext('user-123');
      mockUserService.findByUserId.mockResolvedValue({
        id: 'user-123',
        // No role defined
      });

      // Act & Assert
      await expect(guard.canActivate(context)).rejects.toThrow(
        ForbiddenException,
      );
    });
  });
});
