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

describe('FollowsService', () => {
  const currentUserId = '507f1f77bcf86cd799439011';
  const targetUserId = '507f191e810c19729de860ea';
  const requestId = '507f191e810c19729de860eb';

  const setup = (overrides: Record<string, Record<string, jest.Mock>> = {}) => {
    const followsRepository = {
      findOne: jest.fn().mockResolvedValue(null),
      create: jest.fn().mockResolvedValue({ id: 'follow-1' }),
      deleteById: jest.fn().mockResolvedValue(true),
      deleteRelationship: jest.fn().mockResolvedValue(false),
      deletePendingRequest: jest.fn().mockResolvedValue(undefined),
      isDuplicateKeyError: jest.fn().mockReturnValue(false),
      count: jest.fn().mockResolvedValue(0),
      upsertPendingRequest: jest.fn().mockResolvedValue({ id: requestId }),
      findFollowerUserIds: jest.fn().mockResolvedValue([]),
      findFollowingUserIds: jest.fn().mockResolvedValue([]),
      findFollowingIds: jest.fn().mockResolvedValue([]),
      findPendingRequest: jest.fn().mockResolvedValue(null),
      findPendingRequestsForTarget: jest.fn().mockResolvedValue([]),
      countPendingRequestsForTarget: jest.fn().mockResolvedValue(0),
      updateRequestStatus: jest.fn().mockResolvedValue(null),
      findRequestByIdForTarget: jest.fn().mockResolvedValue(null),
      runInTransaction: jest.fn().mockImplementation(async (operation: (session?: unknown) => Promise<unknown>) => operation(undefined)),
      ...overrides.followsRepository,
    };
    const usersRepository = {
      findById: jest.fn().mockImplementation(async (id: string) => ({
        id,
        isDisabled: false,
        isPrivate: false,
        musicRoles: [],
        musicGenres: [],
        favoriteInstruments: [],
        favoriteMaqamat: [],
        followersCount: 0,
      })),
      setFollowingCount: jest.fn().mockResolvedValue(undefined),
      setFollowersCount: jest.fn().mockResolvedValue(undefined),
      incrementFollowingCount: jest.fn().mockResolvedValue(undefined),
      incrementFollowersCount: jest.fn().mockResolvedValue(undefined),
      findManyByIds: jest.fn().mockResolvedValue([]),
      findSuggestionCandidates: jest.fn().mockResolvedValue([]),
      ...overrides.usersRepository,
    };
    const outboxService = {
      enqueueFollowNotification: jest.fn().mockResolvedValue(undefined),
      enqueueFollowRequestApprovedNotification: jest.fn().mockResolvedValue(undefined),
      createFollowRequestApprovedEvent: jest.fn().mockResolvedValue(`follow.request.approved:${requestId}`),
      enqueuePersistedEvent: jest.fn().mockResolvedValue(undefined),
      ...overrides.outboxService,
    };
    const feedVersionService = {
      bumpGlobalVersion: jest.fn().mockResolvedValue(1),
      bumpUserVersion: jest.fn().mockResolvedValue(1),
      ...overrides.feedVersionService,
    };
    const blocksRepository = {
      findAnyBetween: jest.fn().mockResolvedValue(null),
      ...overrides.blocksRepository,
    };
    const service = new FollowsService(
      followsRepository as any,
      usersRepository as any,
      outboxService as any,
      feedVersionService as any,
      blocksRepository as any,
    );
    return {
      service,
      followsRepository,
      usersRepository,
      outboxService,
      feedVersionService,
      blocksRepository,
    };
  };

  it('validates toggle targets before querying repositories', async () => {
    const invalid = setup();
    await expect(invalid.service.toggleFollow(currentUserId, { targetUserId: 'bad-id' })).rejects.toThrow(
      'Invalid target user id',
    );
    expect(invalid.usersRepository.findById).not.toHaveBeenCalled();

    const self = setup();
    await expect(self.service.toggleFollow(currentUserId, { targetUserId: currentUserId })).rejects.toThrow(
      'You cannot follow yourself',
    );
  });

  it('rejects disabled, missing and blocked toggle targets', async () => {
    const disabled = setup({ usersRepository: { findById: jest.fn().mockResolvedValue({ isDisabled: true }) } });
    await expect(disabled.service.toggleFollow(currentUserId, { targetUserId })).rejects.toThrow(
      'Target user not found',
    );

    const blocked = setup({ blocksRepository: { findAnyBetween: jest.fn().mockResolvedValue({ id: 'block' }) } });
    await expect(blocked.service.toggleFollow(currentUserId, { targetUserId })).rejects.toThrow(
      'You cannot follow this user',
    );
    expect(blocked.followsRepository.create).not.toHaveBeenCalled();
  });

  it('toggles an existing follow off and synchronizes both counters', async () => {
    const ctx = setup({
      followsRepository: {
        findOne: jest.fn().mockResolvedValue({ id: 'follow-1' }),
        count: jest.fn().mockResolvedValueOnce(3).mockResolvedValueOnce(7),
      },
    });

    await expect(ctx.service.toggleFollow(currentUserId, { targetUserId })).resolves.toEqual({ following: false });
    expect(ctx.followsRepository.deleteById).toHaveBeenCalledWith('follow-1');
    expect(ctx.usersRepository.setFollowingCount).toHaveBeenCalledWith(currentUserId, 3);
    expect(ctx.usersRepository.setFollowersCount).toHaveBeenCalledWith(targetUserId, 7);
    expect(ctx.feedVersionService.bumpUserVersion).toHaveBeenCalledWith(currentUserId);
  });

  it('creates a pending request instead of following a private user', async () => {
    const ctx = setup({
      usersRepository: {
        findById: jest.fn().mockResolvedValue({ id: targetUserId, isDisabled: false, isPrivate: true }),
      },
    });

    await expect(ctx.service.toggleFollow(currentUserId, { targetUserId })).resolves.toEqual({
      following: false,
      requested: true,
      requestId,
    });
    expect(ctx.followsRepository.create).not.toHaveBeenCalled();
    expect(ctx.feedVersionService.bumpUserVersion).not.toHaveBeenCalled();
  });

  it('handles a duplicate-key race as a successful public follow', async () => {
    const duplicate = new Error('duplicate');
    const ctx = setup({
      followsRepository: {
        create: jest.fn().mockRejectedValue(duplicate),
        isDuplicateKeyError: jest.fn().mockReturnValue(true),
        count: jest.fn().mockResolvedValueOnce(4).mockResolvedValueOnce(9),
      },
    });

    await expect(ctx.service.toggleFollow(currentUserId, { targetUserId })).resolves.toEqual({ following: true });
    expect(ctx.outboxService.enqueueFollowNotification).not.toHaveBeenCalled();
    expect(ctx.usersRepository.setFollowingCount).toHaveBeenCalledWith(currentUserId, 4);

    const failure = setup({
      followsRepository: {
        create: jest.fn().mockRejectedValue(new Error('database down')),
        isDuplicateKeyError: jest.fn().mockReturnValue(false),
      },
    });
    await expect(failure.service.toggleFollow(currentUserId, { targetUserId })).rejects.toThrow('database down');
  });

  it('returns a detailed private follow-request response', async () => {
    const ctx = setup({
      usersRepository: {
        findById: jest.fn().mockResolvedValue({ id: targetUserId, isDisabled: false, isPrivate: true }),
      },
      followsRepository: { count: jest.fn().mockResolvedValueOnce(2).mockResolvedValueOnce(5) },
    });

    await expect(ctx.service.followUser(currentUserId, targetUserId)).resolves.toEqual({
      message: 'Follow request sent',
      following: false,
      isFollowing: false,
      targetUserId,
      followersCount: 5,
      followingCount: 2,
      requested: true,
      requestId,
    });
  });

  it('follows a public account and returns synchronized counts', async () => {
    const ctx = setup({
      followsRepository: { count: jest.fn().mockResolvedValueOnce(6).mockResolvedValueOnce(11) },
    });

    await expect(ctx.service.followUser(currentUserId, targetUserId)).resolves.toMatchObject({
      message: 'User followed successfully',
      isFollowing: true,
      followersCount: 11,
      followingCount: 6,
    });
    expect(ctx.outboxService.enqueueFollowNotification).toHaveBeenCalledWith(
      currentUserId,
      targetUserId,
      'follow-1',
    );
  });

  it('validates followUser and blocks forbidden relationships', async () => {
    const invalid = setup();
    await expect(invalid.service.followUser(currentUserId, 'bad')).rejects.toThrow('Invalid target user id');

    const blocked = setup({ blocksRepository: { findAnyBetween: jest.fn().mockResolvedValue({}) } });
    await expect(blocked.service.followUser(currentUserId, targetUserId)).rejects.toThrow(
      'You cannot follow this user',
    );
    expect(blocked.usersRepository.findById).not.toHaveBeenCalled();
  });

  it('unfollows idempotently and always resynchronizes counters', async () => {
    const ctx = setup({
      followsRepository: {
        findOne: jest.fn().mockResolvedValue({ id: 'follow-1' }),
        count: jest.fn().mockResolvedValueOnce(1).mockResolvedValueOnce(8),
      },
    });

    await expect(ctx.service.unfollowUser(currentUserId, targetUserId)).resolves.toMatchObject({
      message: 'User unfollowed successfully',
      isFollowing: false,
      followingCount: 1,
      followersCount: 8,
    });
    expect(ctx.feedVersionService.bumpUserVersion).toHaveBeenCalledWith(currentUserId);

    const absent = setup();
    await absent.service.unfollowUser(currentUserId, targetUserId);
    expect(absent.followsRepository.deleteById).not.toHaveBeenCalled();
    expect(absent.feedVersionService.bumpUserVersion).not.toHaveBeenCalled();
  });

  it('rejects invalid unfollow targets', async () => {
    const ctx = setup();
    await expect(ctx.service.unfollowUser(currentUserId, 'bad')).rejects.toThrow('Invalid target user id');
    await expect(ctx.service.unfollowUser(currentUserId, currentUserId)).rejects.toThrow(
      'You cannot follow yourself',
    );
  });

  it('removes only the requested follower, synchronizes counts and revokes feed access', async () => {
    const followerId = targetUserId;
    const ctx = setup({
      followsRepository: {
        deleteRelationship: jest.fn().mockResolvedValue(true),
        count: jest.fn().mockResolvedValueOnce(1).mockResolvedValueOnce(8),
      },
    });

    await expect(ctx.service.removeFollower(currentUserId, followerId)).resolves.toEqual({
      message: 'Follower removed successfully',
      followerId,
      isFollowing: false,
      followersCount: 8,
      followingCount: 1,
    });
    expect(ctx.followsRepository.deleteRelationship).toHaveBeenCalledWith(
      followerId,
      currentUserId,
    );
    expect(ctx.followsRepository.deletePendingRequest).toHaveBeenCalledWith(
      followerId,
      currentUserId,
    );
    expect(ctx.usersRepository.setFollowingCount).toHaveBeenCalledWith(followerId, 1);
    expect(ctx.usersRepository.setFollowersCount).toHaveBeenCalledWith(currentUserId, 8);
    expect(ctx.feedVersionService.bumpUserVersion).toHaveBeenCalledWith(followerId);
  });

  it('removes a missing follower idempotently without targeting another account', async () => {
    const ctx = setup();
    await expect(ctx.service.removeFollower(currentUserId, targetUserId)).resolves.toMatchObject({
      isFollowing: false,
      followerId: targetUserId,
    });
    expect(ctx.followsRepository.deleteRelationship).toHaveBeenCalledTimes(1);
    expect(ctx.followsRepository.deleteRelationship).toHaveBeenCalledWith(
      targetUserId,
      currentUserId,
    );
  });

  it('rejects invalid remove-follower targets', async () => {
    const ctx = setup();
    await expect(ctx.service.removeFollower(currentUserId, 'bad')).rejects.toThrow(
      'Invalid follower user id',
    );
    await expect(ctx.service.removeFollower(currentUserId, currentUserId)).rejects.toThrow(
      'You cannot remove yourself as a follower',
    );
    expect(ctx.followsRepository.deleteRelationship).not.toHaveBeenCalled();
  });

  it('lists followers in repository order and decorates viewer state', async () => {
    const followerId = '507f191e810c19729de860ec';
    const userDoc = {
      id: followerId,
      toObject: () => ({
        _id: followerId,
        name: 'Follower',
        username: 'follower',
        followersCount: 9,
      }),
    };
    const ctx = setup({
      followsRepository: {
        findFollowerUserIds: jest.fn().mockResolvedValue([{ followerId }]),
        count: jest.fn().mockResolvedValue(1),
        findFollowingIds: jest.fn().mockResolvedValue([followerId]),
      },
      usersRepository: { findManyByIds: jest.fn().mockResolvedValue([userDoc]) },
    });

    const result = await ctx.service.getFollowers(targetUserId, {
      page: 1,
      limit: 10,
      sortOrder: SortOrder.ASC,
    }, currentUserId);

    expect(result).toMatchObject({
      total: 1,
      items: [{ name: 'Follower', username: 'follower', isFollowing: true }],
    });
    expect(ctx.followsRepository.findFollowerUserIds).toHaveBeenCalledWith(targetUserId, 0, 10, {
      createdAt: 1,
    });
  });

  it('lists following users and tolerates deleted user records', async () => {
    const firstId = '507f191e810c19729de860ec';
    const missingId = '507f191e810c19729de860ed';
    const doc = {
      id: firstId,
      toObject: () => ({ _id: firstId, username: 'one', isVerified: true }),
    };
    const ctx = setup({
      followsRepository: {
        findFollowingUserIds: jest.fn().mockResolvedValue([{ followingId: firstId }, { followingId: missingId }]),
        count: jest.fn().mockResolvedValue(2),
      },
      usersRepository: { findManyByIds: jest.fn().mockResolvedValue([doc]) },
    });

    const result = await ctx.service.getFollowing(currentUserId, { page: 1, limit: 20 });
    expect(result.items).toHaveLength(1);
    expect(result.items[0]).toMatchObject({ username: 'one', followersCount: 0, isFollowing: false });
  });

  it('validates list targets before reading follower data', async () => {
    const invalid = setup();
    await expect(invalid.service.getFollowers('bad', {})).rejects.toThrow('Invalid user id');

    const missing = setup({ usersRepository: { findById: jest.fn().mockResolvedValue(null) } });
    await expect(missing.service.getFollowing(targetUserId, {})).rejects.toThrow('Target user not found');
  });

  it('reports self, mutual and pending follow states', async () => {
    const self = setup();
    await expect(self.service.getFollowStatus(currentUserId, currentUserId)).resolves.toEqual({
      following: false,
      isFollowing: false,
      isFollowedBy: false,
      isMutual: false,
      targetUserId: currentUserId,
    });

    const mutual = setup({
      followsRepository: {
        findOne: jest.fn().mockResolvedValueOnce({ id: 'a' }).mockResolvedValueOnce({ id: 'b' }),
        findPendingRequest: jest.fn().mockResolvedValue({ id: requestId }),
      },
    });
    await expect(mutual.service.getFollowStatus(currentUserId, targetUserId)).resolves.toMatchObject({
      following: true,
      isFollowing: true,
      isFollowedBy: true,
      isMutual: true,
      requested: true,
    });

    await expect(setup().service.getFollowStatus(currentUserId, 'bad')).rejects.toThrow(
      'Invalid target user id',
    );
  });

  it('paginates pending requests', async () => {
    const items = [{ id: requestId }];
    const ctx = setup({
      followsRepository: {
        findPendingRequestsForTarget: jest.fn().mockResolvedValue(items),
        countPendingRequestsForTarget: jest.fn().mockResolvedValue(4),
      },
    });

    const result = await ctx.service.getPendingRequests(currentUserId, {
      page: 2,
      limit: 3,
      sortOrder: SortOrder.ASC,
    });
    expect(result).toMatchObject({ items, total: 4, page: 2, limit: 3 });
    expect(ctx.followsRepository.findPendingRequestsForTarget).toHaveBeenCalledWith(
      currentUserId,
      3,
      3,
      { createdAt: 1 },
    );
  });

  it('approves a request, creates the follow once and updates the requester feed', async () => {
    const ctx = setup({
      followsRepository: {
        updateRequestStatus: jest.fn().mockResolvedValue({ requesterId: { toString: () => targetUserId } }),
        count: jest.fn().mockResolvedValueOnce(2).mockResolvedValueOnce(10),
      },
    });

    await expect(ctx.service.approveRequest(currentUserId, requestId)).resolves.toEqual({
      approved: true,
      following: true,
      requesterId: targetUserId,
    });
    expect(ctx.followsRepository.create).toHaveBeenCalledWith(targetUserId, currentUserId, undefined);
    expect(ctx.feedVersionService.bumpUserVersion).toHaveBeenCalledWith(targetUserId);
    expect(ctx.outboxService.createFollowRequestApprovedEvent).toHaveBeenCalledWith(
      currentUserId,
      targetUserId,
      requestId,
      undefined,
    );

    const existing = setup({
      followsRepository: {
        updateRequestStatus: jest.fn().mockResolvedValue({ requesterId: { toString: () => targetUserId } }),
        findOne: jest.fn().mockResolvedValue({ id: 'already' }),
      },
    });
    await existing.service.approveRequest(currentUserId, requestId);
    expect(existing.followsRepository.create).not.toHaveBeenCalled();
    expect(existing.outboxService.createFollowRequestApprovedEvent).toHaveBeenCalledTimes(1);
  });

  it('validates approve and reject requests and handles missing records', async () => {
    await expect(setup().service.approveRequest(currentUserId, 'bad')).rejects.toThrow(
      'Invalid follow request id',
    );
    await expect(setup().service.approveRequest(currentUserId, requestId)).rejects.toThrow(
      'Follow request not found',
    );
    expect(setup().outboxService.createFollowRequestApprovedEvent).not.toHaveBeenCalled();
    await expect(setup().service.rejectRequest(currentUserId, 'bad')).rejects.toThrow(
      'Invalid follow request id',
    );
    await expect(setup().service.rejectRequest(currentUserId, requestId)).rejects.toThrow(
      'Follow request not found',
    );

    const rejected = setup({
      followsRepository: {
        updateRequestStatus: jest.fn().mockResolvedValue({ requesterId: { toString: () => targetUserId } }),
      },
    });
    await expect(rejected.service.rejectRequest(currentUserId, requestId)).resolves.toEqual({
      rejected: true,
      requesterId: targetUserId,
    });
  });

  it('keeps duplicate approvals idempotent and propagates an outbox transaction failure', async () => {
    const duplicate = setup({
      followsRepository: {
        updateRequestStatus: jest.fn().mockResolvedValue(null),
        findRequestByIdForTarget: jest.fn().mockResolvedValue({
          status: 'approved', requesterId: { toString: () => targetUserId },
        }),
      },
    });
    await expect(duplicate.service.approveRequest(currentUserId, requestId)).resolves.toEqual({
      approved: true, following: true, requesterId: targetUserId,
    });
    expect(duplicate.followsRepository.create).not.toHaveBeenCalled();
    expect(duplicate.outboxService.createFollowRequestApprovedEvent).not.toHaveBeenCalled();

    const failed = setup({
      followsRepository: { updateRequestStatus: jest.fn().mockResolvedValue({ requesterId: { toString: () => targetUserId } }) },
      outboxService: { createFollowRequestApprovedEvent: jest.fn().mockRejectedValue(new Error('outbox insert failed')) },
    });
    await expect(failed.service.approveRequest(currentUserId, requestId)).rejects.toThrow('outbox insert failed');
    expect(failed.feedVersionService.bumpUserVersion).not.toHaveBeenCalled();
  });

  it('ranks and explains personalized suggestions with stable pagination', async () => {
    jest.spyOn(Math, 'random').mockReturnValue(0.25);
    const current = {
      id: currentUserId,
      musicRoles: ['Singer'],
      musicGenres: ['Rock'],
      favoriteInstruments: ['Guitar'],
      favoriteMaqamat: ['Bayati'],
      location: ' Riyadh ',
    };
    const best = {
      id: targetUserId,
      musicRoles: ['singer'],
      musicGenres: ['rock'],
      favoriteInstruments: ['guitar'],
      favoriteMaqamat: ['bayati'],
      location: 'riyadh',
      followersCount: 900,
      isVerified: true,
    };
    const other = {
      id: '507f191e810c19729de860ec',
      musicRoles: [],
      musicGenres: [],
      favoriteInstruments: [],
      favoriteMaqamat: [],
      location: 'Jeddah',
      followersCount: 1,
      isVerified: false,
    };
    const ctx = setup({
      usersRepository: {
        findById: jest.fn().mockResolvedValue(current),
        findSuggestionCandidates: jest.fn().mockResolvedValue([other, best]),
      },
      followsRepository: { findFollowingIds: jest.fn().mockResolvedValue(['507f191e810c19729de860ed']) },
    });

    const result = await ctx.service.getSuggestions(currentUserId, { page: 1, limit: 10 });
    expect(result.items[0]).toMatchObject({
      user: best,
      reasons: ['shared_music_roles', 'shared_genres', 'same_location', 'verified_account', 'popular_creator'],
    });
    expect(ctx.usersRepository.findSuggestionCandidates).toHaveBeenCalledWith(
      expect.objectContaining({ isDisabled: false }),
      500,
    );
    expect(result.total).toBe(2);

    const ascending = await ctx.service.getSuggestions(currentUserId, {
      page: 1,
      limit: 1,
      sortOrder: SortOrder.ASC,
    });
    expect(ascending.items[0].user).toBe(other);
    jest.restoreAllMocks();
  });

  it('rejects suggestions for a missing current user', async () => {
    const ctx = setup({ usersRepository: { findById: jest.fn().mockResolvedValue(null) } });
    await expect(ctx.service.getSuggestions(currentUserId, {})).rejects.toThrow('Current user not found');
  });
  it('keeps follow successful even if notification creation fails and resyncs counters', async () => {
    const currentUserId = '507f1f77bcf86cd799439011';
    const targetUserId = '507f191e810c19729de860ea';

    const followsRepository = {
      findOne: jest.fn().mockResolvedValue(null),
      create: jest.fn().mockResolvedValue({ id: 'follow-1' }),
      count: jest
        .fn()
        .mockResolvedValueOnce(6)
        .mockResolvedValueOnce(14),
    };
    const usersRepository = {
      findById: jest.fn().mockResolvedValue({ id: targetUserId }),
      setFollowingCount: jest.fn().mockResolvedValue(undefined),
      setFollowersCount: jest.fn().mockResolvedValue(undefined),
    };
    const outboxService = {
      enqueueFollowNotification: jest.fn().mockRejectedValue(new Error('socket down')),
    };
    const blocksRepository = {
      findAnyBetween: jest.fn().mockResolvedValue(null),
    };

    const service = new FollowsService(
      followsRepository as any,
      usersRepository as any,
      outboxService as any,
      { bumpGlobalVersion: jest.fn(), bumpUserVersion: jest.fn().mockResolvedValue(1) } as any,
      blocksRepository as any,
    );

    await expect(service.toggleFollow(currentUserId, { targetUserId })).resolves.toEqual({
      following: true,
    });
    expect(usersRepository.setFollowingCount).toHaveBeenCalledWith(currentUserId, 6);
    expect(usersRepository.setFollowersCount).toHaveBeenCalledWith(targetUserId, 14);
    expect(outboxService.enqueueFollowNotification).toHaveBeenCalledWith(
      currentUserId,
      targetUserId,
      'follow-1',
    );
  });

  it('prevents users from following themselves', async () => {
    const userId = '507f1f77bcf86cd799439011';
    const service = new FollowsService(
      {} as any,
      {} as any,
      {} as any,
      { bumpGlobalVersion: jest.fn(), bumpUserVersion: jest.fn() } as any,
      { findAnyBetween: jest.fn() } as any,
    );

    await expect(service.followUser(userId, userId)).rejects.toThrow('You cannot follow yourself');
  });

  it('returns already-following response without creating duplicate notification', async () => {
    const currentUserId = '507f1f77bcf86cd799439011';
    const targetUserId = '507f191e810c19729de860ea';
    const followsRepository = {
      findOne: jest.fn().mockResolvedValue({ id: 'existing-follow' }),
      count: jest.fn().mockResolvedValueOnce(1).mockResolvedValueOnce(2),
      create: jest.fn(),
    };
    const usersRepository = {
      findById: jest.fn().mockResolvedValue({ id: targetUserId, isDisabled: false, isPrivate: false }),
      setFollowingCount: jest.fn(),
      setFollowersCount: jest.fn(),
    };
    const outboxService = {
      enqueueFollowNotification: jest.fn(),
    };
    const service = new FollowsService(
      followsRepository as any,
      usersRepository as any,
      outboxService as any,
      { bumpGlobalVersion: jest.fn(), bumpUserVersion: jest.fn() } as any,
      { findAnyBetween: jest.fn().mockResolvedValue(null) } as any,
    );

    await expect(service.followUser(currentUserId, targetUserId)).resolves.toMatchObject({
      message: 'Already following user',
      isFollowing: true,
      followersCount: 2,
      followingCount: 1,
    });
    expect(followsRepository.create).not.toHaveBeenCalled();
    expect(outboxService.enqueueFollowNotification).not.toHaveBeenCalled();
  });
});
