import { type IUserRepository } from '../../domain/repositories/user.repository';
import { RegisterUserCommand } from '../commands/register-user/register-user.command';
import { RegisterUserHandler } from '../commands/register-user/register-user.handler';

describe('RegisterUserHandler', () => {
  let handler: RegisterUserHandler;
  let mockUserRepo: { [K in keyof IUserRepository]: ReturnType<typeof vi.fn> };
  let mockTokenService: { generateTokenPair: ReturnType<typeof vi.fn> };
  let mockEventBus: { publish: ReturnType<typeof vi.fn> };

  const tokenPair = {
    accessToken: 'access-jwt',
    refreshToken: 'family.refresh-hex',
    expiresIn: 900,
  };

  beforeEach(() => {
    mockUserRepo = {
      findById: vi.fn(),
      findByPhone: vi.fn().mockResolvedValue(null),
      findByEmail: vi.fn().mockResolvedValue(null),
      save: vi.fn().mockResolvedValue(undefined),
      update: vi.fn(),
    };
    mockTokenService = { generateTokenPair: vi.fn().mockResolvedValue(tokenPair) };
    mockEventBus = { publish: vi.fn() };

    handler = new RegisterUserHandler(
      mockUserRepo as any,
      mockTokenService as any,
      mockEventBus as any,
    );
  });

  it('registers a new user and returns token pair', async () => {
    const command = new RegisterUserCommand('0912345678', 'StrongP@ss1', 'Nguyen Van A');
    const result = await handler.execute(command);

    expect(result).toEqual(tokenPair);
    expect(mockUserRepo.save).toHaveBeenCalledTimes(1);
    expect(mockTokenService.generateTokenPair).toHaveBeenCalledWith(
      expect.objectContaining({ phone: '+84912345678', role: 'BUYER' }),
    );
    expect(mockEventBus.publish).toHaveBeenCalled();
  });

  it('registers user with optional email', async () => {
    const command = new RegisterUserCommand(
      '0912345678', 'StrongP@ss1', 'Nguyen Van A', 'test@example.com',
    );
    const result = await handler.execute(command);

    expect(result).toEqual(tokenPair);
    expect(mockUserRepo.findByEmail).toHaveBeenCalledWith('test@example.com');
    expect(mockUserRepo.save).toHaveBeenCalledTimes(1);
  });

  it('throws ConflictException when phone already exists', async () => {
    mockUserRepo.findByPhone.mockResolvedValue({ id: 'existing-user' });

    const command = new RegisterUserCommand('0912345678', 'StrongP@ss1', 'Nguyen Van A');

    await expect(handler.execute(command)).rejects.toThrow('Số điện thoại đã được đăng ký');
  });

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

    const command = new RegisterUserCommand(
      '0912345678', 'StrongP@ss1', 'Nguyen Van A', 'taken@example.com',
    );

    await expect(handler.execute(command)).rejects.toThrow('Email đã được đăng ký');
  });

  it('throws ValidationException for invalid phone number', async () => {
    const command = new RegisterUserCommand('12345', 'StrongP@ss1', 'Nguyen Van A');

    await expect(handler.execute(command)).rejects.toThrow();
  });

  it('throws ValidationException for invalid email format', async () => {
    const command = new RegisterUserCommand(
      '0912345678', 'StrongP@ss1', 'Nguyen Van A', 'not-an-email',
    );

    await expect(handler.execute(command)).rejects.toThrow();
  });

  it('publishes UserRegisteredEvent after saving', async () => {
    const command = new RegisterUserCommand('0912345678', 'StrongP@ss1', 'Nguyen Van A');
    await handler.execute(command);

    expect(mockEventBus.publish).toHaveBeenCalledWith(
      expect.objectContaining({ eventName: 'user.registered' }),
    );
  });

  it('does not save user if phone validation fails', async () => {
    const command = new RegisterUserCommand('invalid', 'StrongP@ss1', 'Nguyen Van A');

    await expect(handler.execute(command)).rejects.toThrow();
    expect(mockUserRepo.save).not.toHaveBeenCalled();
  });
});
