type PromiseOptional<T> = T | Promise<T>;

export async function runBenchmark<T, R>({
  prepareTest,
  testRunner,
  runs = 100,
  id,
}: {
  prepareTest: (i: number) => PromiseOptional<{input: T; resultCheck?: (result: R) => boolean}>;
  testRunner: (input: T) => PromiseOptional<R>;
  runs?: number;
  id: string;
}): Promise<void> {
  const diffsNanoSec: bigint[] = [];

  for (let i = 0; i < runs; i++) {
    const {input, resultCheck} = await prepareTest(i);

    const start = process.hrtime.bigint();
    const result = await testRunner(input);
    const end = process.hrtime.bigint();

    if (resultCheck && !resultCheck(result)) throw Error("Result fails check test");
    diffsNanoSec.push(end - start);
  }

  const average = averageBigint(diffsNanoSec);
  const opsPerSec = 1e9 / Number(average);
  // eslint-disable-next-line no-console
  console.log(`${id}: ${opsPerSec.toPrecision(5)} ops/sec (${runs} runs)`); // ±1.74%
}

function averageBigint(arr: bigint[]): bigint {
  const total = arr.reduce((total, value) => total + value);
  return total / BigInt(arr.length);
}