import { LikesService } from './likes.service';

describe('LikesService', () => {
  const userId = '507f1f77bcf86cd799439012';
  const ownerId = '507f191e810c19729de860ea';
  const postId = '507f1f77bcf86cd799439011';

  const setup = (overrides: Record<string, Record<string, jest.Mock>> = {}) => {
    const likesRepository = {
      findOne: jest.fn().mockResolvedValue(null),
      createIfAbsent: jest.fn().mockResolvedValue({ id: 'like-1' }),
      updateReaction: jest.fn().mockResolvedValue(undefined),
      deleteById: jest.fn().mockResolvedValue(true),
      getReactionSummary: jest.fn().mockResolvedValue({ like: 1 }),
      ...overrides.likesRepository,
    };
    const postsRepository = {
      findById: jest.fn().mockResolvedValue({ id: postId, authorId: ownerId, content: 'A post preview' }),
      incrementLikesCount: jest.fn().mockResolvedValue(undefined),
      ...overrides.postsRepository,
    };
    const commentsRepository = {
      findById: jest.fn().mockResolvedValue(null),
      ...overrides.commentsRepository,
    };
    const feedVersionService = {
      bumpUserVersion: jest.fn().mockResolvedValue(1),
      ...overrides.feedVersionService,
    };
    const notificationsService = {
      createLikeNotification: jest.fn().mockResolvedValue(undefined),
      ...overrides.notificationsService,
    };
    const blocksRepository = {
      findAnyBetween: jest.fn().mockResolvedValue(null),
      ...overrides.blocksRepository,
    };
    const postsService = {
      canViewerSeePost: jest.fn().mockResolvedValue(true),
      ...overrides.postsService,
    };
    const service = new LikesService(
      likesRepository as any,
      postsRepository as any,
      commentsRepository as any,
      feedVersionService as any,
      notificationsService as any,
      blocksRepository as any,
      postsService as any,
    );
    return {
      service,
      likesRepository,
      postsRepository,
      commentsRepository,
      feedVersionService,
      notificationsService,
      blocksRepository,
      postsService,
    };
  };

  it('toggles to like or unlike according to the current state', async () => {
    const first = setup();
    const like = jest.spyOn(first.service, 'like').mockResolvedValue({} as any);
    await first.service.toggle(userId, { targetId: postId, targetType: 'post' });
    expect(like).toHaveBeenCalled();

    const second = setup({ likesRepository: { findOne: jest.fn().mockResolvedValue({ id: 'like-1' }) } });
    const unlike = jest.spyOn(second.service, 'unlike').mockResolvedValue({} as any);
    await second.service.toggle(userId, { targetId: postId, targetType: 'post' });
    expect(unlike).toHaveBeenCalled();
  });

  it('creates a post reaction, updates its counter and notifies the owner', async () => {
    const ctx = setup();

    await expect(
      ctx.service.like(userId, { targetId: postId, targetType: 'post', reactionType: 'love' as any }),
    ).resolves.toEqual({
      liked: true,
      reacted: true,
      targetId: postId,
      targetType: 'post',
      reactionType: 'love',
    });

    expect(ctx.likesRepository.createIfAbsent).toHaveBeenCalledWith(userId, postId, 'post', 'love');
    expect(ctx.postsRepository.incrementLikesCount).toHaveBeenCalledWith(postId, 1);
    expect(ctx.feedVersionService.bumpUserVersion).toHaveBeenCalledWith(userId);
    expect(ctx.notificationsService.createLikeNotification).toHaveBeenCalledWith(
      userId,
      ownerId,
      postId,
      { resourceType: 'post', previewText: 'A post preview' },
    );
  });

  it('updates an existing reaction without incrementing counters', async () => {
    const ctx = setup({
      likesRepository: {
        findOne: jest.fn().mockResolvedValue({ id: 'like-1', reactionType: 'like' }),
      },
    });

    await ctx.service.like(userId, {
      targetId: postId,
      targetType: 'post',
      reactionType: 'love' as any,
    });

    expect(ctx.likesRepository.updateReaction).toHaveBeenCalledWith('like-1', 'love');
    expect(ctx.likesRepository.createIfAbsent).not.toHaveBeenCalled();
    expect(ctx.postsRepository.incrementLikesCount).not.toHaveBeenCalled();
  });

  it('treats a concurrent duplicate like as successful without double counting', async () => {
    const ctx = setup({ likesRepository: { createIfAbsent: jest.fn().mockResolvedValue(null) } });

    await expect(ctx.service.like(userId, { targetId: postId, targetType: 'post' })).resolves.toMatchObject({
      liked: true,
      reactionType: 'like',
    });
    expect(ctx.postsRepository.incrementLikesCount).not.toHaveBeenCalled();
    expect(ctx.feedVersionService.bumpUserVersion).not.toHaveBeenCalled();
  });

  it('keeps a successful like when notification delivery fails', async () => {
    const ctx = setup({
      notificationsService: { createLikeNotification: jest.fn().mockRejectedValue('offline') },
    });

    await expect(ctx.service.like(userId, { targetId: postId, targetType: 'post' })).resolves.toMatchObject({
      liked: true,
    });
    expect(ctx.postsRepository.incrementLikesCount).toHaveBeenCalledWith(postId, 1);
  });

  it('supports comment likes and resolves the comment owner and preview', async () => {
    const commentId = '507f191e810c19729de860eb';
    const ctx = setup({
      commentsRepository: {
        findById: jest.fn().mockResolvedValue({
          id: commentId,
          postId: { toString: () => postId },
          authorId: { toString: () => ownerId },
          content: 'comment preview',
        }),
      },
    });

    await ctx.service.like(userId, { targetId: commentId, targetType: 'comment' });

    expect(ctx.postsRepository.incrementLikesCount).not.toHaveBeenCalled();
    expect(ctx.notificationsService.createLikeNotification).toHaveBeenCalledWith(
      userId,
      ownerId,
      commentId,
      { resourceType: 'comment', previewText: 'comment preview' },
    );
  });

  it('rejects missing or hidden targets with a not-found response', async () => {
    const missingPost = setup({ postsRepository: { findById: jest.fn().mockResolvedValue(null) } });
    await expect(missingPost.service.like(userId, { targetId: postId, targetType: 'post' })).rejects.toThrow(
      'Post not found',
    );

    const hiddenPost = setup({ postsService: { canViewerSeePost: jest.fn().mockResolvedValue(false) } });
    await expect(hiddenPost.service.like(userId, { targetId: postId, targetType: 'post' })).rejects.toThrow(
      'Post not found',
    );

    const missingComment = setup();
    await expect(
      missingComment.service.like(userId, { targetId: postId, targetType: 'comment' }),
    ).rejects.toThrow('Comment not found');
  });

  it('unlikes idempotently and decrements only after a successful delete', async () => {
    const absent = setup();
    await expect(absent.service.unlike(userId, { targetId: postId, targetType: 'post' })).resolves.toMatchObject({
      liked: false,
    });
    expect(absent.postsRepository.incrementLikesCount).not.toHaveBeenCalled();

    const lostRace = setup({
      likesRepository: {
        findOne: jest.fn().mockResolvedValue({ id: 'like-1' }),
        deleteById: jest.fn().mockResolvedValue(false),
      },
    });
    await lostRace.service.unlike(userId, { targetId: postId, targetType: 'post' });
    expect(lostRace.postsRepository.incrementLikesCount).not.toHaveBeenCalled();

    const deleted = setup({
      likesRepository: { findOne: jest.fn().mockResolvedValue({ id: 'like-1' }) },
    });
    await deleted.service.unlike(userId, { targetId: postId, targetType: 'post' });
    expect(deleted.postsRepository.incrementLikesCount).toHaveBeenCalledWith(postId, -1);
    expect(deleted.feedVersionService.bumpUserVersion).toHaveBeenCalledWith(userId);
  });

  it('returns reaction status and summary only for a visible target', async () => {
    const ctx = setup({
      likesRepository: {
        findOne: jest.fn().mockResolvedValue({ reactionType: 'love' }),
        getReactionSummary: jest.fn().mockResolvedValue({ like: 2, love: 3 }),
      },
    });

    await expect(ctx.service.getStatus(userId, { targetId: postId, targetType: 'post' })).resolves.toEqual({
      liked: true,
      reacted: true,
      targetId: postId,
      targetType: 'post',
      reactionType: 'love',
      reactionSummary: { like: 2, love: 3 },
    });

    const hidden = setup({ postsService: { canViewerSeePost: jest.fn().mockResolvedValue(false) } });
    await hidden.service.getStatus(userId, { targetId: postId, targetType: 'post' });
    expect(hidden.likesRepository.getReactionSummary).not.toHaveBeenCalled();
  });
  it('returns liked false from status when target post no longer exists', async () => {
    const likesRepository = {
      findOne: jest.fn(),
    };
    const postsRepository = {
      findById: jest.fn().mockResolvedValue(null),
    };
    const commentsRepository = {
      findById: jest.fn(),
    };
    const blocksRepository = {
      findAnyBetween: jest.fn().mockResolvedValue(null),
    };

    const service = new LikesService(
      likesRepository as any,
      postsRepository as any,
      commentsRepository as any,
      { bumpGlobalVersion: jest.fn() } as any,
      { createLikeNotification: jest.fn() } as any,
      blocksRepository as any,
      { canViewerSeePost: jest.fn().mockResolvedValue(true) } as any,
    );

    await expect(
      service.getStatus('user-1', { targetId: '507f1f77bcf86cd799439011', targetType: 'post' }),
    ).resolves.toEqual({
      liked: false,
      reacted: false,
      targetId: '507f1f77bcf86cd799439011',
      targetType: 'post',
    });
    expect(likesRepository.findOne).not.toHaveBeenCalled();
  });

  it('prevents liking a post when either user blocked the other', async () => {
    const ownerId = '507f191e810c19729de860ea';
    const postId = '507f1f77bcf86cd799439011';
    const userId = '507f1f77bcf86cd799439012';
    const likesRepository = {
      findOne: jest.fn(),
    };
    const postsRepository = {
      findById: jest.fn().mockResolvedValue({ authorId: ownerId }),
    };
    const commentsRepository = {
      findById: jest.fn(),
    };
    const blocksRepository = {
      findAnyBetween: jest.fn().mockResolvedValue({ id: 'block-1' }),
    };

    const service = new LikesService(
      likesRepository as any,
      postsRepository as any,
      commentsRepository as any,
      { bumpGlobalVersion: jest.fn() } as any,
      { createLikeNotification: jest.fn() } as any,
      blocksRepository as any,
      { canViewerSeePost: jest.fn().mockResolvedValue(true) } as any,
    );

    await expect(service.like(userId, { targetId: postId, targetType: 'post' })).rejects.toThrow(
      'You cannot interact with this user',
    );
    expect(likesRepository.findOne).not.toHaveBeenCalled();
  });
});
