import { SendNotificationCommand } from '../commands/send-notification/send-notification.command';
import { SendNotificationHandler } from '../commands/send-notification/send-notification.handler';

describe('SendNotificationHandler', () => {
  let handler: SendNotificationHandler;
  let mockNotificationRepo: {
    create: ReturnType<typeof vi.fn>;
    updateStatus: ReturnType<typeof vi.fn>;
    findByUserId: ReturnType<typeof vi.fn>;
  };
  let mockPreferenceRepo: {
    findByUserId: ReturnType<typeof vi.fn>;
    isEnabled: ReturnType<typeof vi.fn>;
    upsert: ReturnType<typeof vi.fn>;
  };
  let mockEmailService: { send: ReturnType<typeof vi.fn> };
  let mockFcmService: { send: ReturnType<typeof vi.fn> };
  let mockStringeeSmsService: { sendNotification: ReturnType<typeof vi.fn>; isAvailable: boolean };
  let mockZaloOaService: { sendMessage: ReturnType<typeof vi.fn>; isAvailable: boolean };
  let mockTemplateService: { render: ReturnType<typeof vi.fn> };
  let mockEventBus: { publish: ReturnType<typeof vi.fn> };
  let mockLogger: { log: ReturnType<typeof vi.fn>; warn: ReturnType<typeof vi.fn>; error: ReturnType<typeof vi.fn> };

  const notificationEntity = {
    id: 'notif-1',
    userId: 'user-1',
    channel: 'EMAIL' as const,
    templateKey: 'user.registered',
    subject: 'Chào mừng!',
    body: '<p>Chào mừng!</p>',
    metadata: null,
    status: 'PENDING' as const,
    errorDetail: null,
    sentAt: null,
    createdAt: new Date(),
  };

  beforeEach(() => {
    mockNotificationRepo = {
      create: vi.fn().mockResolvedValue(notificationEntity),
      updateStatus: vi.fn().mockResolvedValue(undefined),
      findByUserId: vi.fn(),
    };
    mockPreferenceRepo = {
      findByUserId: vi.fn(),
      isEnabled: vi.fn().mockResolvedValue(true),
      upsert: vi.fn(),
    };
    mockEmailService = { send: vi.fn().mockResolvedValue({ messageId: 'msg-1' }) };
    mockFcmService = { send: vi.fn().mockResolvedValue('fcm-msg-1') };
    mockStringeeSmsService = {
      sendNotification: vi.fn().mockResolvedValue({ messageId: 'sms-msg-1' }),
      isAvailable: true,
    };
    mockZaloOaService = {
      sendMessage: vi.fn().mockResolvedValue({ messageId: 'zalo-msg-1' }),
      isAvailable: false,
    };
    mockTemplateService = {
      render: vi.fn().mockReturnValue({ subject: 'Chào mừng!', body: '<p>Chào mừng!</p>' }),
    };
    mockEventBus = { publish: vi.fn() };
    mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn() };

    handler = new SendNotificationHandler(
      mockNotificationRepo as any,
      mockPreferenceRepo as any,
      mockEmailService as any,
      mockFcmService as any,
      mockStringeeSmsService as any,
      mockZaloOaService as any,
      mockTemplateService as any,
      mockEventBus as any,
      mockLogger as any,
    );
  });

  it('sends email notification successfully', async () => {
    const command = new SendNotificationCommand(
      'user-1', 'EMAIL', 'user.registered', { phone: '0901234567', role: 'BUYER' }, 'test@test.com',
    );

    await handler.execute(command);

    expect(mockPreferenceRepo.isEnabled).toHaveBeenCalledWith('user-1', 'EMAIL', 'user.registered');
    expect(mockTemplateService.render).toHaveBeenCalledWith('user.registered', { phone: '0901234567', role: 'BUYER' });
    expect(mockNotificationRepo.create).toHaveBeenCalled();
    expect(mockEmailService.send).toHaveBeenCalledWith({
      to: 'test@test.com',
      subject: 'Chào mừng!',
      html: '<p>Chào mừng!</p>',
    });
    expect(mockNotificationRepo.updateStatus).toHaveBeenCalledWith('notif-1', 'SENT');
    expect(mockEventBus.publish).toHaveBeenCalled();
  });

  it('sends push notification successfully', async () => {
    const command = new SendNotificationCommand(
      'user-1', 'PUSH', 'user.registered', { phone: '0901234567', role: 'BUYER' }, 'fcm-token-abc',
    );

    await handler.execute(command);

    expect(mockFcmService.send).toHaveBeenCalledWith({
      token: 'fcm-token-abc',
      title: 'Chào mừng!',
      body: 'Chào mừng!', // HTML stripped
    });
    expect(mockNotificationRepo.updateStatus).toHaveBeenCalledWith('notif-1', 'SENT');
  });

  it('sends SMS notification via Stringee successfully', async () => {
    const command = new SendNotificationCommand(
      'user-1', 'SMS', 'user.registered', { phone: '0901234567', role: 'BUYER' }, '+84901234567',
    );

    await handler.execute(command);

    expect(mockStringeeSmsService.sendNotification).toHaveBeenCalledWith({
      to: '+84901234567',
      message: 'Chào mừng!', // HTML stripped
    });
    expect(mockNotificationRepo.updateStatus).toHaveBeenCalledWith('notif-1', 'SENT');
    expect(mockEventBus.publish).toHaveBeenCalled();
  });

  it('logs pending when Stringee SMS is not available', async () => {
    mockStringeeSmsService.isAvailable = false;

    const command = new SendNotificationCommand(
      'user-1', 'SMS', 'user.registered', {}, '+84901234567',
    );

    await handler.execute(command);

    expect(mockLogger.warn).toHaveBeenCalledWith(
      expect.stringContaining('Stringee is not configured'),
      'SendNotificationHandler',
    );
    expect(mockNotificationRepo.updateStatus).toHaveBeenCalledWith('notif-1', 'PENDING');
    expect(mockStringeeSmsService.sendNotification).not.toHaveBeenCalled();
  });

  it('marks notification as FAILED when SMS send throws', async () => {
    mockStringeeSmsService.sendNotification.mockRejectedValue(new Error('Stringee API error'));

    const command = new SendNotificationCommand(
      'user-1', 'SMS', 'user.registered', {}, '+84901234567',
    );

    await handler.execute(command);

    expect(mockNotificationRepo.updateStatus).toHaveBeenCalledWith('notif-1', 'FAILED', 'Stringee API error');
    expect(mockLogger.error).toHaveBeenCalled();
    expect(mockEventBus.publish).not.toHaveBeenCalled();
  });

  it('skips notification when user preference is disabled', async () => {
    mockPreferenceRepo.isEnabled.mockResolvedValue(false);

    const command = new SendNotificationCommand(
      'user-1', 'EMAIL', 'user.registered', {}, 'test@test.com',
    );

    await handler.execute(command);

    expect(mockNotificationRepo.create).not.toHaveBeenCalled();
    expect(mockEmailService.send).not.toHaveBeenCalled();
    expect(mockLogger.log).toHaveBeenCalled();
  });

  it('logs pending when Zalo OA is not available', async () => {
    const command = new SendNotificationCommand(
      'user-1', 'ZALO_OA', 'user.registered', {}, 'zalo-id',
    );

    await handler.execute(command);

    expect(mockLogger.warn).toHaveBeenCalledWith(
      expect.stringContaining('Zalo OA is not configured'),
      'SendNotificationHandler',
    );
    expect(mockNotificationRepo.updateStatus).toHaveBeenCalledWith('notif-1', 'PENDING');
    expect(mockZaloOaService.sendMessage).not.toHaveBeenCalled();
  });

  it('marks notification as FAILED when email send throws', async () => {
    mockEmailService.send.mockRejectedValue(new Error('SMTP connection refused'));

    const command = new SendNotificationCommand(
      'user-1', 'EMAIL', 'user.registered', {}, 'test@test.com',
    );

    await handler.execute(command);

    expect(mockNotificationRepo.updateStatus).toHaveBeenCalledWith('notif-1', 'FAILED', 'SMTP connection refused');
    expect(mockLogger.error).toHaveBeenCalled();
    expect(mockEventBus.publish).not.toHaveBeenCalled();
  });
});
