import { NotFoundException } from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import { Types } from 'mongoose';
import { SortOrder } from '../../common/enums/sort-order.enum';
import { NotificationUnreadCountQueryDto } from './dto/notification-query.dto';
import { NotificationsService } from './notifications.service';

describe('NotificationsService', () => {
  it('rejects invalid unread-count category through DTO validation', async () => {
    const dto = plainToInstance(NotificationUnreadCountQueryDto, { category: 'badges' });

    const errors = await validate(dto);

    expect(errors.length).toBeGreaterThan(0);
    expect(errors[0].property).toBe('category');
  });

  it('creates mention notifications with mention type', async () => {
    const notificationsRepository = {
      create: jest.fn().mockResolvedValue({ toJSON: () => ({ _id: 'notification-1' }) }),
      countUnread: jest.fn().mockResolvedValue(5),
      countUnreadByFilter: jest.fn().mockResolvedValue(0),
    };
    const notificationsGateway = {
      emitCreated: jest.fn(),
    };

    const service = new NotificationsService(
      notificationsRepository as any,
      notificationsGateway as any,
    );

    await service.createMentionNotification(
      '507f1f77bcf86cd799439011',
      '507f191e810c19729de860ea',
      '507f1f77bcf86cd799439012',
      { previewText: 'Hello @rami' },
    );

    expect(notificationsRepository.create).toHaveBeenCalledWith(
      expect.objectContaining({
        type: 'mention',
        previewText: 'Hello @rami',
      }),
    );
  });

  it('creates message notifications with Flutter navigation metadata', async () => {
    const notificationsRepository = {
      create: jest.fn().mockResolvedValue({ toJSON: () => ({ _id: 'notification-1' }) }),
      countUnread: jest.fn().mockResolvedValue(1),
      countUnreadByFilter: jest.fn().mockResolvedValue(0),
    };
    const notificationsGateway = {
      emitCreated: jest.fn(),
    };

    const service = new NotificationsService(
      notificationsRepository as any,
      notificationsGateway as any,
    );

    await service.createMessageNotification(
      '507f1f77bcf86cd799439011',
      '507f191e810c19729de860ea',
      '507f1f77bcf86cd799439012',
      'hello',
      '507f1f77bcf86cd799439013',
    );

    expect(notificationsRepository.create).toHaveBeenCalledWith(
      expect.objectContaining({
        type: 'message',
        referenceId: expect.any(Types.ObjectId),
        resourceType: 'conversation',
        deepLink: '/chat/conversations/507f1f77bcf86cd799439012',
        previewText: 'hello',
        metadata: {
          type: 'message',
          conversationId: '507f1f77bcf86cd799439012',
          messageId: '507f1f77bcf86cd799439013',
        },
      }),
    );
  });

  it('creates follow approval notifications with string-only Flutter metadata', async () => {
    const notificationsRepository = {
      create: jest.fn().mockResolvedValue({ toJSON: () => ({ _id: 'notification-1' }) }),
      countUnread: jest.fn().mockResolvedValue(1),
      countUnreadByFilter: jest.fn().mockResolvedValue(0),
    };
    const notificationsGateway = { emitCreated: jest.fn() };
    const service = new NotificationsService(
      notificationsRepository as any,
      notificationsGateway as any,
    );
    const actorId = '507f1f77bcf86cd799439011';
    const recipientId = '507f191e810c19729de860ea';
    const requestId = '507f1f77bcf86cd799439012';

    await service.createFollowRequestApprovedNotification({
      actorId,
      recipientId,
      requestId,
    });

    expect(notificationsRepository.create).toHaveBeenCalledWith(
      expect.objectContaining({
        recipientId: new Types.ObjectId(recipientId),
        actorId: new Types.ObjectId(actorId),
        type: 'follow_request_approved',
        referenceId: new Types.ObjectId(requestId),
        title: 'Follow request approved',
        resourceType: 'user',
        deepLink: `/users/${actorId}`,
        metadata: {
          type: 'follow_request_approved',
          userId: actorId,
          requestId,
        },
        read: false,
      }),
    );
    expect(notificationsGateway.emitCreated).toHaveBeenCalled();
  });

  it('recalculates unread count after markAllRead', async () => {
    const notificationsRepository = {
      markAllRead: jest.fn().mockResolvedValue(4),
      countUnread: jest.fn().mockResolvedValue(2),
      countUnreadByFilter: jest.fn().mockResolvedValue(0),
    };
    const notificationsGateway = {
      emitUnreadCount: jest.fn(),
    };

    const service = new NotificationsService(
      notificationsRepository as any,
      notificationsGateway as any,
    );

    await expect(service.markAllRead('user-1')).resolves.toEqual({
      message: 'All notifications marked as read',
      modifiedCount: 4,
      updatedCount: 4,
      unreadCount: 2,
    });
    expect(notificationsGateway.emitUnreadCount).toHaveBeenCalledWith(
      'user-1',
      2,
      expect.objectContaining({
        total: 2,
        interactions: 0,
        messages: 0,
      }),
    );
  });

  it('throws not found for invalid notification id in markRead', async () => {
    const notificationsRepository = {
      markRead: jest.fn(),
      countUnread: jest.fn(),
    };
    const notificationsGateway = {
      emitUnreadCount: jest.fn(),
    };

    const service = new NotificationsService(
      notificationsRepository as any,
      notificationsGateway as any,
    );

    await expect(service.markRead('user-1', 'invalid-id')).rejects.toBeInstanceOf(NotFoundException);
    expect(notificationsRepository.markRead).not.toHaveBeenCalled();
  });

  it('returns total unread count for the current user in the notifications list', async () => {
    const notificationsRepository = {
      findMine: jest.fn().mockResolvedValue([{ id: 'notification-1' }]),
      countMine: jest.fn().mockResolvedValue(30),
      countUnread: jest.fn().mockResolvedValue(7),
    };
    const notificationsGateway = {
      emitUnreadCount: jest.fn(),
    };

    const service = new NotificationsService(
      notificationsRepository as any,
      notificationsGateway as any,
    );

    await expect(
      service.getMine('507f1f77bcf86cd799439011', {
        page: 1,
        limit: 20,
        sortOrder: 'desc' as any,
      }),
    ).resolves.toMatchObject({
      count: 1,
      page: 1,
      limit: 20,
      total: 30,
      unreadCount: 7,
      pagination: {
        mode: 'offset',
        hasNextPage: true,
      },
    });
    expect(notificationsRepository.countUnread).toHaveBeenCalledWith('507f1f77bcf86cd799439011');
  });

  it('returns total unread count without category for backward compatibility', async () => {
    const notificationsRepository = {
      countUnread: jest.fn().mockResolvedValue(10),
    };
    const notificationsGateway = {};

    const service = new NotificationsService(
      notificationsRepository as any,
      notificationsGateway as any,
    );

    await expect(service.getUnreadCountByCategory('507f1f77bcf86cd799439011')).resolves.toEqual({
      unreadCount: 10,
    });
    expect(notificationsRepository.countUnread).toHaveBeenCalledWith('507f1f77bcf86cd799439011');
  });

  it('returns unread count for interactions category only', async () => {
    const notificationsRepository = {
      countUnreadByFilter: jest.fn().mockResolvedValue(4),
    };
    const notificationsGateway = {};

    const service = new NotificationsService(
      notificationsRepository as any,
      notificationsGateway as any,
    );

    await expect(
      service.getUnreadCountByCategory('507f1f77bcf86cd799439011', 'interactions'),
    ).resolves.toEqual({
      unreadCount: 4,
      category: 'interactions',
    });
    expect(notificationsRepository.countUnreadByFilter).toHaveBeenCalledWith(
      '507f1f77bcf86cd799439011',
      {
        type: {
          $in: [
            'like',
            'comment',
            'reply',
            'mention',
            'save',
            'share',
            'collaboration_request',
            'collaboration_request_approved',
            'collaboration_request_rejected',
            'system',
          ],
        },
      },
    );
  });

  it('returns unread count for messages category only', async () => {
    const notificationsRepository = {
      countUnreadByFilter: jest.fn().mockResolvedValue(3),
    };
    const notificationsGateway = {};

    const service = new NotificationsService(
      notificationsRepository as any,
      notificationsGateway as any,
    );

    await expect(
      service.getUnreadCountByCategory('507f1f77bcf86cd799439011', 'messages'),
    ).resolves.toEqual({
      unreadCount: 3,
      category: 'messages',
    });
    expect(notificationsRepository.countUnreadByFilter).toHaveBeenCalledWith(
      '507f1f77bcf86cd799439011',
      { type: { $in: ['message'] } },
    );
  });

  it('returns unread count for follow requests category only', async () => {
    const notificationsRepository = {
      countUnreadByFilter: jest.fn().mockResolvedValue(2),
    };
    const notificationsGateway = {};

    const service = new NotificationsService(
      notificationsRepository as any,
      notificationsGateway as any,
    );

    await expect(
      service.getUnreadCountByCategory('507f1f77bcf86cd799439011', 'follow_requests'),
    ).resolves.toEqual({
      unreadCount: 2,
      category: 'follow_requests',
    });
    expect(notificationsRepository.countUnreadByFilter).toHaveBeenCalledWith(
      '507f1f77bcf86cd799439011',
      {
        type: {
          $in: ['follow_request', 'follow_request_approved', 'follow_request_rejected'],
        },
      },
    );
  });

  it('returns grouped unread counts for badge hydration', async () => {
    const notificationsRepository = {
      countUnread: jest.fn().mockResolvedValue(10),
      countUnreadByFilter: jest
        .fn()
        .mockResolvedValueOnce(4)
        .mockResolvedValueOnce(3)
        .mockResolvedValueOnce(1)
        .mockResolvedValueOnce(2)
        .mockResolvedValueOnce(1)
        .mockResolvedValueOnce(0),
    };
    const notificationsGateway = {};

    const service = new NotificationsService(
      notificationsRepository as any,
      notificationsGateway as any,
    );

    await expect(service.getUnreadCounts('507f1f77bcf86cd799439011')).resolves.toEqual({
      total: 10,
      interactions: 4,
      messages: 3,
      follows: 1,
      followRequests: 2,
      collaboration: 1,
      system: 0,
    });
  });

  it('filters notifications by interactions category', async () => {
    const notificationsRepository = {
      findMine: jest.fn().mockResolvedValue([]),
      countMine: jest.fn().mockResolvedValue(0),
      countUnread: jest.fn().mockResolvedValue(0),
    };
    const notificationsGateway = {
      emitUnreadCount: jest.fn(),
    };

    const service = new NotificationsService(
      notificationsRepository as any,
      notificationsGateway as any,
    );

    await service.getMine('507f1f77bcf86cd799439011', {
      page: 1,
      limit: 20,
      category: 'interactions',
    });

    expect(notificationsRepository.findMine).toHaveBeenCalledWith(
      '507f1f77bcf86cd799439011',
      {
        type: {
          $in: [
            'like',
            'comment',
            'reply',
            'mention',
            'save',
            'share',
            'collaboration_request',
            'collaboration_request_approved',
            'collaboration_request_rejected',
            'system',
          ],
        },
      },
      0,
      20,
      { createdAt: -1 },
    );
  });

  it('keeps type filter priority over category for backward compatibility', async () => {
    const notificationsRepository = {
      findMine: jest.fn().mockResolvedValue([]),
      countMine: jest.fn().mockResolvedValue(0),
      countUnread: jest.fn().mockResolvedValue(0),
    };
    const notificationsGateway = {
      emitUnreadCount: jest.fn(),
    };

    const service = new NotificationsService(
      notificationsRepository as any,
      notificationsGateway as any,
    );

    await service.getMine('507f1f77bcf86cd799439011', {
      page: 1,
      limit: 20,
      type: 'message',
      category: 'interactions',
    });

    expect(notificationsRepository.findMine).toHaveBeenCalledWith(
      '507f1f77bcf86cd799439011',
      { type: 'message' },
      0,
      20,
      { createdAt: -1 },
    );
  });

  it('creates system notifications with system resource mapping when requested', async () => {
    const notificationsRepository = {
      create: jest.fn().mockResolvedValue({ toJSON: () => ({ _id: 'notification-1' }) }),
      countUnread: jest.fn().mockResolvedValue(1),
      countUnreadByFilter: jest.fn().mockResolvedValue(0),
    };
    const notificationsGateway = {
      emitCreated: jest.fn(),
    };

    const service = new NotificationsService(
      notificationsRepository as any,
      notificationsGateway as any,
    );

    await service.create({
      actorId: '507f1f77bcf86cd799439011',
      recipientId: '507f191e810c19729de860ea',
      type: 'system',
    });

    expect(notificationsRepository.create).toHaveBeenCalledWith(
      expect.objectContaining({
        type: 'system',
        resourceType: 'system',
        deepLink: '',
      }),
    );
  });

  describe('complete notification behavior', () => {
    const setup = () => {
      const notificationsRepository = {
        create: jest.fn().mockImplementation(async (value) => ({
          id: 'notification-1',
          ...value,
          toJSON: () => ({ id: 'notification-1', type: value.type }),
        })),
        findMine: jest.fn().mockResolvedValue([]),
        countMine: jest.fn().mockResolvedValue(0),
        findMany: jest.fn().mockResolvedValue([]),
        count: jest.fn().mockResolvedValue(0),
        countUnread: jest.fn().mockResolvedValue(3),
        countUnreadAll: jest.fn().mockResolvedValue(7),
        countUnreadByFilter: jest.fn().mockResolvedValue(0),
        markRead: jest.fn(),
        markAllRead: jest.fn().mockResolvedValue(0),
      };
      const notificationsGateway = {
        emitCreated: jest.fn(),
        emitUnreadCount: jest.fn(),
      };
      return {
        service: new NotificationsService(
          notificationsRepository as any,
          notificationsGateway as any,
        ),
        notificationsRepository,
        notificationsGateway,
      };
    };

    it('suppresses self-notifications before writing or broadcasting', async () => {
      const { service, notificationsRepository, notificationsGateway } = setup();
      const userId = new Types.ObjectId().toString();

      await expect(
        service.create({ actorId: userId, recipientId: userId, type: 'like' }),
      ).resolves.toBeNull();
      expect(notificationsRepository.create).not.toHaveBeenCalled();
      expect(notificationsGateway.emitCreated).not.toHaveBeenCalled();
    });

    it('creates every supported title and resource navigation shape', async () => {
      const { service, notificationsRepository } = setup();
      const actorId = new Types.ObjectId().toString();
      const recipientId = new Types.ObjectId().toString();
      const referenceId = new Types.ObjectId().toString();
      const cases = [
        ['like', 'New like'],
        ['comment', 'New comment'],
        ['follow', 'New follower'],
        ['message', 'New message'],
        ['save', 'Post saved'],
        ['share', 'Post shared'],
        ['mention', 'New mention'],
        ['reply', 'New reply'],
        ['system', 'Notification'],
        ['collaboration_request', 'Collaboration request'],
        ['collaboration_request_approved', 'Collaboration request approved'],
        ['collaboration_request_rejected', 'Collaboration request rejected'],
        ['collaboration_request_cancelled', 'Collaboration request cancelled'],
        ['follow_request', 'Follow request'],
        ['follow_request_approved', 'Follow request approved'],
        ['follow_request_rejected', 'Follow request rejected'],
        ['support_reply', 'Support reply'],
        ['support_ticket_status', 'Support ticket updated'],
      ] as const;

      for (const [type, title] of cases) {
        await service.create({ actorId, recipientId, type, referenceId });
        expect(notificationsRepository.create).toHaveBeenLastCalledWith(
          expect.objectContaining({ type, title }),
        );
      }

      expect(notificationsRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          type: 'follow_request',
          resourceType: 'user',
          deepLink: `/users/${referenceId}`,
        }),
      );
      expect(notificationsRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          type: 'support_reply',
          resourceType: 'support_ticket',
          deepLink: `/support/tickets/${referenceId}`,
        }),
      );
    });

    it('honors trimmed custom fields and handles a notification without a reference', async () => {
      const { service, notificationsRepository } = setup();
      const actorId = new Types.ObjectId().toString();
      const recipientId = new Types.ObjectId().toString();

      await service.create({
        actorId,
        recipientId,
        type: 'like',
        title: '  Custom title  ',
        previewText: '  Preview  ',
        resourceType: ' post ',
        deepLink: ' /custom ',
        metadata: { source: 'test' },
      });

      expect(notificationsRepository.create).toHaveBeenLastCalledWith(
        expect.objectContaining({
          title: 'Custom title',
          previewText: 'Preview',
          resourceType: 'post',
          deepLink: '/custom',
          referenceId: undefined,
          metadata: { source: 'test' },
        }),
      );

      await service.create({ actorId, recipientId, type: 'like' });
      expect(notificationsRepository.create).toHaveBeenLastCalledWith(
        expect.objectContaining({ deepLink: '' }),
      );
    });

    it('executes all convenience creators with their defaults and options', async () => {
      const { service } = setup();
      const actorId = new Types.ObjectId().toString();
      const recipientId = new Types.ObjectId().toString();
      const referenceId = new Types.ObjectId().toString();
      const createSpy = jest.spyOn(service, 'create');

      await service.createFollowNotification(actorId, recipientId);
      await service.createLikeNotification(actorId, recipientId, referenceId, {
        resourceType: 'reel',
        previewText: 'liked',
      });
      await service.createCommentNotification(actorId, recipientId, referenceId);
      await service.createSaveNotification(actorId, recipientId, referenceId, {
        previewText: 'saved',
      });
      await service.createShareNotification(actorId, recipientId, referenceId);
      await service.createMessageNotification(actorId, recipientId, referenceId);

      expect(createSpy).toHaveBeenCalledWith(
        expect.objectContaining({ type: 'follow', referenceId: actorId }),
      );
      expect(createSpy).toHaveBeenCalledWith(
        expect.objectContaining({ type: 'like', resourceType: 'reel', previewText: 'liked' }),
      );
      expect(createSpy).toHaveBeenCalledWith(
        expect.objectContaining({ type: 'comment', resourceType: 'post', previewText: '' }),
      );
      expect(createSpy).toHaveBeenCalledWith(
        expect.objectContaining({ type: 'message', metadata: expect.not.objectContaining({ messageId: expect.anything() }) }),
      );
    });

    it('applies read, resource, paging, and ascending sort filters for the user', async () => {
      const { service, notificationsRepository } = setup();
      notificationsRepository.findMine.mockResolvedValue([{ id: 'n1' }]);
      notificationsRepository.countMine.mockResolvedValue(8);

      const result = await service.getMine('user-1', {
        page: 2,
        limit: 3,
        read: false,
        resourceType: ' post ',
        sortOrder: SortOrder.ASC,
      });

      expect(notificationsRepository.findMine).toHaveBeenCalledWith(
        'user-1',
        { read: false, resourceType: 'post' },
        3,
        3,
        { createdAt: 1 },
      );
      expect(result).toMatchObject({ page: 2, limit: 3, total: 8, unreadCount: 3 });
    });

    it('lists super-admin notifications while counting unread independently of read filter', async () => {
      const { service, notificationsRepository } = setup();
      notificationsRepository.findMany.mockResolvedValue([{ id: 'n1' }]);
      notificationsRepository.count.mockResolvedValue(4);

      const result = await service.getForSuperAdmin({
        page: 2,
        limit: 2,
        read: true,
        category: 'messages',
        resourceType: ' conversation ',
        sortOrder: SortOrder.ASC,
      });

      const filter = {
        read: true,
        type: { $in: ['message'] },
        resourceType: 'conversation',
      };
      expect(notificationsRepository.findMany).toHaveBeenCalledWith(filter, 2, 2, {
        createdAt: 1,
      });
      expect(notificationsRepository.count).toHaveBeenCalledWith(filter);
      expect(notificationsRepository.countUnreadAll).toHaveBeenCalledWith({
        type: { $in: ['message'] },
        resourceType: 'conversation',
      });
      expect(result).toMatchObject({ page: 2, limit: 2, total: 4, unreadCount: 7 });
    });

    it('marks an existing notification read and rejects an inaccessible one', async () => {
      const { service, notificationsRepository, notificationsGateway } = setup();
      const notificationId = new Types.ObjectId().toString();
      notificationsRepository.markRead.mockResolvedValueOnce(null).mockResolvedValueOnce({
        id: notificationId,
      });

      await expect(service.markRead('user-1', notificationId)).rejects.toThrow(
        'Notification not found',
      );
      await expect(service.markRead('user-1', notificationId)).resolves.toMatchObject({
        message: 'Notification marked as read',
        unreadCount: 3,
        item: { id: notificationId },
      });
      expect(notificationsGateway.emitUnreadCount).toHaveBeenCalledWith(
        'user-1',
        3,
        expect.objectContaining({ total: 3 }),
      );
    });
  });
});
