import { Types } from 'mongoose';
import { ChatService } from './chat.service';

describe('ChatService realtime message broadcasting', () => {
  const senderId = new Types.ObjectId().toString();
  const recipientId = new Types.ObjectId().toString();
  const conversationId = new Types.ObjectId().toString();

  let chatRepository: Record<string, jest.Mock>;
  let notificationsService: Record<string, jest.Mock>;
  let storageService: Record<string, jest.Mock>;
  let chatRealtimeService: Record<string, jest.Mock>;
  let service: ChatService;

  const conversation = {
    id: conversationId,
    participantIds: [new Types.ObjectId(senderId), new Types.ObjectId(recipientId)],
  };

  beforeEach(() => {
    chatRepository = {
      findConversationById: jest.fn().mockResolvedValue(conversation),
      findAnyBlockBetween: jest.fn().mockResolvedValue(null),
      createMessage: jest.fn(),
      updateConversationAfterNewMessage: jest.fn().mockResolvedValue(conversation),
      findMessageById: jest.fn(),
      markMessageSeen: jest.fn().mockResolvedValue(undefined),
      clearConversationUnreadForUser: jest.fn().mockResolvedValue(undefined),
      markMessageDelivered: jest.fn().mockResolvedValue(undefined),
    };
    notificationsService = {
      createMessageNotification: jest.fn().mockResolvedValue(null),
    };
    storageService = {
      saveFile: jest.fn().mockResolvedValue('/uploads/chat/media/message.jpg'),
      deleteFile: jest.fn().mockResolvedValue(undefined),
    };
    chatRealtimeService = {
      emitNewMessage: jest.fn(),
    };

    service = new ChatService(
      chatRepository as any,
      {} as any,
      notificationsService as any,
      storageService as any,
      chatRealtimeService as any,
    );
  });

  it('emits new_message after a text message is created through the service', async () => {
    const message = {
      id: new Types.ObjectId().toString(),
      conversationId: new Types.ObjectId(conversationId),
      senderId: new Types.ObjectId(senderId),
      content: 'hello',
      messageType: 'text',
    };
    chatRepository.createMessage.mockResolvedValue(message);

    const result = await service.sendMessage(senderId, {
      conversationId,
      content: 'hello',
      messageType: 'text',
    });

    expect(result).toBe(message);
    expect(chatRealtimeService.emitNewMessage).toHaveBeenCalledTimes(1);
    expect(chatRealtimeService.emitNewMessage).toHaveBeenCalledWith(conversationId, message);
  });

  it('emits new_message after a media upload message is created through the service', async () => {
    const message = {
      id: new Types.ObjectId().toString(),
      conversationId: new Types.ObjectId(conversationId),
      senderId: new Types.ObjectId(senderId),
      content: '',
      mediaUrl: '/uploads/chat/media/message.jpg',
      messageType: 'image',
    };
    chatRepository.createMessage.mockResolvedValue(message);

    const result = await service.sendMessageWithUpload(
      senderId,
      {
        conversationId,
      },
      {
        mimetype: 'image/jpeg',
        size: 1024,
        buffer: Buffer.from('image'),
        originalname: 'message.jpg',
      },
    );

    expect(result).toBe(message);
    expect(chatRepository.createMessage).toHaveBeenCalledWith(
      expect.objectContaining({
        conversationId,
        senderId,
        messageType: 'image',
        mediaUrl: '/uploads/chat/media/message.jpg',
      }),
    );
    expect(chatRealtimeService.emitNewMessage).toHaveBeenCalledTimes(1);
    expect(chatRealtimeService.emitNewMessage).toHaveBeenCalledWith(conversationId, message);
  });

  it('marks a message delivered for a participant that is not the sender', async () => {
    const messageId = new Types.ObjectId().toString();
    chatRepository.findMessageById.mockResolvedValue({
      id: messageId,
      conversationId: new Types.ObjectId(conversationId),
      senderId: new Types.ObjectId(senderId),
    });

    const result = await service.markMessageDelivered(recipientId, conversationId, messageId);

    expect(chatRepository.markMessageDelivered).toHaveBeenCalledWith(
      messageId,
      recipientId,
      expect.any(Date),
    );
    expect(result).toMatchObject({
      conversationId,
      messageId,
      userId: recipientId,
    });
    expect(result.deliveredAt).toEqual(expect.any(String));
  });

  it('does not let the sender mark their own message delivered', async () => {
    const messageId = new Types.ObjectId().toString();
    chatRepository.findMessageById.mockResolvedValue({
      id: messageId,
      conversationId: new Types.ObjectId(conversationId),
      senderId: new Types.ObjectId(senderId),
    });

    await expect(service.markMessageDelivered(senderId, conversationId, messageId)).rejects.toThrow(
      'Sender cannot mark own message delivered',
    );
    expect(chatRepository.markMessageDelivered).not.toHaveBeenCalled();
  });

  it('marks a message seen and returns conversationId and seenAt for realtime payloads', async () => {
    const messageId = new Types.ObjectId().toString();
    chatRepository.findMessageById.mockResolvedValue({
      id: messageId,
      conversationId: new Types.ObjectId(conversationId),
      senderId: new Types.ObjectId(senderId),
    });

    const result = await service.markMessageSeen(recipientId, messageId, conversationId);

    expect(chatRepository.markMessageSeen).toHaveBeenCalledWith(messageId, recipientId);
    expect(chatRepository.clearConversationUnreadForUser).toHaveBeenCalledWith(conversationId, recipientId);
    expect(result).toMatchObject({
      success: true,
      conversationId,
      messageId,
      userId: recipientId,
    });
    expect(result.seenAt).toEqual(expect.any(String));
  });
});
