import { describe, expect, it, vi } from 'vitest';
import { mapAsync } from './mapAsync';
import { delay } from '../promise/delay';

describe('mapAsync', () => {
  it('maps array asynchronously', async () => {
    const arr = [1, 2, 3];

    const callback = vi.fn(async n => n * 2);
    const result = await mapAsync(arr, callback);

    expect(result).toEqual([2, 4, 6]);

    expect(callback).toHaveBeenCalledTimes(arr.length);
    expect(callback.mock.calls[0]).toEqual([1, 0, arr]);
    expect(callback.mock.calls[1]).toEqual([2, 1, arr]);
    expect(callback.mock.calls[2]).toEqual([3, 2, arr]);
  });

  it('returns empty array if given empty array', async () => {
    const arr: number[] = [];
    const callback = vi.fn(async n => n * 2);

    const result = await mapAsync(arr, callback);
    expect(result).toEqual([]);

    expect(callback).toHaveBeenCalledTimes(0);
  });

  it('propagates rejection if any callback throws', async () => {
    const arr = [1, 2, 3];
    const errorFn = async (item: number) => {
      if (item === 2) {
        throw new Error('fail');
      }
      return item;
    };
    await expect(mapAsync(arr, errorFn)).rejects.toThrow('fail');
  });

  it('respects concurrency limit', async () => {
    const arr = [1, 2, 3, 4, 5];

    let running = 0;
    let maxRunning = 0;

    const fn = vi.fn(async (item: number) => {
      running++;

      if (running > maxRunning) {
        maxRunning = running;
      }

      await delay(20);
      running--;
      return item * 2;
    });

    const concurrency = 2;
    const result = await mapAsync(arr, fn, { concurrency });

    expect(result).toEqual([2, 4, 6, 8, 10]);
    expect(maxRunning).toBeLessThanOrEqual(concurrency);
    expect(fn).toHaveBeenCalledTimes(arr.length);
  });

  it('uses full concurrency when not specified', async () => {
    const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    let running = 0;
    let maxRunning = 0;

    const fn = async (item: number) => {
      running++;
      if (running > maxRunning) {
        maxRunning = running;
      }
      await delay(20);
      running--;
      return item;
    };

    await mapAsync(arr, fn);
    expect(maxRunning).toBe(10);
  });
});
