import { NotFoundException } from '@nestjs/common';
import { NotificationsService } from './notifications.service';

describe('NotificationsService', () => {
  const notificationRepository = { create: jest.fn(), save: jest.fn() };
  const recipientRepository = {
    insert: jest.fn(),
    createQueryBuilder: jest.fn(),
    findOne: jest.fn(),
    save: jest.fn(),
    update: jest.fn(),
  };
  const playerRepository = { createQueryBuilder: jest.fn() };
  let service: NotificationsService;

  beforeEach(() => {
    jest.resetAllMocks();
    notificationRepository.create.mockImplementation((value) => value);
    service = new NotificationsService(
      notificationRepository as any,
      recipientRepository as any,
      playerRepository as any,
    );
  });

  function chain(overrides: Record<string, jest.Mock>) {
    const query: Record<string, jest.Mock> = {};
    ['innerJoin', 'innerJoinAndSelect', 'where', 'andWhere', 'select', 'orderBy', 'offset', 'limit']
      .forEach((method) => (query[method] = jest.fn(() => query)));
    return Object.assign(query, overrides);
  }

  describe('create', () => {
    it('does nothing when the team has no other active members with a login', async () => {
      const playerQuery = chain({ getRawMany: jest.fn().mockResolvedValue([]) });
      playerRepository.createQueryBuilder.mockReturnValue(playerQuery);

      await service.create({
        teamId: 10,
        event: 'player_creation',
        actorUserId: 5,
        payload: { playerId: 1, playerName: 'Ada Lovelace' },
      });

      expect(playerQuery.where).toHaveBeenCalledWith('player.teamId = :teamId', { teamId: 10 });
      expect(playerQuery.andWhere).toHaveBeenCalledWith('player.active = :active', { active: true });
      expect(playerQuery.andWhere).toHaveBeenCalledWith('player.userId IS NOT NULL');
      expect(playerQuery.andWhere).toHaveBeenCalledWith('player.userId != :actorUserId', { actorUserId: 5 });

      expect(notificationRepository.save).not.toHaveBeenCalled();
      expect(recipientRepository.insert).not.toHaveBeenCalled();
    });

    it('creates one notification and fans it out to every recipient', async () => {
      const playerQuery = chain({ getRawMany: jest.fn().mockResolvedValue([{ userId: 7 }, { userId: 8 }]) });
      playerRepository.createQueryBuilder.mockReturnValue(playerQuery);
      notificationRepository.save.mockResolvedValue({ id: 99 });

      await service.create({
        teamId: 10,
        event: 'player_creation',
        actorUserId: 5,
        payload: { playerId: 1, playerName: 'Ada Lovelace' },
      });

      expect(playerQuery.where).toHaveBeenCalledWith('player.teamId = :teamId', { teamId: 10 });
      expect(playerQuery.andWhere).toHaveBeenCalledWith('player.active = :active', { active: true });
      expect(playerQuery.andWhere).toHaveBeenCalledWith('player.userId IS NOT NULL');
      expect(playerQuery.andWhere).toHaveBeenCalledWith('player.userId != :actorUserId', { actorUserId: 5 });

      expect(notificationRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({
          team: { id: 10 },
          event: 'player_creation',
          actorUserId: 5,
          payload: JSON.stringify({ playerId: 1, playerName: 'Ada Lovelace' }),
        }),
      );
      expect(recipientRepository.insert).toHaveBeenCalledWith([
        { notification: { id: 99 }, userId: 7 },
        { notification: { id: 99 }, userId: 8 },
      ]);
    });
  });

  describe('listForUser', () => {
    it('maps recipient rows to notification DTOs with parsed payloads', async () => {
      const query = chain({
        getCount: jest.fn().mockResolvedValue(1),
        getMany: jest.fn().mockResolvedValue([
          {
            id: 1,
            userId: 7,
            read: false,
            notification: {
              event: 'player_creation',
              actorUserId: 5,
              payload: JSON.stringify({ playerId: 1, playerName: 'Ada Lovelace' }),
              createdAt: new Date('2026-08-04T10:00:00.000Z'),
            },
          },
        ]),
      });
      recipientRepository.createQueryBuilder.mockReturnValue(query);

      const page = await service.listForUser(7, 10, 1, 20);

      expect(page).toEqual({
        data: [
          {
            id: 1,
            event: 'player_creation',
            actorUserId: 5,
            payload: { playerId: 1, playerName: 'Ada Lovelace' },
            read: false,
            createdAt: new Date('2026-08-04T10:00:00.000Z'),
          },
        ],
        page: 1,
        limit: 20,
        total: 1,
        hasNextPage: false,
      });
    });
  });

  describe('getUnreadCount', () => {
    it('counts only unread recipient rows for the given user and team', async () => {
      const query = chain({ getCount: jest.fn().mockResolvedValue(3) });
      recipientRepository.createQueryBuilder.mockReturnValue(query);

      await expect(service.getUnreadCount(7, 10)).resolves.toBe(3);
      expect(query.where).toHaveBeenCalledWith('recipient.userId = :userId', { userId: 7 });
    });
  });

  describe('markRead', () => {
    it('marks a recipient row as read', async () => {
      recipientRepository.findOne.mockResolvedValue({ id: 1, userId: 7, read: false, readAt: null });

      await service.markRead(1, 7);

      expect(recipientRepository.save).toHaveBeenCalledWith(expect.objectContaining({ read: true }));
    });

    it('rejects marking a recipient row that belongs to another user', async () => {
      recipientRepository.findOne.mockResolvedValue({ id: 1, userId: 999, read: false });

      await expect(service.markRead(1, 7)).rejects.toBeInstanceOf(NotFoundException);
      expect(recipientRepository.save).not.toHaveBeenCalled();
    });

    it('rejects marking a recipient row that does not exist', async () => {
      recipientRepository.findOne.mockResolvedValue(null);

      await expect(service.markRead(1, 7)).rejects.toBeInstanceOf(NotFoundException);
    });
  });

  describe('markAllRead', () => {
    it('marks every unread recipient row for the user and team as read', async () => {
      const query = chain({ getRawMany: jest.fn().mockResolvedValue([{ id: 1 }, { id: 2 }]) });
      recipientRepository.createQueryBuilder.mockReturnValue(query);

      await service.markAllRead(7, 10);

      expect(recipientRepository.update).toHaveBeenCalledWith(
        [1, 2],
        expect.objectContaining({ read: true }),
      );
    });

    it('does nothing when there is nothing unread', async () => {
      const query = chain({ getRawMany: jest.fn().mockResolvedValue([]) });
      recipientRepository.createQueryBuilder.mockReturnValue(query);

      await service.markAllRead(7, 10);

      expect(recipientRepository.update).not.toHaveBeenCalled();
    });
  });
});
