import { describe, test, expect } from 'bun:test';
import { runWithConcurrency } from '../../src/client/concurrency.js';

async function* range(n: number): AsyncIterable<number> {
  for (let i = 0; i < n; i++) yield i;
}

function delay(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

describe('runWithConcurrency', () => {
  test('runs all items', async () => {
    const seen: number[] = [];
    await runWithConcurrency(range(10), async (i) => {
      seen.push(i);
    }, { concurrency: 4 });
    expect(seen.sort((a, b) => a - b)).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
  });

  test('respects concurrency cap (never exceeds N inflight)', async () => {
    let inflight = 0;
    let peak = 0;
    await runWithConcurrency(range(50), async () => {
      inflight++;
      peak = Math.max(peak, inflight);
      await delay(5);
      inflight--;
    }, { concurrency: 4 });
    expect(peak).toBeLessThanOrEqual(4);
    expect(peak).toBeGreaterThanOrEqual(2);
  });

  test('throws on first error by default (fail-fast)', async () => {
    let processed = 0;
    await expect(
      runWithConcurrency(range(20), async (i) => {
        await delay(1);
        if (i === 3) throw new Error('boom');
        processed++;
      }, { concurrency: 2 }),
    ).rejects.toThrow('boom');
    // We don't process all 20; bounded by fail-fast.
    expect(processed).toBeLessThan(20);
  });

  test('continueOnError reports each + drains', async () => {
    const errors: number[] = [];
    let processed = 0;
    await runWithConcurrency(range(10), async (i) => {
      await delay(1);
      if (i % 3 === 0) throw new Error(`bad-${i}`);
      processed++;
    }, {
      concurrency: 3,
      continueOnError: true,
      onError: (item) => errors.push(item),
    });
    expect(processed).toBe(6); // i = 1,2,4,5,7,8
    expect(errors.sort((a, b) => a - b)).toEqual([0, 3, 6, 9]);
  });

  test('aborts via signal', async () => {
    const ctrl = new AbortController();
    let processed = 0;
    setTimeout(() => ctrl.abort(), 20);
    await expect(
      runWithConcurrency(range(100), async () => {
        await delay(5);
        processed++;
      }, { concurrency: 4, signal: ctrl.signal }),
    ).rejects.toThrow();
    expect(processed).toBeLessThan(100);
  });

  test('concurrency=1 is sequential', async () => {
    const order: number[] = [];
    await runWithConcurrency(range(5), async (i) => {
      order.push(i);
      await delay(1);
    }, { concurrency: 1 });
    expect(order).toEqual([0, 1, 2, 3, 4]);
  });

  test('throws on concurrency < 1', () => {
    expect(() =>
      runWithConcurrency(range(0), async () => undefined, { concurrency: 0 }),
    ).toThrow('concurrency must be ≥ 1');
  });
});
