import { XSadd } from 'ml-xsadd';
import { it, describe, beforeEach, expect } from 'vitest';

import { Random } from '../Random';

let random: Random;
describe('random.choice', () => {
  beforeEach(() => {
    random = new Random(28);
  });
  it('should return all elements', () => {
    random = new Random();
    expect(random.choice(13, { size: 13 }).sort((a, b) => a - b)).toEqual([
      0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
    ]);
  });

  it('should throw because size option is too large', () => {
    expect(() => {
      random.choice(2, { size: 3 });
    }).toThrow(/too large/);
  });

  it('should select elements without replacement', () => {
    const xsadd = new XSadd(28);
    random = new Random(xsadd.random);
    expect(random.choice(10, { size: 10 })).toMatchSnapshot();
  });

  it('should choose values with replacement', () => {
    expect(random.choice(10, { size: 20, replace: true })).toMatchSnapshot();
  });

  it('pass values as an array', () => {
    expect(
      random
        .choice([1, 2, 3, 4, 5, 6, 7, 8, 9], {
          size: 9,
        })
        .sort((a, b) => a - b),
    ).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
  });

  it('should produce choice given probabilities', () => {
    const samples = 10000;
    const r = random.choice(2, {
      size: samples,
      replace: true,
      probabilities: [0.3, 0.7],
    });
    const count = r.filter((value) => value === 0).length;
    expect(count / samples).toBeCloseTo(0.3, 2);
  });

  it('should throw if probabilities option has the wrong size', () => {
    expect(() => {
      random.choice(3, { replace: true, probabilities: [0.5, 0.5] });
    }).toThrow(
      'the length of probabilities option should be equal to the number of choices',
    );
  });

  it('should throw if probabilities option does not sum to 1', () => {
    expect(() => {
      random.choice(3, { replace: true, probabilities: [0.1, 0.5, 0.4001] });
    }).toThrow('probabilities should sum to 1');
  });

  it('choice with probabilities and no replacement not implemented', () => {
    expect(() => {
      random.choice(3, { replace: false, probabilities: [0.1, 0.5, 0.4] });
    }).toThrow(/not implemented/);
  });
});

describe('random.random', () => {
  it('should generate random numbers', () => {
    random = new Random(14);
    const r: number[] = [];
    for (let i = 0; i < 10; i++) {
      r.push(random.random());
    }
    expect(r).toMatchSnapshot();
  });
});

describe('random.randomSample', () => {
  it('should generate an array of random numbers', () => {
    const numbers = random.randomSample(10);
    expect(numbers).toHaveLength(10);
    for (const num of numbers) expect(num).toBeLessThan(1);
    for (const num of numbers) expect(num).toBeGreaterThanOrEqual(0);
    expect(numbers).toMatchSnapshot();
  });
});

describe('random.randInt', () => {
  it('should generate integers within an range with uniform distribution', () => {
    const n = 50000;
    const low = 4;
    const high = 8;
    const values: number[] = [];
    for (let i = 0; i < n; i++) {
      values.push(random.randInt(low, high));
    }
    const expFreq = 1 / (high - low);
    expect(freq(values, 4)).toBeCloseTo(expFreq);
    expect(freq(values, 5)).toBeCloseTo(expFreq);
    expect(freq(values, 6)).toBeCloseTo(expFreq);
    expect(freq(values, 7)).toBeCloseTo(expFreq);

    expect(freq(values, 3)).toEqual(0);
    expect(freq(values, 8)).toEqual(0);
  });

  it('if only low is specified, low is 0 and high is low', () => {
    const rand1 = new Random(12);
    const rand2 = new Random(12);
    const n = 20;
    const values1: number[] = [];
    const values2: number[] = [];
    for (let i = 0; i < n; i++) {
      values1.push(rand1.randInt(2));
      values2.push(rand2.randInt(0, 2));
    }
    expect(values1).toEqual(values2);
  });
});

function freq(arr: number[], n: number) {
  return arr.filter((value) => value === n).length / arr.length;
}
