import { UsersService } from '../services/users.service';
import { type UsersRepository } from '../repositories/users.repository';
import { type RabbitMQService } from '@claw/shared-rabbitmq';
import { EventPattern } from '@claw/shared-types';
import { DuplicateEntityException, EntityNotFoundException } from '../../../common/errors';
import { UserRole, UserStatus } from '../../../common/enums';
import { validatePasswordStrength } from '../service.utilities/password-policy.utility';

const mockUser = {
  id: 'user-1',
  email: 'test@example.com',
  username: 'testuser',
  passwordHash: 'hashed',
  role: UserRole.VIEWER,
  status: UserStatus.ACTIVE,
  mustChangePassword: false,
  languagePreference: 'EN' as const,
  appearancePreference: 'SYSTEM' as const,
  createdAt: new Date(),
  updatedAt: new Date(),
};

const mockRepository = (): Record<keyof UsersRepository, jest.Mock> => ({
  create: jest.fn(),
  findById: jest.fn(),
  findByEmail: jest.fn(),
  findByUsername: jest.fn(),
  findAll: jest.fn(),
  updateById: jest.fn(),
  deleteById: jest.fn(),
  countAll: jest.fn(),
  updatePreferences: jest.fn(),
});

const mockRabbitMQ = (): Partial<Record<keyof RabbitMQService, jest.Mock>> => ({
  publish: jest.fn().mockResolvedValue(void 0),
});

describe('UsersService', () => {
  let service: UsersService;
  let repository: ReturnType<typeof mockRepository>;
  let rabbitMQ: ReturnType<typeof mockRabbitMQ>;

  beforeEach(() => {
    repository = mockRepository();
    rabbitMQ = mockRabbitMQ();
    service = new UsersService(
      repository as unknown as UsersRepository,
      rabbitMQ as unknown as RabbitMQService,
    );
  });

  describe('updateUser', () => {
    it('should update user successfully', async () => {
      const updatedUser = { ...mockUser, email: 'new@example.com' };
      repository.findById.mockResolvedValue(mockUser);
      repository.findByEmail.mockResolvedValue(null);
      repository.updateById.mockResolvedValue(updatedUser);

      const result = await service.updateUser('user-1', { email: 'new@example.com' }, 'admin-1');

      expect(result.email).toBe('new@example.com');
      expect(repository.updateById).toHaveBeenCalledWith('user-1', {
        email: 'new@example.com',
        username: undefined,
        role: undefined,
        status: undefined,
      });
    });

    it('should throw EntityNotFoundException if user not found', async () => {
      repository.findById.mockResolvedValue(null);

      await expect(
        service.updateUser('nonexistent', { email: 'new@example.com' }, 'admin-1'),
      ).rejects.toThrow(EntityNotFoundException);
    });

    it('should throw DuplicateEntityException if email already taken', async () => {
      repository.findById.mockResolvedValue(mockUser);
      repository.findByEmail.mockResolvedValue({ ...mockUser, id: 'other-user' });

      await expect(
        service.updateUser('user-1', { email: 'taken@example.com' }, 'admin-1'),
      ).rejects.toThrow(DuplicateEntityException);
    });
  });

  describe('deactivateUser', () => {
    it('should deactivate user and publish event', async () => {
      const deactivated = { ...mockUser, status: UserStatus.SUSPENDED };
      repository.findById.mockResolvedValue(mockUser);
      repository.updateById.mockResolvedValue(deactivated);

      const result = await service.deactivateUser('user-1', 'admin-1');

      expect(result.status).toBe(UserStatus.SUSPENDED);
      expect(repository.updateById).toHaveBeenCalledWith('user-1', {
        status: UserStatus.SUSPENDED,
      });
      expect(rabbitMQ.publish).toHaveBeenCalledWith(
        EventPattern.USER_DEACTIVATED,
        expect.objectContaining({
          userId: 'user-1',
          deactivatedBy: 'admin-1',
        }),
      );
    });

    it('should throw EntityNotFoundException if user not found', async () => {
      repository.findById.mockResolvedValue(null);

      await expect(service.deactivateUser('nonexistent', 'admin-1')).rejects.toThrow(
        EntityNotFoundException,
      );
    });
  });

  describe('reactivateUser', () => {
    it('should reactivate user', async () => {
      const suspendedUser = { ...mockUser, status: UserStatus.SUSPENDED };
      const reactivated = { ...mockUser, status: UserStatus.ACTIVE };
      repository.findById.mockResolvedValue(suspendedUser);
      repository.updateById.mockResolvedValue(reactivated);

      const result = await service.reactivateUser('user-1', 'admin-1');

      expect(result.status).toBe(UserStatus.ACTIVE);
      expect(repository.updateById).toHaveBeenCalledWith('user-1', {
        status: UserStatus.ACTIVE,
      });
    });

    it('should throw EntityNotFoundException if user not found', async () => {
      repository.findById.mockResolvedValue(null);

      await expect(service.reactivateUser('nonexistent', 'admin-1')).rejects.toThrow(
        EntityNotFoundException,
      );
    });
  });

  describe('changeRole', () => {
    it('should change role and publish event', async () => {
      const updated = { ...mockUser, role: UserRole.OPERATOR };
      repository.findById.mockResolvedValue(mockUser);
      repository.updateById.mockResolvedValue(updated);

      const result = await service.changeRole('user-1', UserRole.OPERATOR, 'admin-1');

      expect(result.role).toBe(UserRole.OPERATOR);
      expect(rabbitMQ.publish).toHaveBeenCalledWith(
        EventPattern.USER_ROLE_CHANGED,
        expect.objectContaining({
          userId: 'user-1',
          previousRole: UserRole.VIEWER,
          newRole: UserRole.OPERATOR,
          changedBy: 'admin-1',
        }),
      );
    });

    it('should throw EntityNotFoundException if user not found', async () => {
      repository.findById.mockResolvedValue(null);

      await expect(service.changeRole('nonexistent', UserRole.ADMIN, 'admin-1')).rejects.toThrow(
        EntityNotFoundException,
      );
    });
  });

  describe('create', () => {
    it('rejects weak password with WEAK_PASSWORD code', async () => {
      await expect(
        service.create({
          email: 'a@b.c',
          username: 'a',
          password: 'weak',
          role: UserRole.OPERATOR,
        } as never),
      ).rejects.toMatchObject({ code: 'WEAK_PASSWORD' });
    });

    it('rejects duplicate email with DuplicateEntityException', async () => {
      repository.findByEmail.mockResolvedValue(mockUser);
      await expect(
        service.create({
          email: mockUser.email,
          username: 'newname',
          password: 'StrongPass1',
          role: UserRole.OPERATOR,
        } as never),
      ).rejects.toThrow(DuplicateEntityException);
    });

    it('rejects duplicate username with DuplicateEntityException', async () => {
      repository.findByEmail.mockResolvedValue(null);
      repository.findByUsername.mockResolvedValue(mockUser);
      await expect(
        service.create({
          email: 'new@b.c',
          username: mockUser.username,
          password: 'StrongPass1',
          role: UserRole.OPERATOR,
        } as never),
      ).rejects.toThrow(DuplicateEntityException);
    });

    it('creates user, persists hash, and publishes USER_CREATED', async () => {
      repository.findByEmail.mockResolvedValue(null);
      repository.findByUsername.mockResolvedValue(null);
      repository.create.mockResolvedValue({
        ...mockUser,
        id: 'new-1',
        email: 'new@b.c',
        username: 'newuser',
        role: UserRole.OPERATOR,
      });

      const result = await service.create({
        email: 'new@b.c',
        username: 'newuser',
        password: 'StrongPass1',
        role: UserRole.OPERATOR,
      } as never);

      expect(result.id).toBe('new-1');
      expect(repository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          email: 'new@b.c',
          username: 'newuser',
          role: UserRole.OPERATOR,
          status: 'ACTIVE',
        }),
      );
      const calledArgs = repository.create.mock.calls[0]?.[0];
      expect(calledArgs.passwordHash).not.toBe('StrongPass1'); // must be hashed
      expect(rabbitMQ.publish).toHaveBeenCalledWith(
        EventPattern.USER_CREATED,
        expect.objectContaining({ userId: 'new-1', email: 'new@b.c' }),
      );
    });
  });

  describe('findById', () => {
    it('returns SafeUser when found', async () => {
      repository.findById.mockResolvedValue(mockUser);
      const result = await service.findById('user-1');
      expect(result.id).toBe('user-1');
      expect((result as unknown as Record<string, unknown>).passwordHash).toBeUndefined();
    });

    it('throws EntityNotFoundException when not found', async () => {
      repository.findById.mockResolvedValue(null);
      await expect(service.findById('nope')).rejects.toThrow(EntityNotFoundException);
    });
  });

  describe('findAll', () => {
    it('paginates results and computes totalPages', async () => {
      repository.findAll.mockResolvedValue({ users: [mockUser], total: 25 });
      const result = await service.findAll({
        page: 2,
        limit: 10,
        search: 'foo',
        role: UserRole.OPERATOR,
        status: UserStatus.ACTIVE,
        sortBy: 'createdAt',
        sortOrder: 'desc',
      } as never);
      expect(result.meta.total).toBe(25);
      expect(result.meta.page).toBe(2);
      expect(result.meta.totalPages).toBe(3);
      expect(repository.findAll).toHaveBeenCalledWith(
        expect.objectContaining({ skip: 10, take: 10 }),
      );
    });
  });

  describe('updatePreferences', () => {
    it('throws when user not found', async () => {
      repository.findById.mockResolvedValue(null);
      await expect(
        service.updatePreferences('nope', { languagePreference: 'EN' as never }),
      ).rejects.toThrow(EntityNotFoundException);
    });

    it('returns SafeUser on success', async () => {
      repository.findById.mockResolvedValue(mockUser);
      repository.updatePreferences.mockResolvedValue({
        ...mockUser,
        languagePreference: 'FR' as const,
      });
      const result = await service.updatePreferences('user-1', {
        languagePreference: 'FR' as never,
      });
      expect(result.languagePreference).toBe('FR');
    });
  });

  describe('changePassword', () => {
    it('throws when user not found', async () => {
      repository.findById.mockResolvedValue(null);
      await expect(
        service.changePassword('nope', { currentPassword: 'x', newPassword: 'StrongPass1' }),
      ).rejects.toThrow(EntityNotFoundException);
    });

    it('throws when current password is wrong', async () => {
      repository.findById.mockResolvedValue({
        ...mockUser,
        passwordHash: '$argon2id$v=19$m=65536,t=3,p=4$invalid$invalid',
      });
      await expect(
        service.changePassword('user-1', {
          currentPassword: 'wrong',
          newPassword: 'StrongPass1',
        }),
      ).rejects.toThrow();
    });
  });
});

describe('Password Policy', () => {
  it('should accept a valid password', () => {
    const result = validatePasswordStrength('StrongPass1');
    expect(result.valid).toBe(true);
    expect(result.errors).toHaveLength(0);
  });

  it('should reject password shorter than 8 characters', () => {
    const result = validatePasswordStrength('Ab1');
    expect(result.valid).toBe(false);
    expect(result.errors).toContain('Password must be at least 8 characters');
  });

  it('should reject password longer than 128 characters', () => {
    const longPass = `Aa1${'a'.repeat(126)}`;
    const result = validatePasswordStrength(longPass);
    expect(result.valid).toBe(false);
    expect(result.errors).toContain('Password must be at most 128 characters');
  });

  it('should reject password without uppercase letter', () => {
    const result = validatePasswordStrength('lowercase1');
    expect(result.valid).toBe(false);
    expect(result.errors).toContain('Password must contain an uppercase letter');
  });

  it('should reject password without lowercase letter', () => {
    const result = validatePasswordStrength('UPPERCASE1');
    expect(result.valid).toBe(false);
    expect(result.errors).toContain('Password must contain a lowercase letter');
  });

  it('should reject password without number', () => {
    const result = validatePasswordStrength('NoNumberHere');
    expect(result.valid).toBe(false);
    expect(result.errors).toContain('Password must contain a number');
  });

  it('should return multiple errors for very weak password', () => {
    const result = validatePasswordStrength('abc');
    expect(result.valid).toBe(false);
    expect(result.errors.length).toBeGreaterThan(1);
  });
});
