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 * as bcrypt from 'bcrypt';
import { UserService } from './user.service';
import { UserRepository } from './repositories/user.repository';
import { RoleEntity } from '../../entities/role.entity';
import { CLS_USER_ID } from '../../common/cls/cls.constants';

jest.mock('bcrypt');

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

  const mockUserRepo = {
    findAllWithRole: jest.fn(),
    findByIdWithRole: jest.fn(),
    findByEmail: jest.fn(),
    findById: jest.fn(),
    save: jest.fn(),
    update: jest.fn(),
    softDelete: jest.fn(),
  };

  const mockRoleRepo = {
    findOneBy: jest.fn(),
  };

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

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserService,
        { provide: UserRepository, useValue: mockUserRepo },
        { provide: getRepositoryToken(RoleEntity), useValue: mockRoleRepo },
        { provide: ClsService, useValue: mockCls },
      ],
    }).compile();

    service = module.get<UserService>(UserService);
  });

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

  describe('findAll', () => {
    it('should return all users mapped to list response', async () => {
      const users = [
        {
          id: '1', name: 'John', email: 'john@test.com', phone: null,
          role: { id: 'r1', name: 'Admin' },
          is_active: true, last_login_at: null, created_at: new Date(), updated_at: new Date(),
        },
      ];
      mockUserRepo.findAllWithRole.mockResolvedValue(users);

      const result = await service.findAll();

      expect(result).toHaveLength(1);
      expect(result[0].id).toBe('1');
      expect(result[0].role).toEqual({ id: 'r1', name: 'Admin' });
    });

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

  describe('findById', () => {
    it('should return user detail response when found', async () => {
      const user = {
        id: '1', name: 'John', email: 'john@test.com', phone: null,
        is_active: true, last_login_at: null,
        created_at: new Date(), updated_at: new Date(),
        created_by: null, updated_by: null,
        role: { id: 'r1', name: 'Admin', description: 'Full access', is_system: true, permissions: [] },
      };
      mockUserRepo.findByIdWithRole.mockResolvedValue(user);

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

      expect(result.id).toBe('1');
      expect(result.role.is_system).toBe(true);
    });

    it('should throw NotFoundException when user not found', async () => {
      mockUserRepo.findByIdWithRole.mockResolvedValue(null);

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

  describe('create', () => {
    const createDto = {
      name: 'John',
      email: 'john@test.com',
      password: 'Password123',
      role_id: 'r1',
    };

    it('should create user with hashed password and return success', async () => {
      mockUserRepo.findByEmail.mockResolvedValue(null);
      mockRoleRepo.findOneBy.mockResolvedValue({ id: 'r1' });
      (bcrypt.hash as jest.Mock).mockResolvedValue('hashed-pw');
      mockUserRepo.save.mockResolvedValue({ id: 'new-1' });

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

      expect(result).toEqual({ id: 'new-1', message: 'User created successfully' });
      expect(bcrypt.hash).toHaveBeenCalledWith('Password123', 10);
      expect(mockUserRepo.save).toHaveBeenCalledWith(
        expect.objectContaining({ password_hash: 'hashed-pw' }),
      );
    });

    it('should throw ConflictException when email already exists', async () => {
      mockUserRepo.findByEmail.mockResolvedValue({ id: 'existing' });

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

    it('should throw BadRequestException when role_id is invalid', async () => {
      mockUserRepo.findByEmail.mockResolvedValue(null);
      mockRoleRepo.findOneBy.mockResolvedValue(null);

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

  describe('update', () => {
    const existingUser = { id: '1', email: 'john@test.com', role: { id: 'r1' } };

    it('should update and return success response', async () => {
      mockUserRepo.findByIdWithRole.mockResolvedValue(existingUser);
      mockUserRepo.update.mockResolvedValue(undefined);

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

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

    it('should throw NotFoundException when user not found', async () => {
      mockUserRepo.findByIdWithRole.mockResolvedValue(null);

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

    it('should throw ConflictException when new email conflicts', async () => {
      mockUserRepo.findByIdWithRole.mockResolvedValue(existingUser);
      mockUserRepo.findByEmail.mockResolvedValue({ id: 'other-id' });

      await expect(
        service.update('1', { email: 'taken@test.com' } as any),
      ).rejects.toThrow(ConflictException);
    });

    it('should throw BadRequestException when new role_id is invalid', async () => {
      mockCls.get.mockReturnValue('different-user-id');
      mockUserRepo.findByIdWithRole.mockResolvedValue(existingUser);
      mockRoleRepo.findOneBy.mockResolvedValue(null);

      await expect(
        service.update('1', { role_id: 'bad-role' } as any),
      ).rejects.toThrow(BadRequestException);
    });

    it('should throw ForbiddenException when user tries to change own role', async () => {
      mockCls.get.mockReturnValue('1');

      await expect(
        service.update('1', { role_id: 'r2' } as any),
      ).rejects.toThrow(ForbiddenException);
    });
  });

  describe('resetPassword', () => {
    it('should hash new password and update user', async () => {
      mockUserRepo.findById.mockResolvedValue({ id: '1' });
      (bcrypt.hash as jest.Mock).mockResolvedValue('new-hashed-pw');
      mockUserRepo.update.mockResolvedValue(undefined);

      const result = await service.resetPassword('1', { new_password: 'NewPass123' } as any);

      expect(result).toEqual({ message: 'Password reset successfully' });
      expect(bcrypt.hash).toHaveBeenCalledWith('NewPass123', 10);
    });

    it('should throw NotFoundException when user not found', async () => {
      mockUserRepo.findById.mockResolvedValue(null);

      await expect(
        service.resetPassword('999', { new_password: 'x' } as any),
      ).rejects.toThrow(NotFoundException);
    });
  });

  describe('toggleStatus', () => {
    it('should toggle active status and return new state', async () => {
      mockCls.get.mockReturnValue('other-user-id');
      mockUserRepo.findById.mockResolvedValue({ id: '1', is_active: true });
      mockUserRepo.update.mockResolvedValue(undefined);

      const result = await service.toggleStatus('1');

      expect(result).toEqual({
        id: '1',
        is_active: false,
        message: 'User deactivated successfully',
      });
    });

    it('should throw ForbiddenException when toggling own status', async () => {
      mockCls.get.mockReturnValue('1');

      await expect(service.toggleStatus('1')).rejects.toThrow(ForbiddenException);
    });

    it('should throw NotFoundException when user not found', async () => {
      mockCls.get.mockReturnValue('other-user');
      mockUserRepo.findById.mockResolvedValue(null);

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

  describe('remove', () => {
    it('should soft delete and return success message', async () => {
      mockCls.get.mockReturnValue('other-user-id');
      mockUserRepo.findById.mockResolvedValue({ id: '1' });
      mockUserRepo.softDelete.mockResolvedValue(undefined);

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

      expect(result).toEqual({ message: 'User deleted successfully' });
      expect(mockUserRepo.softDelete).toHaveBeenCalledWith('1');
    });

    it('should throw ForbiddenException when deleting own account', async () => {
      mockCls.get.mockReturnValue('1');

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

    it('should throw NotFoundException when user not found', async () => {
      mockCls.get.mockReturnValue('other-user');
      mockUserRepo.findById.mockResolvedValue(null);

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