import { SortOrder } from '../../common/enums/sort-order.enum';
import { SavesService } from './saves.service';

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

  const setup = (overrides: Record<string, Record<string, jest.Mock>> = {}) => {
    const savesRepository = {
      findOne: jest.fn().mockResolvedValue(null),
      createIfAbsent: jest.fn().mockResolvedValue({ id: 'save-1' }),
      deleteById: jest.fn().mockResolvedValue(true),
      findUserSavedPostIds: jest.fn().mockResolvedValue([]),
      countByUser: jest.fn().mockResolvedValue(0),
      ...overrides.savesRepository,
    };
    const postsRepository = {
      findById: jest.fn().mockResolvedValue({ id: postId, authorId: ownerId, content: 'post preview' }),
      incrementSavesCount: jest.fn().mockResolvedValue(undefined),
      findManyByIds: jest.fn().mockResolvedValue([]),
      ...overrides.postsRepository,
    };
    const feedVersionService = {
      bumpUserVersion: jest.fn().mockResolvedValue(1),
      ...overrides.feedVersionService,
    };
    const notificationsService = {
      createSaveNotification: 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 SavesService(
      savesRepository as any,
      postsRepository as any,
      feedVersionService as any,
      notificationsService as any,
      blocksRepository as any,
      postsService as any,
    );
    return {
      service,
      savesRepository,
      postsRepository,
      feedVersionService,
      notificationsService,
      blocksRepository,
      postsService,
    };
  };

  it('toggles between save and unsave using the current state', async () => {
    const first = setup();
    const save = jest.spyOn(first.service, 'save').mockResolvedValue({ saved: true, postId });
    await first.service.toggle(userId, { postId });
    expect(save).toHaveBeenCalledWith(userId, { postId });

    const second = setup({ savesRepository: { findOne: jest.fn().mockResolvedValue({ id: 'save-1' }) } });
    const unsave = jest.spyOn(second.service, 'unsave').mockResolvedValue({ saved: false, postId });
    await second.service.toggle(userId, { postId });
    expect(unsave).toHaveBeenCalledWith(userId, { postId });
  });

  it('saves a visible post, increments its counter and notifies its owner', async () => {
    const ctx = setup();

    await expect(ctx.service.save(userId, { postId })).resolves.toEqual({ saved: true, postId });

    expect(ctx.savesRepository.createIfAbsent).toHaveBeenCalledWith(userId, postId);
    expect(ctx.postsRepository.incrementSavesCount).toHaveBeenCalledWith(postId, 1);
    expect(ctx.feedVersionService.bumpUserVersion).toHaveBeenCalledWith(userId);
    expect(ctx.notificationsService.createSaveNotification).toHaveBeenCalledWith(
      userId,
      ownerId,
      postId,
      { resourceType: 'post', previewText: 'post preview' },
    );
  });

  it('does not double count an existing or concurrently inserted save', async () => {
    const existing = setup({
      savesRepository: { findOne: jest.fn().mockResolvedValue({ id: 'save-1' }) },
    });
    await existing.service.save(userId, { postId });
    expect(existing.savesRepository.createIfAbsent).not.toHaveBeenCalled();
    expect(existing.postsRepository.incrementSavesCount).not.toHaveBeenCalled();

    const race = setup({ savesRepository: { createIfAbsent: jest.fn().mockResolvedValue(null) } });
    await race.service.save(userId, { postId });
    expect(race.postsRepository.incrementSavesCount).not.toHaveBeenCalled();
    expect(race.feedVersionService.bumpUserVersion).not.toHaveBeenCalled();
  });

  it('does not notify the post owner when they save their own post', async () => {
    const ctx = setup({
      postsRepository: {
        findById: jest.fn().mockResolvedValue({ id: postId, authorId: userId, content: 'own' }),
      },
    });

    await ctx.service.save(userId, { postId });
    expect(ctx.blocksRepository.findAnyBetween).not.toHaveBeenCalled();
    expect(ctx.notificationsService.createSaveNotification).not.toHaveBeenCalled();
  });

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

    await expect(ctx.service.save(userId, { postId })).resolves.toEqual({ saved: true, postId });
    expect(ctx.postsRepository.incrementSavesCount).toHaveBeenCalledWith(postId, 1);
  });

  it('rejects missing and hidden posts', async () => {
    const missing = setup({ postsRepository: { findById: jest.fn().mockResolvedValue(null) } });
    await expect(missing.service.save(userId, { postId })).rejects.toThrow('Post not found');

    const hidden = setup({ postsService: { canViewerSeePost: jest.fn().mockResolvedValue(false) } });
    await expect(hidden.service.save(userId, { postId })).rejects.toThrow('Post not found');
    expect(hidden.savesRepository.findOne).not.toHaveBeenCalled();
  });

  it('unsaves idempotently and decrements only after a successful deletion', async () => {
    const absent = setup();
    await expect(absent.service.unsave(userId, { postId })).resolves.toEqual({ saved: false, postId });
    expect(absent.postsRepository.incrementSavesCount).not.toHaveBeenCalled();

    const race = setup({
      savesRepository: {
        findOne: jest.fn().mockResolvedValue({ id: 'save-1' }),
        deleteById: jest.fn().mockResolvedValue(false),
      },
    });
    await race.service.unsave(userId, { postId });
    expect(race.postsRepository.incrementSavesCount).not.toHaveBeenCalled();

    const deleted = setup({
      savesRepository: { findOne: jest.fn().mockResolvedValue({ id: 'save-1' }) },
    });
    await deleted.service.unsave(userId, { postId });
    expect(deleted.postsRepository.incrementSavesCount).toHaveBeenCalledWith(postId, -1);
    expect(deleted.feedVersionService.bumpUserVersion).toHaveBeenCalledWith(userId);
  });

  it('returns save status only for posts visible to the viewer', async () => {
    const saved = setup({
      savesRepository: { findOne: jest.fn().mockResolvedValue({ id: 'save-1' }) },
    });
    await expect(saved.service.getStatus(userId, { postId })).resolves.toEqual({ saved: true, postId });

    const hidden = setup({ postsService: { canViewerSeePost: jest.fn().mockResolvedValue(false) } });
    await expect(hidden.service.getStatus(userId, { postId })).resolves.toEqual({ saved: false, postId });
    expect(hidden.savesRepository.findOne).not.toHaveBeenCalled();
  });

  it('paginates saved posts and removes posts that are no longer visible', async () => {
    const ids = [postId, '507f191e810c19729de860eb'];
    const posts = [{ id: ids[0] }, { id: ids[1] }];
    const ctx = setup({
      savesRepository: {
        findUserSavedPostIds: jest.fn().mockResolvedValue(ids),
        countByUser: jest.fn().mockResolvedValue(5),
      },
      postsRepository: { findManyByIds: jest.fn().mockResolvedValue(posts) },
      postsService: {
        canViewerSeePost: jest.fn().mockResolvedValueOnce(true).mockResolvedValueOnce(false),
      },
    });

    const result = await ctx.service.getMySavedPosts(userId, {
      page: 2,
      limit: 2,
      sortOrder: SortOrder.ASC,
    });

    expect(ctx.savesRepository.findUserSavedPostIds).toHaveBeenCalledWith(userId, 2, 2, { createdAt: 1 });
    expect(result).toMatchObject({ items: [posts[0]], total: 4, page: 2, limit: 2 });
  });
  it('returns saved false from status when post no longer exists', async () => {
    const savesRepository = {
      findOne: jest.fn(),
    };
    const postsRepository = {
      findById: jest.fn().mockResolvedValue(null),
    };
    const blocksRepository = {
      findAnyBetween: jest.fn().mockResolvedValue(null),
    };

    const service = new SavesService(
      savesRepository as any,
      postsRepository as any,
      { bumpGlobalVersion: jest.fn() } as any,
      { createSaveNotification: jest.fn() } as any,
      blocksRepository as any,
      { canViewerSeePost: jest.fn().mockResolvedValue(true) } as any,
    );

    await expect(
      service.getStatus('user-1', { postId: '507f1f77bcf86cd799439011' }),
    ).resolves.toEqual({
      saved: false,
      postId: '507f1f77bcf86cd799439011',
    });
    expect(savesRepository.findOne).not.toHaveBeenCalled();
  });

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

    const service = new SavesService(
      savesRepository as any,
      postsRepository as any,
      { bumpGlobalVersion: jest.fn() } as any,
      { createSaveNotification: jest.fn() } as any,
      blocksRepository as any,
      { canViewerSeePost: jest.fn().mockResolvedValue(true) } as any,
    );

    await expect(service.save(userId, { postId })).rejects.toThrow(
      'You cannot interact with this user',
    );
    expect(savesRepository.findOne).not.toHaveBeenCalled();
  });
});
