import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from '../user.service';
import { HttpException } from '@nestjs/common';
import { getRepositoryToken } from '@nestjs/typeorm';
import { AuthService } from '../../auth/auth.service';
import { I18nContext, I18nService } from 'nestjs-i18n';
import { In, Repository } from 'typeorm';
import { User, Role } from '@app/models';
import * as crypto from 'crypto';

jest.mock('uuid', () => ({
  v7: jest.fn(() => 'mocked-uuid-v7'),
}));

describe('UserService', () => {
  let service: UserService;
  let userRepository: Repository<User>;
  let roleRepository: Repository<Role>;
  let authService: AuthService;
  let i18nService: I18nService;

  const mockPaginateMethod = jest.fn()
  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserService,
        {
          provide: getRepositoryToken(User),
          useClass: Repository,
        },
        {
          provide: getRepositoryToken(Role),
          useClass: Repository,
        },
        {
          provide: AuthService,
          useValue: {
            kickOut: jest.fn(),
          },
        },
        {
          provide: I18nService,
          useValue: {
            translate: jest.fn().mockReturnValue('translated message'),
          },
        },
      ],
    }).compile();

    jest.spyOn(I18nContext, 'current')
    .mockReturnValue({
      current: jest.fn().mockReturnValue({
        lang: ''
      })
    } as any)
    jest.mock('nestjs-typeorm-paginate', () => ({
      ...jest.requireActual('nestjs-typeorm-paginate'),
      paginate: mockPaginateMethod,
    }))
    service = module.get<UserService>(UserService);
    userRepository = module.get<Repository<User>>(getRepositoryToken(User));
    roleRepository = module.get<Repository<Role>>(getRepositoryToken(Role));
    authService = module.get<AuthService>(AuthService);
    i18nService = module.get<I18nService>(I18nService);
  });

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

  describe('create', () => {
    it('should return existing user if isInit is true and user exists', async () => {
      jest.spyOn(service, 'getUserInfo').mockResolvedValueOnce({ id: 1 } as User);
      const result = await service.create({ email: 'test@example.com', password: 'pwd', name: 'name' } as any, true);
      expect(result).toEqual({ id: 1 });
    });

    it('should throw an exception if user already exists', async () => {
      jest.spyOn(service, 'getUserInfo').mockResolvedValueOnce({ id: 1 } as User);
      await expect(service.create({ email: 'test@example.com' } as any, false)).rejects.toThrow(
        HttpException,
      );
    });

    it('should create and save a new user', async () => {
      jest.spyOn(service, 'getUserInfo').mockResolvedValueOnce(null);
      jest.spyOn(roleRepository, 'find').mockResolvedValueOnce([]);
      jest.spyOn(userRepository, 'create').mockReturnValue({} as User);
      jest.spyOn(userRepository, 'save').mockResolvedValueOnce({ id: 1 } as User);

      const result = await service.create({ email: 'test@example.com', password: 'pwd', name: 'name' } as any, false);
      expect(result).toEqual({ id: 1 });
    });

    it('should throw an exception if saving user fails', async () => {
      jest.spyOn(service, 'getUserInfo').mockResolvedValueOnce(null);
      jest.spyOn(roleRepository, 'find').mockResolvedValueOnce([]);
      jest.spyOn(userRepository, 'create').mockReturnValue({} as User);
      jest.spyOn(userRepository, 'save').mockRejectedValueOnce(new Error('Save failed'));

      await expect(service.create({ email: 'test@example.com' } as any, false)).rejects.toThrow(
        Error,
      );
    });
  });

  describe('deleteUser', () => {
    it('should delete a user if more than one user exists', async () => {
      jest.spyOn(userRepository, 'find').mockResolvedValueOnce([{ id: 1 }, { id: 2 }] as User[]);
      jest.spyOn(userRepository, 'findOne').mockResolvedValueOnce({ id: 1 } as User);
      jest.spyOn(userRepository, 'remove').mockResolvedValueOnce({} as User);

      const result = await service.deleteUser('test@example.com');
      expect(authService.kickOut).toHaveBeenCalledWith(1);
      expect(result).toBeDefined();
    });

    it('should throw an exception if only one user exists', async () => {
      jest.spyOn(userRepository, 'find').mockResolvedValueOnce([{ id: 1 }] as User[]);

      await expect(service.deleteUser('test@example.com')).rejects.toThrow(HttpException);
    });

    it('should do nothing if user does not exist', async () => {
      jest.spyOn(userRepository, 'find').mockResolvedValueOnce([{ id: 1 }, { id: 2 }] as User[]);
      jest.spyOn(userRepository, 'findOne').mockResolvedValueOnce(null);

      const result = await service.deleteUser('test@example.com');
      expect(result).toBeUndefined();
    });
  });

  describe('verifyPassword', () => {
    it('should return true if password matches', async () => {
      const password = 'password';
      const salt = 'salt';
      const hash = crypto.pbkdf2Sync(password, salt, 1000, 18, 'sha256').toString('hex');

      const result = await service.verifyPassword(password, hash, salt);
      expect(result).toBe(true);
    });

    it('should return false if password does not match', async () => {
      const result = await service.verifyPassword('password', 'wrongHash', 'salt');
      expect(result).toBe(false);
    });
  });

  describe('updatePwdUser', () => {
    it('should update password if old password is correct', async () => {
      const user = { email: 'test@example.com', password: 'oldHash', salt: 'salt' } as User;
      jest.spyOn(userRepository, 'findOne').mockResolvedValueOnce(user);
      jest.spyOn(service, 'verifyPassword').mockResolvedValueOnce(true);
      jest.spyOn(service, 'encry').mockResolvedValueOnce('newHash');
      jest.spyOn(userRepository, 'save').mockResolvedValueOnce(user);

      const result = await service.updatePwdUser({
        email: 'test@example.com',
        oldPassword: 'oldPassword',
        newPassword: 'newPassword',
        token: 'token',
      });
      expect(authService.kickOut).toHaveBeenCalled()
      expect(result).toBeUndefined();
    });

    it('should throw an exception if old password is incorrect', async () => {
      const user = { email: 'test@example.com', password: 'oldHash', salt: 'salt' } as User;
      jest.spyOn(userRepository, 'findOne').mockResolvedValueOnce(user);
      jest.spyOn(service, 'verifyPassword').mockResolvedValueOnce(false);

      await expect(
        service.updatePwdUser({
          email: 'test@example.com',
          oldPassword: 'wrongPassword',
          newPassword: 'newPassword',
          token: 'token',
        }),
      ).rejects.toThrow(HttpException);
    });
  });
  describe('getUserInfo', () => {
    it('should return user information with formatted dates if user exists', async () => {
      const mockUser = {
        id: 1,
        name: 'Test User',
        email: 'test@example.com',
        probationStart: new Date('2023-01-01'),
        probationEnd: new Date('2023-06-01'),
        protocolStart: new Date('2023-07-01'),
        protocolEnd: new Date('2023-12-01'),
        department: 'Engineering',
        employeeType: 'Full-time',
        address: '123 Test Street',
        status: 'Active',
      } as unknown as User;

      jest.spyOn(userRepository, 'findOne').mockResolvedValueOnce(mockUser);
      jest.spyOn(service, 'formatDateToDay').mockImplementation(async (date) => {
        return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
      });

      const result = await service.getUserInfo('test@example.com');
      expect(result).toEqual({
        ...mockUser,
        probationStart: '2023-01-01',
        probationEnd: '2023-06-01',
        protocolStart: '2023-07-01',
        protocolEnd: '2023-12-01',
      });
    });

    it('should return null if user does not exist', async () => {
      jest.spyOn(userRepository, 'findOne').mockResolvedValueOnce(null);

      const result = await service.getUserInfo('nonexistent@example.com');
      expect(result).toBeNull();
    });

    it('should call findOne with correct parameters', async () => {
      const email = 'test@example.com';
      const relations = ['role', 'role.permission'];

      jest.spyOn(userRepository, 'findOne').mockResolvedValueOnce(null);

      await service.getUserInfo(email, relations);
      expect(userRepository.findOne).toHaveBeenCalledWith({
        where: { email },
        select: [
          'id',
          'name',
          'email',
          'department',
          'employeeType',
          'protocolStart',
          'protocolEnd',
          'probationEnd',
          'probationStart',
          'probationDuration',
          'address',
          'status',
        ],
        relations,
      });
    });

    it('should not format dates if they are null', async () => {
      const mockUser = {
        id: 1,
        name: 'Test User',
        email: 'test@example.com',
        probationStart: null,
        probationEnd: null,
        protocolStart: null,
        protocolEnd: null,
        department: 'Engineering',
        employeeType: 'Full-time',
        address: '123 Test Street',
        status: 'Active',
      } as unknown as User;

      jest.spyOn(userRepository, 'findOne').mockResolvedValueOnce(mockUser);

      const result = await service.getUserInfo('test@example.com');
      expect(result).toEqual(mockUser);
    });
  });
  describe('getAllUser', () => {
    it('should return paginated user data with formatted dates', async () => {
      const mockUsers = [
        {
          id: 1,
          name: 'User 1',
          email: 'user1@example.com',
          probationStart: new Date('2023-01-01'),
          probationEnd: new Date('2023-06-01'),
          protocolStart: new Date('2023-07-01'),
          protocolEnd: new Date('2023-12-01'),
          department: 'Engineering',
          employeeType: 'Full-time',
          address: '123 Test Street',
          status: 'Active',
        },
        {
          id: 2,
          name: 'User 2',
          email: 'user2@example.com',
          probationStart: null,
          probationEnd: null,
          protocolStart: null,
          protocolEnd: null,
          department: 'HR',
          employeeType: 'Part-time',
          address: '456 Test Avenue',
          status: 'Inactive',
        },
      ];
      mockPaginateMethod.mockImplementationOnce(()=>{
        return Promise.resolve({items: mockUsers, meta: {}})
      })
      jest.spyOn(userRepository,'count').mockResolvedValue(2)
      jest.spyOn(userRepository, 'find').mockResolvedValueOnce([...mockUsers as any])
      jest.spyOn(userRepository, 'findAndCount').mockResolvedValueOnce([mockUsers as any, 2]);
      jest.spyOn(service, 'formatDateToDay').mockImplementation(async (date) => {
        return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
      });

      const result = await service.getAllUser({ page: 1, limit: 10 });
      expect(result.items).toEqual([
        {
          ...mockUsers[0],
          probationStart: '2023-01-01',
          probationEnd: '2023-06-01',
          protocolStart: '2023-07-01',
          protocolEnd: '2023-12-01',
        },
        mockUsers[1],
      ]);
      expect(result.meta.totalItems).toBe(2);
    });

    it('should filter users by name, role, and email', async () => {
      const mockUsers = [
        {
          id: 1,
          name: 'User 1',
          email: 'user1@example.com',
          department: 'Engineering',
          employeeType: 'Full-time',
          address: '123 Test Street',
          status: 'Active',
        },
      ];

      mockPaginateMethod.mockImplementationOnce(()=>{
        return Promise.resolve({items: mockUsers, meta: {}})
      })
      jest.spyOn(userRepository, 'count').mockResolvedValue(2)
      jest.spyOn(userRepository, 'find').mockResolvedValueOnce([...mockUsers as any])
      jest.spyOn(userRepository, 'findAndCount').mockResolvedValueOnce([mockUsers, 1] as any);

      const result = await service.getAllUser(
        { page: 1, limit: 10 },
        'User 1',
        [1],
        'user1@example.com',
      );
      expect(result.items).toEqual(mockUsers);
      expect(result.meta.totalItems).toBe(2);
    });

    it('should handle empty results gracefully', async () => {
      jest.spyOn(userRepository, 'findAndCount').mockResolvedValueOnce([[], 0]);
      jest.spyOn(userRepository, 'find').mockResolvedValueOnce([])
      mockPaginateMethod.mockImplementationOnce(()=>{
        return Promise.resolve({items: {}, meta: {}})
      })
      jest.spyOn(userRepository, 'count').mockResolvedValue(0)
      const result = await service.getAllUser({ page: 1, limit: 10 });
      expect(result.items).toEqual([]);
      expect(result.meta.totalItems).toBe(0);
    });
  });
  describe('getUserPermission', () => {
    it('should return a list of unique permissions for the user', async () => {
      const mockUser = {
        email: 'test@example.com',
        role: [
          {
            permission: [
              { name: 'read' },
              { name: 'write' },
            ],
          },
          {
            permission: [
              { name: 'write' },
              { name: 'delete' },
            ],
          },
        ],
      } as unknown as User;

      jest.spyOn(service, 'getUserInfo').mockResolvedValueOnce(mockUser);

      const result = await service.getUserPermission('token', mockUser);
      expect(result).toEqual(['read', 'write', 'delete']);
    });

    it('should return an empty array if the user has no roles or permissions', async () => {
      const mockUser = {
        email: 'test@example.com',
        role: [],
      } as unknown as User;

      jest.spyOn(service, 'getUserInfo').mockResolvedValueOnce(mockUser);

      const result = await service.getUserPermission('token', mockUser);
      expect(result).toEqual([]);
    });

    it('should handle null user information gracefully', async () => {
      jest.spyOn(service, 'getUserInfo').mockResolvedValueOnce(null);

      const result = await service.getUserPermission('token', {} as User);
      expect(result).toEqual([]);
    });

    it('should call getUserInfo with correct parameters', async () => {
      const mockUser = { email: 'test@example.com' } as User;
      const getUserInfoSpy = jest.spyOn(service, 'getUserInfo').mockResolvedValueOnce(null);

      await service.getUserPermission('token', mockUser);
      expect(getUserInfoSpy).toHaveBeenCalledWith('test@example.com', ['role', 'role.permission']);
    });
  });
  describe('updatePwdUser', () => {
    it('should update the password if the old password is correct', async () => {
      const mockUser = {
        id: 1,
        email: 'test@example.com',
        password: 'oldHash',
        salt: 'salt',
      } as User;

      jest.spyOn(userRepository, 'findOne').mockResolvedValueOnce(mockUser);
      jest.spyOn(service, 'verifyPassword').mockResolvedValueOnce(true);
      jest.spyOn(service, 'encry').mockResolvedValueOnce('newHash');
      jest.spyOn(userRepository, 'save').mockResolvedValueOnce(mockUser);

      const updatePwdUserDto = {
        email: 'test@example.com',
        oldPassword: 'oldPassword',
        newPassword: 'newPassword',
        token: 'token',
      };

      const result = await service.updatePwdUser(updatePwdUserDto);

      expect(service.verifyPassword).toHaveBeenCalledWith(
        'oldPassword',
        'oldHash',
        'salt',
      );
      expect(service.encry).toHaveBeenCalledWith('newPassword', 'salt');
      expect(userRepository.save).toHaveBeenCalledWith({
        ...mockUser,
        password: 'newHash',
      });
      expect(authService.kickOut).toHaveBeenCalledWith(1);
      expect(result).toBeUndefined();
    });

    it('should throw an exception if the old password is incorrect', async () => {
      const mockUser = {
        id: 1,
        email: 'test@example.com',
        password: 'oldHash',
        salt: 'salt',
      } as User;

      jest.spyOn(userRepository, 'findOne').mockResolvedValueOnce(mockUser);
      jest.spyOn(service, 'verifyPassword').mockResolvedValueOnce(false);
      jest.spyOn(userRepository, 'save').mockResolvedValueOnce(null);

      const updatePwdUserDto = {
        email: 'test@example.com',
        oldPassword: 'wrongPassword',
        newPassword: 'newPassword',
        token: 'token',
      };

      await expect(service.updatePwdUser(updatePwdUserDto)).rejects.toThrow(
        HttpException,
      );

      expect(service.verifyPassword).toHaveBeenCalledWith(
        'wrongPassword',
        'oldHash',
        'salt',
      );
      expect(userRepository.save).not.toHaveBeenCalled();
      expect(authService.kickOut).not.toHaveBeenCalled();
    });

    it('should throw an exception if the user does not exist', async () => {
      jest.spyOn(userRepository, 'findOne').mockResolvedValueOnce(null);
      jest.spyOn(userRepository, 'save').mockResolvedValue(null)
      jest.spyOn(service, 'verifyPassword').mockResolvedValue(true)
      jest.spyOn(authService, 'kickOut').mockResolvedValue(null)

      const updatePwdUserDto = {
        email: 'nonexistent@example.com',
        oldPassword: 'oldPassword',
        newPassword: 'newPassword',
        token: 'token',
      };

      await expect(service.updatePwdUser(updatePwdUserDto)).rejects.toThrow();

      expect(userRepository.findOne).toHaveBeenCalledWith({
        where: { email: 'nonexistent@example.com' },
        select: ['id', 'name', 'email', 'salt', 'password'],
      });
      expect(service.verifyPassword).not.toHaveBeenCalled();
      expect(userRepository.save).not.toHaveBeenCalled();
      expect(authService.kickOut).not.toHaveBeenCalled();
    });
  });
  describe('updatePwdAdmin', () => {
    it('should update the password and kick out the user', async () => {
      const mockUser = {
        id: 1,
        email: 'test@example.com',
        password: 'oldHash',
        salt: 'salt',
      } as User;

      jest.spyOn(userRepository, 'findOne').mockResolvedValueOnce(mockUser);
      jest.spyOn(service, 'encry').mockResolvedValueOnce('newHash');
      jest.spyOn(userRepository, 'save').mockResolvedValueOnce(mockUser);

      const updatePwdAdminDto = {
        email: 'test@example.com',
        newPassword: 'newPassword',
      };

      const result = await service.updatePwdAdmin(updatePwdAdminDto);

      expect(service.encry).toHaveBeenCalledWith('newPassword', 'salt');
      expect(userRepository.save).toHaveBeenCalledWith({
        ...mockUser,
        password: 'newHash',
      });
      expect(authService.kickOut).toHaveBeenCalledWith(1);
      expect(result).toBeUndefined();
    });

    it('should throw an exception if the user does not exist', async () => {
      jest.spyOn(userRepository, 'findOne').mockResolvedValueOnce(null);
      jest.spyOn(userRepository,'save').mockResolvedValue(null);

      const updatePwdAdminDto = {
        email: 'nonexistent@example.com',
        newPassword: 'newPassword',
      };

      await expect(service.updatePwdAdmin(updatePwdAdminDto)).rejects.toThrow();

      expect(userRepository.findOne).toHaveBeenCalledWith({
        where: { email: 'nonexistent@example.com' },
        select: ['id', 'name', 'email', 'salt', 'password'],
      });
      expect(userRepository.save).not.toHaveBeenCalled();
      expect(authService.kickOut).not.toHaveBeenCalled();
    });
  });
  describe('updateUserInfo', () => {
    it('should update user information and kick out the user if roles are changed', async () => {
      const mockUser = {
        id: 1,
        email: 'test@example.com',
        name: 'Old Name',
        department: 'Old Department',
        employeeType: 'Old Type',
        probationStart: new Date('2023-01-01'),
        probationEnd: new Date('2023-06-01'),
        probationDuration: 6,
        protocolStart: new Date('2023-07-01'),
        protocolEnd: new Date('2023-12-01'),
        address: 'Old Address',
        status: 'Inactive',
        role: [{ id: 1 }],
      } as unknown as User;

      const updatedRoles = [{ id: 2 }] as any;
      const updateUserDto: any = {
        email: 'test@example.com',
        name: 'New Name',
        department: 'New Department',
        employeeType: 'New Type',
        probationStart: new Date('2023-02-01'),
        probationEnd: new Date('2023-07-01'),
        probationDuration: 5,
        protocolStart: new Date('2023-08-01'),
        protocolEnd: new Date('2023-12-31'),
        address: 'New Address',
        status: 'Active',
        roleIds: [2],
      };

      jest.spyOn(service, 'getUserInfo').mockResolvedValueOnce(mockUser);
      jest.spyOn(roleRepository, 'find').mockResolvedValueOnce(updatedRoles as any);
      jest.spyOn(userRepository, 'save').mockResolvedValueOnce({
        ...mockUser,
        ...updateUserDto,
        role: updatedRoles,
      }) as any;
      jest.spyOn(authService, 'kickOut').mockResolvedValueOnce(undefined);

      const result = await service.updateUserInfo(updateUserDto as any);

      expect(service.getUserInfo).toHaveBeenCalledWith('test@example.com', ['role']);
      expect(roleRepository.find).toHaveBeenCalledWith({ where: { id: In([2]) } });
      expect(authService.kickOut).toHaveBeenCalledWith(1);
      expect(result).toEqual({
        ...mockUser,
        ...updateUserDto,
        role: updatedRoles,
      });
    });

    it('should update user information without kicking out the user if roles are unchanged', async () => {
      const mockUser = {
        id: 1,
        email: 'test@example.com',
        name: 'Old Name',
        department: 'Old Department',
        employeeType: 'Old Type',
        probationStart: new Date('2023-01-01'),
        probationEnd: new Date('2023-06-01'),
        probationDuration: 6,
        protocolStart: new Date('2023-07-01'),
        protocolEnd: new Date('2023-12-01'),
        address: 'Old Address',
        status: 'Inactive',
        role: [{ id: 1 }],
      } as unknown as User;

      const updateUserDto = {
        email: 'test@example.com',
        name: 'New Name',
        department: 'New Department',
        employeeType: 'New Type',
        probationStart: new Date('2023-02-01'),
        probationEnd: new Date('2023-07-01'),
        probationDuration: 5,
        protocolStart: new Date('2023-08-01'),
        protocolEnd: new Date('2023-12-31'),
        address: 'New Address',
        status: 'Active',
        roleIds: [1],
      };

      jest.spyOn(service, 'getUserInfo').mockResolvedValueOnce(mockUser);
      jest.spyOn(roleRepository, 'find').mockResolvedValueOnce(mockUser.role);
      jest.spyOn(userRepository, 'save').mockResolvedValueOnce({
        ...mockUser,
        ...updateUserDto,
      } as any);

      const result = await service.updateUserInfo(updateUserDto as any);

      expect(service.getUserInfo).toHaveBeenCalledWith('test@example.com', ['role']);
      expect(roleRepository.find).toHaveBeenCalledWith({ where: { id: In([1]) } });
      expect(authService.kickOut).not.toHaveBeenCalled();
      expect(result).toEqual({
        ...mockUser,
        ...updateUserDto,
      });
    });

    it('should throw an exception if the user does not exist', async () => {
      const updateUserDto = {
        email: 'nonexistent@example.com',
        name: 'New Name',
        department: 'New Department',
        employeeType: 'New Type',
        probationStart: new Date('2023-02-01'),
        probationEnd: new Date('2023-07-01'),
        probationDuration: 5,
        protocolStart: new Date('2023-08-01'),
        protocolEnd: new Date('2023-12-31'),
        address: 'New Address',
        status: 'Active',
        roleIds: [1],
      };

      jest.spyOn(userRepository, 'save').mockResolvedValueOnce(null);
      jest.spyOn(authService, 'kickOut').mockResolvedValueOnce(null);
      jest.spyOn(roleRepository, 'find').mockResolvedValueOnce(null);
      jest.spyOn(service, 'getUserInfo').mockResolvedValueOnce(null);

      await expect(service.updateUserInfo(updateUserDto as any)).rejects.toThrow();

      expect(service.getUserInfo).toHaveBeenCalledWith('nonexistent@example.com', ['role']);;
      expect(userRepository.save).not.toHaveBeenCalled();
      expect(authService.kickOut).not.toHaveBeenCalled();
    });
  });
});