import {describe, expect, test} from 'bun:test'

import {mapLimit} from '@/lib/concurrency'

// resolve after a microtask so several workers overlap before any settles
const tick = () => new Promise((r) => setTimeout(r, 0))

describe('mapLimit', () => {
  test('maps in input order regardless of completion order', async () => {
    const out = await mapLimit([1, 2, 3, 4], 2, async (n) => {
      await new Promise((r) => setTimeout(r, (5 - n) * 5))
      return n * 10
    })
    expect(out).toEqual([10, 20, 30, 40])
  })

  test('never exceeds the concurrency limit', async () => {
    let active = 0
    let peak = 0
    await mapLimit(
      Array.from({length: 12}, (_, i) => i),
      3,
      async () => {
        active++
        peak = Math.max(peak, active)
        await tick()
        active--
      },
    )
    expect(peak).toBe(3)
  })

  test('runs every item even when limit exceeds the count', async () => {
    const seen: number[] = []
    await mapLimit([1, 2, 3], 10, async (n) => {
      seen.push(n)
    })
    expect(seen.sort()).toEqual([1, 2, 3])
  })

  test('an empty list resolves to an empty array without running fn', async () => {
    let calls = 0
    const out = await mapLimit([], 4, async () => calls++)
    expect(out).toEqual([])
    expect(calls).toBe(0)
  })

  test('propagates the first rejection', async () => {
    await expect(
      mapLimit([1, 2, 3], 2, async (n) => {
        if (n === 2) throw new Error('boom')
        return n
      }),
    ).rejects.toThrow('boom')
  })
})
