import { Types } from 'mongoose';
import { ChatRepository } from './chat.repository';

const queryResult = <T>(value: T) => ({
  exec: jest.fn().mockResolvedValue(value),
});

describe('ChatRepository unread counters', () => {
  let conversationModel: Record<string, jest.Mock>;
  let messageModel: Record<string, jest.Mock>;
  let repository: ChatRepository;

  beforeEach(() => {
    conversationModel = {
      create: jest.fn(),
      findById: jest.fn(),
      findByIdAndUpdate: jest.fn(),
      updateOne: jest.fn(),
    };
    messageModel = {
      updateOne: jest.fn(),
    };
    repository = new ChatRepository(conversationModel as any, messageModel as any, {} as any);
  });

  it('creates conversations with string user ids as unread counter keys', async () => {
    const userA = new Types.ObjectId().toString();
    const userB = new Types.ObjectId().toString();
    conversationModel.create.mockResolvedValue({ id: 'conversation-1' });

    await repository.createConversation({
      participantIds: [userA, userB],
      isGroup: false,
      createdBy: userA,
    });

    expect(conversationModel.create).toHaveBeenCalledWith(
      expect.objectContaining({
        unreadCountByUser: {
          [userA]: 0,
          [userB]: 0,
        },
      }),
    );
  });

  it('increments unread count for recipients and resets sender with atomic updates', async () => {
    const conversationId = new Types.ObjectId().toString();
    const messageId = new Types.ObjectId().toString();
    const senderId = new Types.ObjectId().toString();
    const recipientId = new Types.ObjectId().toString();
    const updatedConversation = { id: conversationId };

    conversationModel.findById.mockReturnValue(
      queryResult({
        participantIds: [new Types.ObjectId(senderId), new Types.ObjectId(recipientId)],
      }),
    );
    conversationModel.findByIdAndUpdate.mockReturnValue(queryResult(updatedConversation));

    await repository.updateConversationAfterNewMessage(conversationId, messageId, senderId, 'hello');

    expect(conversationModel.findByIdAndUpdate).toHaveBeenCalledWith(
      conversationId,
      expect.objectContaining({
        $set: expect.objectContaining({
          lastMessageId: expect.any(Types.ObjectId),
          lastMessageText: 'hello',
          [`unreadCountByUser.${senderId}`]: 0,
        }),
        $inc: {
          [`unreadCountByUser.${recipientId}`]: 1,
        },
      }),
      { new: true },
    );
  });

  it('clears unread count for current user without saving a Mongoose Map', async () => {
    const conversationId = new Types.ObjectId().toString();
    const userId = new Types.ObjectId().toString();
    conversationModel.updateOne.mockReturnValue(queryResult({ modifiedCount: 1 }));

    await repository.clearConversationUnreadForUser(conversationId, userId);

    expect(conversationModel.updateOne).toHaveBeenCalledWith(
      { _id: new Types.ObjectId(conversationId) },
      {
        $set: {
          [`unreadCountByUser.${userId}`]: 0,
        },
      },
    );
  });

  it('adds a delivered receipt only when the user has not already delivered the message', async () => {
    const messageId = new Types.ObjectId().toString();
    const userId = new Types.ObjectId().toString();
    const deliveredAt = new Date('2026-06-07T10:00:00.000Z');
    messageModel.updateOne.mockReturnValue(queryResult({ modifiedCount: 1 }));

    await repository.markMessageDelivered(messageId, userId, deliveredAt);

    expect(messageModel.updateOne).toHaveBeenCalledWith(
      {
        _id: new Types.ObjectId(messageId),
        'deliveredBy.userId': { $ne: new Types.ObjectId(userId) },
      },
      {
        $push: {
          deliveredBy: {
            userId: new Types.ObjectId(userId),
            deliveredAt,
          },
        },
      },
    );
  });
});
