import { Types } from 'mongoose';
import { UsersRepository } from './users.repository';

const query = <T>(value: T) => ({
  select: jest.fn().mockReturnThis(),
  sort: jest.fn().mockReturnThis(),
  skip: jest.fn().mockReturnThis(),
  limit: jest.fn().mockReturnThis(),
  exec: jest.fn().mockResolvedValue(value),
});

describe('UsersRepository', () => {
  const createRepository = () => {
    const model = {
      create: jest.fn().mockResolvedValue({ id: 'created' }),
      findById: jest.fn(() => query({ id: 'found' })),
      findOne: jest.fn(() => query({ id: 'found' })),
      findByIdAndUpdate: jest.fn(() => query({ id: 'updated' })),
      findByIdAndDelete: jest.fn(() => query({ id: 'deleted' })),
      find: jest.fn(() => query([{ id: 'found' }])),
      countDocuments: jest.fn(() => query(4)),
    };
    return { repository: new UsersRepository(model as any), model };
  };

  it('delegates basic CRUD with explicit password projection', async () => {
    const { repository, model } = createRepository();
    await expect(repository.create({ username: 'artist' })).resolves.toEqual({ id: 'created' });
    await expect(repository.findById('user-1')).resolves.toEqual({ id: 'found' });
    await expect(repository.findOne({ email: 'a@example.com' })).resolves.toEqual({ id: 'found' });
    await expect(repository.updateById('user-1', { name: 'New' })).resolves.toEqual({ id: 'updated' });
    await expect(repository.deleteById('user-1')).resolves.toEqual({ id: 'deleted' });

    await repository.findByIdWithPassword('user-1');
    const idPasswordQuery = model.findById.mock.results.at(-1)?.value;
    expect(idPasswordQuery.select).toHaveBeenCalledWith('+password');

    await repository.findOneWithPassword({ email: 'a@example.com' });
    const emailPasswordQuery = model.findOne.mock.results.at(-1)?.value;
    expect(emailPasswordQuery.select).toHaveBeenCalledWith('+password');
  });

  it.each([
    ['incrementPostsCount', 'postsCount'],
    ['incrementFollowersCount', 'followersCount'],
    ['incrementFollowingCount', 'followingCount'],
  ] as const)('%s applies an atomic counter update', async (method, field) => {
    const { repository, model } = createRepository();
    const session = { id: 'session' } as any;

    await repository[method]('user-1', 1, session);

    expect(model.findByIdAndUpdate).toHaveBeenCalledWith(
      'user-1',
      { $inc: { [field]: 1 } },
      { new: false, session },
    );
  });

  it('sets reconciled counters and presence without returning full documents', async () => {
    const { repository, model } = createRepository();
    const lastSeenAt = new Date();

    await repository.setFollowersCount('user-1', 10);
    await repository.setFollowingCount('user-1', 5);
    await repository.setPresence('user-1', true, lastSeenAt);

    expect(model.findByIdAndUpdate).toHaveBeenCalledWith(
      'user-1',
      { followersCount: 10 },
      { new: false },
    );
    expect(model.findByIdAndUpdate).toHaveBeenCalledWith(
      'user-1',
      { followingCount: 5 },
      { new: false },
    );
    expect(model.findByIdAndUpdate).toHaveBeenCalledWith(
      'user-1',
      { isOnline: true, lastSeenAt },
      { new: false },
    );
  });

  it('paginates and sorts user queries', async () => {
    const { repository, model } = createRepository();
    await expect(
      repository.findMany({ isDisabled: false }, 20, 10, { followersCount: -1 }),
    ).resolves.toEqual([{ id: 'found' }]);

    const result = model.find.mock.results.at(-1)?.value;
    expect(result.sort).toHaveBeenCalledWith({ followersCount: -1 });
    expect(result.skip).toHaveBeenCalledWith(20);
    expect(result.limit).toHaveBeenCalledWith(10);
  });

  it('short-circuits empty bulk lookups and normalizes deduplicated usernames', async () => {
    const { repository, model } = createRepository();
    await expect(repository.findManyByIds([])).resolves.toEqual([]);
    await expect(repository.findByUsernames([])).resolves.toEqual([]);
    expect(model.find).not.toHaveBeenCalled();

    const first = new Types.ObjectId().toString();
    const second = new Types.ObjectId().toString();
    await repository.findManyByIds([first, second]);
    expect(model.find).toHaveBeenCalledWith({
      _id: { $in: [new Types.ObjectId(first), new Types.ObjectId(second)] },
    });

    await repository.findByUsernames(['Artist', 'artist', 'OTHER']);
    expect(model.find).toHaveBeenCalledWith({ username: { $in: ['artist', 'other'] } });
  });

  it('counts and ranks suggestion candidates', async () => {
    const { repository, model } = createRepository();
    await expect(repository.count({ isDisabled: false })).resolves.toBe(4);
    await repository.findSuggestionCandidates({ isPrivate: false }, 12);

    const result = model.find.mock.results.at(-1)?.value;
    expect(result.sort).toHaveBeenCalledWith({
      isVerified: -1,
      followersCount: -1,
      createdAt: -1,
    });
    expect(result.limit).toHaveBeenCalledWith(12);
  });
});
