import { UserEntity } from '../../domain/entities/user.entity';
import { EmailChangedEvent } from '../../domain/events/email-changed.event';
import { type IUserRepository } from '../../domain/repositories/user.repository';
import { Email } from '../../domain/value-objects/email.vo';
import { type HashedPassword } from '../../domain/value-objects/hashed-password.vo';
import { Phone } from '../../domain/value-objects/phone.vo';
import { VerifyEmailChangeCommand } from '../commands/verify-email-change/verify-email-change.command';
import { VerifyEmailChangeHandler } from '../commands/verify-email-change/verify-email-change.handler';

function createTestUser(overrides?: Partial<{ email: string; id: string }>): UserEntity {
  const phone = Phone.create('0912345678').unwrap();
  const pw = { value: 'hashed' } as HashedPassword;
  const email = overrides?.email ? Email.create(overrides.email).unwrap() : null;
  return new UserEntity(overrides?.id ?? 'user-1', {
    email,
    phone,
    passwordHash: pw,
    fullName: 'Nguyen Van A',
    avatarUrl: null,
    role: 'BUYER',
    kycStatus: 'NONE',
    kycData: null,
    isActive: true,
    totpSecret: null,
    totpEnabled: false,
    totpBackupCodes: [],
    totpEnabledAt: null,
  });
}

describe('VerifyEmailChangeHandler', () => {
  let handler: VerifyEmailChangeHandler;
  let mockUserRepo: { [K in keyof IUserRepository]: ReturnType<typeof vi.fn> };
  let mockRedis: { get: ReturnType<typeof vi.fn>; del: ReturnType<typeof vi.fn>; set: ReturnType<typeof vi.fn> };
  let mockCache: { invalidate: ReturnType<typeof vi.fn> };
  let mockEventBus: { publish: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockUserRepo = {
      findById: vi.fn(),
      findByPhone: vi.fn(),
      findByEmail: vi.fn(),
      save: vi.fn(),
      update: vi.fn(),
      updateMfaSecret: vi.fn(),
      updateMfaEnabled: vi.fn(),
      updateMfaDisabled: vi.fn(),
      updateBackupCodes: vi.fn(),
    };
    mockRedis = {
      get: vi.fn(),
      del: vi.fn().mockResolvedValue(undefined),
      set: vi.fn().mockResolvedValue(undefined),
    };
    mockCache = { invalidate: vi.fn().mockResolvedValue(undefined) };
    mockEventBus = { publish: vi.fn() };

    handler = new VerifyEmailChangeHandler(
      mockUserRepo as any,
      mockRedis as any,
      mockCache as any,
      mockEventBus as any,
      { error: vi.fn() } as any,
    );
  });

  it('verifies OTP and updates email', async () => {
    const user = createTestUser();
    const payload = JSON.stringify({ newEmail: 'new@example.com', code: '123456' });
    mockRedis.get.mockResolvedValue(payload);
    mockUserRepo.findById.mockResolvedValue(user);
    mockUserRepo.findByEmail.mockResolvedValue(null);
    mockUserRepo.update.mockResolvedValue(undefined);

    const command = new VerifyEmailChangeCommand('user-1', '123456');
    const result = await handler.execute(command);

    expect(result.email).toBe('new@example.com');
    expect(result.id).toBe('user-1');
    expect(mockRedis.del).toHaveBeenCalledWith('auth:email_change_otp:user-1');
    expect(mockUserRepo.update).toHaveBeenCalledWith(user);
    expect(mockCache.invalidate).toHaveBeenCalledWith(
      expect.stringContaining('user-1'),
    );
    expect(mockEventBus.publish).toHaveBeenCalledWith(expect.any(EmailChangedEvent));
    const published = mockEventBus.publish.mock.calls[0][0] as EmailChangedEvent;
    expect(published.aggregateId).toBe('user-1');
    expect(published.newEmail).toBe('new@example.com');
    expect(published.oldEmail).toBeNull();
  });

  it('throws ValidationException when OTP has expired', async () => {
    mockRedis.get.mockResolvedValue(null);

    const command = new VerifyEmailChangeCommand('user-1', '123456');
    await expect(handler.execute(command)).rejects.toThrow('hết hạn');
  });

  it('throws ValidationException when OTP code is wrong', async () => {
    const payload = JSON.stringify({ newEmail: 'new@example.com', code: '123456' });
    mockRedis.get.mockResolvedValue(payload);

    const command = new VerifyEmailChangeCommand('user-1', '999999');
    await expect(handler.execute(command)).rejects.toThrow('không đúng');
  });

  it('throws ConflictException when email was taken since OTP was issued', async () => {
    const user = createTestUser();
    const otherUser = createTestUser({ id: 'user-2', email: 'new@example.com' });
    const payload = JSON.stringify({ newEmail: 'new@example.com', code: '123456' });
    mockRedis.get.mockResolvedValue(payload);
    mockUserRepo.findById.mockResolvedValue(user);
    mockUserRepo.findByEmail.mockResolvedValue(otherUser);

    const command = new VerifyEmailChangeCommand('user-1', '123456');
    await expect(handler.execute(command)).rejects.toThrow('Email đã được sử dụng');

    // OTP should be cleaned up on conflict
    expect(mockRedis.del).toHaveBeenCalledWith('auth:email_change_otp:user-1');
  });

  it('throws NotFoundException when user does not exist', async () => {
    const payload = JSON.stringify({ newEmail: 'new@example.com', code: '123456' });
    mockRedis.get.mockResolvedValue(payload);
    mockUserRepo.findById.mockResolvedValue(null);

    const command = new VerifyEmailChangeCommand('user-1', '123456');
    await expect(handler.execute(command)).rejects.toThrow('Người dùng');
  });
});
