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(),
      findOneAndUpdate: jest.fn(),
      find: jest.fn(),
      countDocuments: 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('hides a conversation for one user without deleting it', async () => {
    const conversationId = new Types.ObjectId().toString();
    const userId = new Types.ObjectId().toString();
    const hiddenAt = new Date('2026-06-15T10:00:00.000Z');
    const updatedConversation = { id: conversationId };
    conversationModel.updateOne.mockReturnValue(queryResult({ modifiedCount: 1 }));
    conversationModel.findOneAndUpdate.mockReturnValue(queryResult(updatedConversation));

    const result = await repository.hideConversationForUser(conversationId, userId, hiddenAt);

    expect(result).toBe(updatedConversation);
    expect(conversationModel.updateOne).toHaveBeenCalledWith(
      { _id: new Types.ObjectId(conversationId), participantIds: new Types.ObjectId(userId) },
      { $pull: { hiddenFor: { userId: new Types.ObjectId(userId) } } },
    );
    expect(conversationModel.findOneAndUpdate).toHaveBeenCalledWith(
      { _id: new Types.ObjectId(conversationId), participantIds: new Types.ObjectId(userId) },
      {
        $push: {
          hiddenFor: {
            userId: new Types.ObjectId(userId),
            hiddenAt,
          },
        },
      },
      { new: true },
    );
    expect(messageModel.updateOne).not.toHaveBeenCalled();
  });

  it('filters hidden conversations until a newer message arrives', async () => {
    const userId = new Types.ObjectId().toString();
    const findChain = {
      populate: jest.fn().mockReturnThis(),
      sort: jest.fn().mockReturnThis(),
      skip: jest.fn().mockReturnThis(),
      limit: jest.fn().mockReturnThis(),
      exec: jest.fn().mockResolvedValue([]),
    };
    conversationModel.find.mockReturnValue(findChain);

    await repository.findConversationsForUser(userId, 0, 20);

    const filter = conversationModel.find.mock.calls[0][0];
    expect(filter.participantIds).toEqual(new Types.ObjectId(userId));
    expect(filter.$expr.$let.vars.hiddenEntry.$first.$filter.cond).toEqual({
      $eq: ['$$hidden.userId', new Types.ObjectId(userId)],
    });
    expect(filter.$expr.$let.in.$or[1].$gt).toEqual([
      { $ifNull: ['$lastMessageAt', new Date(0)] },
      '$$hiddenEntry.hiddenAt',
    ]);
  });

  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,
          },
        },
      },
    );
  });
});
