/**
* @license Apache-2.0
*
* Copyright (c) 2018 The Stdlib Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*    http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

'use strict';

// MODULES //

var tape = require( 'tape' );
var Readable = require( 'readable-stream' ).Readable;
var now = require( '@stdlib/time/now' );
var geometric = require( '@stdlib/random/base/geometric' ).factory;
var isBuffer = require( '@stdlib/assert/is-buffer' );
var isnan = require( '@stdlib/math/base/assert/is-nan' );
var isUint32Array = require( '@stdlib/assert/is-uint32array' );
var UINT32_MAX = require( '@stdlib/constants/uint32/max' );
var Uint32Array = require( '@stdlib/array/uint32' );
var minstd = require( '@stdlib/random/base/minstd' );
var inspectStream = require( '@stdlib/streams/node/inspect-sink' );
var randomStream = require( './../lib/main.js' );


// TESTS //

tape( 'main export is a function', function test( t ) {
	t.ok( true, __filename );
	t.equal( typeof randomStream, 'function', 'main export is a function' );
	t.end();
});

tape( 'the function throws an error if success probability `p` is not a valid probability value', function test( t ) {
	var values;
	var i;

	values = [
		'5',
		-1.0,
		1.1,
		1.5,
		null,
		true,
		false,
		void 0,
		NaN,
		[],
		{},
		function noop() {}
	];

	for ( i = 0; i < values.length; i++ ) {
		t.throws( badValue( values[i] ), TypeError, 'throws an error when provided '+values[i] );
	}
	t.end();

	function badValue( value ) {
		return function badValue() {
			randomStream( value );
		};
	}
});

tape( 'the function throws an error if provided an options argument which is not an object', function test( t ) {
	var values;
	var i;

	values = [
		'abc',
		5,
		null,
		true,
		false,
		void 0,
		NaN,
		[],
		function noop() {}
	];

	for ( i = 0; i < values.length; i++ ) {
		t.throws( badValue( values[i] ), TypeError, 'throws an error when provided '+values[i] );
	}
	t.end();

	function badValue( value ) {
		return function badValue() {
			randomStream( 0.8, value );
		};
	}
});

tape( 'the function throws an error if provided an invalid `iter` option', function test( t ) {
	var values;
	var i;

	values = [
		'abc',
		-5,
		3.14,
		null,
		true,
		false,
		void 0,
		NaN,
		[],
		{},
		function noop() {}
	];

	for ( i = 0; i < values.length; i++ ) {
		t.throws( badValue( values[i] ), TypeError, 'throws an error when provided '+values[i] );
	}
	t.end();

	function badValue( value ) {
		return function badValue() {
			randomStream( 0.8, {
				'iter': value
			});
		};
	}
});

tape( 'if provided a `prng` option which is not a function, the function throws an error', function test( t ) {
	var values;
	var i;

	values = [
		'5',
		3.14,
		NaN,
		true,
		false,
		null,
		void 0,
		[],
		{}
	];

	for ( i = 0; i < values.length; i++ ) {
		t.throws( badValue( values[i] ), TypeError, 'throws a type error when provided '+values[i] );
	}
	t.end();

	function badValue( value ) {
		return function badValue() {
			randomStream( 0.8, {
				'prng': value
			});
		};
	}
});

tape( 'if provided a `copy` option which is not a boolean, the function throws an error', function test( t ) {
	var values;
	var i;

	values = [
		'5',
		5,
		NaN,
		null,
		void 0,
		{},
		[],
		function noop() {}
	];

	for ( i = 0; i < values.length; i++ ) {
		t.throws( badValue( values[i] ), TypeError, 'throws a type error when provided '+values[i] );
	}
	t.end();

	function badValue( value ) {
		return function badValue() {
			randomStream( 0.8, {
				'copy': value
			});
		};
	}
});

tape( 'if provided a `seed` which is not a positive integer or a non-empty array-like object, the function throws an error', function test( t ) {
	var values;
	var i;

	values = [
		'5',
		3.14,
		0.0,
		-5.0,
		NaN,
		true,
		false,
		null,
		void 0,
		{},
		[],
		function noop() {}
	];

	for ( i = 0; i < values.length; i++ ) {
		t.throws( badValue( values[i] ), TypeError, 'throws a type error when provided '+values[i] );
	}
	t.end();

	function badValue( value ) {
		return function badValue() {
			randomStream( 0.8, {
				'seed': value
			});
		};
	}
});

tape( 'the function throws a range error if provided a `seed` which is an integer greater than the maximum unsigned 32-bit integer', function test( t ) {
	var values;
	var i;

	values = [
		UINT32_MAX + 1,
		UINT32_MAX + 2,
		UINT32_MAX + 3
	];

	for ( i = 0; i < values.length; i++ ) {
		t.throws( badValue( values[i] ), RangeError, 'throws a range error when provided '+values[i] );
	}
	t.end();

	function badValue( value ) {
		return function badValue() {
			randomStream( 0.8, {
				'seed': value
			});
		};
	}
});

tape( 'if provided a `state` option which is not a Uint32Array, the function throws an error', function test( t ) {
	var values;
	var i;

	values = [
		'5',
		5,
		NaN,
		true,
		false,
		null,
		void 0,
		{},
		[],
		function noop() {}
	];

	for ( i = 0; i < values.length; i++ ) {
		t.throws( badValue( values[i] ), TypeError, 'throws a type error when provided '+values[i] );
	}
	t.end();

	function badValue( value ) {
		return function badValue() {
			randomStream( 0.8, {
				'state': value
			});
		};
	}
});

tape( 'if provided an invalid `state` option, the function throws an error', function test( t ) {
	var values;
	var i;

	values = [
		new Uint32Array( 0 ),
		new Uint32Array( 10 ),
		new Uint32Array( 100 )
	];

	for ( i = 0; i < values.length; i++ ) {
		t.throws( badValue( values[i] ), RangeError, 'throws an error when provided '+values[i] );
	}
	t.end();

	function badValue( value ) {
		return function badValue() {
			randomStream( 0.8, {
				'state': value
			});
		};
	}
});

tape( 'if provided an invalid readable stream option, the function throws an error', function test( t ) {
	var values;
	var i;

	values = [
		'5',
		5,
		NaN,
		null,
		void 0,
		{},
		[],
		function noop() {}
	];

	for ( i = 0; i < values.length; i++ ) {
		t.throws( badValue( values[i] ), TypeError, 'throws a type error when provided '+values[i] );
	}
	t.end();

	function badValue( value ) {
		return function badValue() {
			randomStream( 0.8, {
				'objectMode': value
			});
		};
	}
});

tape( 'the function is a constructor which returns a readable stream', function test( t ) {
	var RandomStream = randomStream;
	var s;

	s = new RandomStream( 0.8 );
	t.equal( s instanceof Readable, true, 'returns expected value' );
	t.end();
});

tape( 'the constructor does not require the `new` operator', function test( t ) {
	var RandomStream = randomStream;
	var s;

	s = randomStream( 0.8 );
	t.equal( s instanceof RandomStream, true, 'returns expected value' );
	t.end();
});

tape( 'the constructor returns a readable stream (no new)', function test( t ) {
	var s = randomStream( 0.8 );
	t.equal( s instanceof Readable, true, 'returns expected value' );
	t.end();
});

tape( 'the returned stream provides a method to destroy a stream (object)', function test( t ) {
	var count = 0;
	var s;

	s = randomStream( 0.8 );

	t.equal( typeof s.destroy, 'function', 'has destroy method' );

	s.on( 'error', onError );
	s.on( 'close', onClose );

	s.destroy({
		'message': 'beep'
	});

	function onError( err ) {
		count += 1;
		if ( err ) {
			t.ok( true, err.message );
		} else {
			t.ok( false, 'does not error' );
		}
		if ( count === 2 ) {
			t.end();
		}
	}
	function onClose() {
		count += 1;
		t.ok( true, 'stream closes' );
		if ( count === 2 ) {
			t.end();
		}
	}
});

tape( 'the returned stream provides a method to destroy a stream (error object)', function test( t ) {
	var count = 0;
	var s;

	s = randomStream( 0.8 );

	t.equal( typeof s.destroy, 'function', 'has destroy method' );

	s.on( 'error', onError );
	s.on( 'close', onClose );

	s.destroy( new Error( 'beep' ) );

	function onError( err ) {
		count += 1;
		if ( err ) {
			t.ok( true, err.message );
		} else {
			t.ok( false, 'does not error' );
		}
		if ( count === 2 ) {
			t.end();
		}
	}
	function onClose() {
		count += 1;
		t.ok( true, 'stream closes' );
		if ( count === 2 ) {
			t.end();
		}
	}
});

tape( 'the returned stream does not allow itself to be destroyed more than once', function test( t ) {
	var s;

	s = randomStream( 0.8 );

	s.on( 'error', onError );
	s.on( 'close', onClose );

	// If the stream is closed twice, the test will error...
	s.destroy();
	s.destroy();

	function onClose() {
		t.ok( true, 'stream closes' );
		t.end();
	}
	function onError( err ) {
		t.ok( false, err.message );
	}
});

tape( 'attached to the returned stream is the underlying PRNG', function test( t ) {
	var s = randomStream( 0.8 );

	t.equal( typeof s.PRNG, 'function', 'has property' );

	s = randomStream( 0.8, {
		'prng': minstd.normalized
	});
	t.equal( s.PRNG, minstd.normalized, 'has property' );

	t.end();
});

tape( 'attached to the returned stream is the generator seed', function test( t ) {
	var s = randomStream( 0.8, {
		'seed': 12345
	});

	t.equal( isUint32Array( s.seed ), true, 'has property' );
	t.equal( s.seed[ 0 ], 12345, 'equal to provided seed' );

	s = randomStream( 0.8, {
		'seed': 12345,
		'prng': minstd.normalized
	});
	t.equal( s.seed, null, 'equal to `null`' );

	t.end();
});

tape( 'attached to the returned stream is the generator seed (array seed)', function test( t ) {
	var actual;
	var seed;
	var s;
	var i;

	seed = [ 1234, 5678 ];
	s = randomStream( 0.8, {
		'seed': seed
	});

	actual = s.seed;
	t.equal( isUint32Array( actual ), true, 'has property' );
	for ( i = 0; i < seed.length; i++ ) {
		t.equal( actual[ i ], seed[ i ], 'returns expected value for word '+i );
	}

	t.end();
});

tape( 'attached to the returned stream is the generator seed length', function test( t ) {
	var s = randomStream( 0.8 );

	t.equal( typeof s.seedLength, 'number', 'has property' );

	s = randomStream( 0.8, {
		'prng': minstd.normalized
	});
	t.equal( s.seedLength, null, 'equal to `null`' );

	t.end();
});

tape( 'attached to the returned stream is the generator state', function test( t ) {
	var s = randomStream( 0.8 );

	t.equal( isUint32Array( s.state ), true, 'has property' );

	s = randomStream( 0.8, {
		'prng': minstd.normalized
	});
	t.equal( s.state, null, 'equal to `null`' );

	t.end();
});

tape( 'attached to the returned stream is the generator state length', function test( t ) {
	var s = randomStream( 0.8 );

	t.equal( typeof s.stateLength, 'number', 'has property' );

	s = randomStream( 0.8, {
		'prng': minstd.normalized
	});
	t.equal( s.stateLength, null, 'equal to `null`' );

	t.end();
});

tape( 'attached to the returned stream is the generator state size', function test( t ) {
	var s = randomStream( 0.8 );

	t.equal( typeof s.byteLength, 'number', 'has property' );

	s = randomStream( 0.8, {
		'prng': minstd.normalized
	});
	t.equal( s.byteLength, null, 'equal to `null`' );

	t.end();
});

tape( 'the constructor returns a stream for generating pseudorandom numbers from a geometric distribution', function test( t ) {
	var iStream;
	var result;
	var rand;
	var opts;
	var s;

	// Note: we assume that the underlying generator is the following PRNG...
	rand = geometric( 0.8, {
		'seed': 12345
	});

	opts = {
		'seed': 12345,
		'iter': 10,
		'sep': '\n'
	};
	s = randomStream( 0.8, opts );
	s.on( 'end', onEnd );

	opts = {
		'objectMode': true
	};
	iStream = inspectStream( opts, inspect );

	result = '';
	s.pipe( iStream );

	function inspect( chunk ) {
		t.equal( isBuffer( chunk ), true, 'returns a buffer' );
		result += chunk.toString();
	}

	function onEnd() {
		var i;

		t.pass( 'stream ended' );

		result = result.split( '\n' );
		t.equal( result.length, 10, 'has expected length' );
		for ( i = 0; i < result.length; i++ ) {
			t.equal( parseFloat( result[ i ] ), rand(), 'returns expected value. i: ' + i + '.' );
		}
		t.end();
	}
});

tape( 'the constructor returns a stream for generating pseudorandom numbers from a geometric distribution (object mode)', function test( t ) {
	var iStream;
	var count;
	var rand;
	var opts;
	var s;

	// Note: we assume that the underlying generator is the following PRNG...
	rand = geometric( 0.8, {
		'seed': 12345
	});

	opts = {
		'seed': 12345,
		'objectMode': true
	};
	s = randomStream( 0.8, opts );
	s.on( 'close', onClose );

	opts = {
		'objectMode': true
	};
	iStream = inspectStream( opts, inspect );

	count = 0;
	s.pipe( iStream );

	function inspect( v ) {
		count += 1;
		t.equal( rand(), v, 'returns expected value. i: '+count+'.' );
		if ( count >= 10 ) {
			s.destroy();
		}
	}

	function onClose() {
		t.pass( 'stream closed' );
		t.end();
	}
});

tape( 'the constructor supports limiting the number of iterations', function test( t ) {
	var iStream;
	var count;
	var niter;
	var opts;
	var s;

	niter = 10;
	count = 0;

	opts = {
		'iter': niter,
		'objectMode': true
	};
	s = randomStream( 0.8, opts );
	s.on( 'end', onEnd );

	opts = {
		'objectMode': true
	};
	iStream = inspectStream( opts, inspect );

	s.pipe( iStream );

	function inspect( v ) {
		count += 1;
		t.equal( typeof v, 'number', 'returns expected value' );
	}

	function onEnd() {
		t.equal( count === niter, true, 'performs expected number of iterations' );
		t.end();
	}
});

tape( 'by default, the constructor generates newline-delimited pseudorandom numbers', function test( t ) {
	var iStream;
	var result;
	var opts;
	var s;

	opts = {
		'iter': 10
	};
	s = randomStream( 0.8, opts );
	s.on( 'end', onEnd );

	iStream = inspectStream( inspect );

	result = '';
	s.pipe( iStream );

	function inspect( chunk ) {
		result += chunk.toString();
	}

	function onEnd() {
		var v;
		var i;

		result = result.split( '\n' );
		t.equal( result.length, opts.iter, 'has expected length' );
		for ( i = 0; i < result.length; i++ ) {
			v = parseFloat( result[ i ] );
			t.equal( typeof v, 'number', 'returns expected value' );
			t.equal( isnan( v ), false, 'is not NaN' );
		}
		t.end();
	}
});

tape( 'the constructor supports providing a custom separator for streamed values', function test( t ) {
	var iStream;
	var result;
	var opts;
	var s;

	opts = {
		'iter': 10,
		'sep': '--++--'
	};
	s = randomStream( 0.8, opts );
	s.on( 'end', onEnd );

	iStream = inspectStream( inspect );

	result = '';
	s.pipe( iStream );

	function inspect( chunk ) {
		result += chunk.toString();
	}

	function onEnd() {
		var v;
		var i;

		result = result.split( opts.sep );
		t.equal( result.length, opts.iter, 'has expected length' );
		for ( i = 0; i < result.length; i++ ) {
			v = parseFloat( result[ i ] );
			t.equal( typeof v, 'number', 'returns expected value' );
			t.equal( isnan( v ), false, 'is not NaN' );
		}
		t.end();
	}
});

tape( 'the constructor supports returning a seeded readable stream', function test( t ) {
	var iStream;
	var opts;
	var seed;
	var arr;
	var s1;
	var s2;
	var i;

	seed = now();

	opts = {
		'objectMode': true,
		'seed': seed,
		'iter': 10
	};
	s1 = randomStream( 0.8, opts );
	s1.on( 'end', onEnd1 );

	s2 = randomStream( 0.8, opts );
	s2.on( 'end', onEnd2 );

	t.notEqual( s1, s2, 'separate streams' );

	opts = {
		'objectMode': true
	};
	iStream = inspectStream( opts, inspect1 );

	arr = [];
	i = 0;

	s1.pipe( iStream );

	function inspect1( v ) {
		arr.push( v );
	}

	function onEnd1() {
		var iStream;
		var opts;

		opts = {
			'objectMode': true
		};
		iStream = inspectStream( opts, inspect2 );

		s2.pipe( iStream );
	}

	function inspect2( v ) {
		t.equal( v, arr[ i ], 'returns expected value' );
		i += 1;
	}

	function onEnd2() {
		t.end();
	}
});

tape( 'the constructor supports specifying the underlying PRNG', function test( t ) {
	var iStream;
	var opts;
	var s;

	opts = {
		'prng': minstd.normalized,
		'objectMode': true,
		'iter': 10
	};
	s = randomStream( 0.8, opts );
	s.on( 'end', onEnd );

	opts = {
		'objectMode': true
	};
	iStream = inspectStream( opts, inspect );

	s.pipe( iStream );

	function inspect( v ) {
		t.equal( typeof v, 'number', 'returns a number' );
	}

	function onEnd() {
		t.end();
	}
});

tape( 'the constructor supports providing a seeded underlying PRNG', function test( t ) {
	var iStream1;
	var iStream2;
	var randu;
	var seed;
	var opts;
	var FLG;
	var s1;
	var s2;
	var r1;
	var r2;

	seed = now();

	randu = minstd.factory({
		'seed': seed
	});
	opts = {
		'prng': randu.normalized,
		'objectMode': true,
		'iter': 10
	};
	s1 = randomStream( 0.8, opts );
	s1.on( 'end', onEnd );

	randu = minstd.factory({
		'seed': seed
	});
	opts = {
		'prng': randu.normalized,
		'objectMode': true,
		'iter': 10
	};
	s2 = randomStream( 0.8, opts );
	s2.on( 'end', onEnd );

	t.notEqual( s1, s2, 'separate streams' );

	opts = {
		'objectMode': true
	};
	iStream1 = inspectStream( opts, inspect1 );
	iStream2 = inspectStream( opts, inspect2 );

	r1 = [];
	r2 = [];

	s1.pipe( iStream1 );
	s2.pipe( iStream2 );

	function inspect1( v ) {
		r1.push( v );
	}

	function inspect2( v ) {
		r2.push( v );
	}

	function onEnd() {
		if ( FLG ) {
			t.deepEqual( r1, r2, 'streams expected values' );
			return t.end();
		}
		FLG = true;
	}
});

tape( 'the constructor supports specifying the underlying generator state', function test( t ) {
	var iStream;
	var state;
	var count;
	var opts;
	var arr;
	var s;

	opts = {
		'objectMode': true,
		'iter': 10,
		'siter': 5
	};
	s = randomStream( 0.8, opts );
	s.on( 'state', onState );
	s.on( 'end', onEnd1 );

	opts = {
		'objectMode': true
	};
	iStream = inspectStream( opts, inspect1 );

	count = 0;
	arr = [];

	// Move to a future state...
	s.pipe( iStream );

	function onState( s ) {
		// Only capture the first emitted state...
		if ( !state ) {
			state = s;
		}
	}

	function inspect1( v ) {
		count += 1;
		if ( count > 5 ) {
			arr.push( v );
		}
	}

	function onEnd1() {
		var iStream;
		var opts;
		var s;

		t.pass( 'first stream ended' );

		// Create another stream using the captured state:
		opts = {
			'objectMode': true,
			'iter': 5,
			'state': state
		};
		s = randomStream( 0.8, opts );
		s.on( 'end', onEnd2 );

		t.deepEqual( state, s.state, 'same state' );

		// Create a new inspect stream:
		opts = {
			'objectMode': true
		};
		iStream = inspectStream( opts, inspect2 );

		// Replay previously generated values...
		count = 0;
		s.pipe( iStream );
	}

	function inspect2( v ) {
		count += 1;
		t.equal( v, arr[ count-1 ], 'returns expected value. i: '+(count-1)+'.' );
	}

	function onEnd2() {
		t.pass( 'second stream ended' );
		t.end();
	}
});

tape( 'the constructor supports specifying a shared underlying generator state', function test( t ) {
	var iStream;
	var shared;
	var state;
	var count;
	var opts;
	var arr;
	var s;

	opts = {
		'objectMode': true,
		'iter': 10,
		'siter': 4
	};
	s = randomStream( 0.8, opts );
	s.on( 'state', onState );
	s.on( 'end', onEnd1 );

	opts = {
		'objectMode': true
	};
	iStream = inspectStream( opts, inspect1 );

	count = 0;
	arr = [];

	// Move to a future state...
	s.pipe( iStream );

	function onState( s ) {
		// Only capture the first emitted state...
		if ( !state ) {
			state = s;

			// Create a copy of the state (to prevent mutation) which will be shared by more than one PRNG:
			shared = new Uint32Array( state );
		}
	}

	function inspect1( v ) {
		count += 1;
		if ( count > 4 ) {
			arr.push( v );
		}
	}

	function onEnd1() {
		var iStream;
		var opts;
		var s;

		t.pass( 'first stream ended' );

		// Create another stream using the captured state:
		opts = {
			'objectMode': true,
			'iter': 3,
			'state': shared,
			'copy': false
		};
		s = randomStream( 0.8, opts );
		s.on( 'end', onEnd2 );

		t.deepEqual( state, s.state, 'same state' );

		// Create a new inspect stream:
		opts = {
			'objectMode': true
		};
		iStream = inspectStream( opts, inspect2 );

		// Replay previously generated values...
		count = 0;
		s.pipe( iStream );
	}

	function inspect2( v ) {
		count += 1;
		t.equal( v, arr[ count-1 ], 'returns expected value. i: '+(count-1)+'.' );
	}

	function onEnd2() {
		var iStream;
		var opts;
		var s;

		t.pass( 'second stream ended' );

		// Create another stream using the captured state:
		opts = {
			'objectMode': true,
			'iter': 3,
			'state': shared,
			'copy': false
		};
		s = randomStream( 0.8, opts );
		s.on( 'end', onEnd3 );

		t.notDeepEqual( state, s.state, 'different state' );

		// Create a new inspect stream:
		opts = {
			'objectMode': true
		};
		iStream = inspectStream( opts, inspect3 );

		// Continue replaying previously generated values...
		s.pipe( iStream );
	}

	function inspect3( v ) {
		count += 1;
		t.equal( v, arr[ count-1 ], 'returns expected value. i: '+(count-1)+'.' );
	}

	function onEnd3() {
		t.pass( 'third stream ended' );
		t.end();
	}
});

tape( 'the returned stream supports setting the underlying generator state', function test( t ) {
	var iStream;
	var state;
	var rand;
	var opts;
	var arr;
	var s;
	var i;

	rand = geometric( 0.8 );

	// Move to a future state...
	for ( i = 0; i < 5; i++ ) {
		rand();
	}
	// Capture the current state:
	state = rand.state;

	// Move to a future state...
	arr = [];
	for ( i = 0; i < 5; i++ ) {
		arr.push( rand() );
	}
	// Create a random stream:
	opts = {
		'objectMode': true,
		'iter': 5
	};
	s = randomStream( 0.8, opts );
	s.on( 'end', onEnd );

	// Set the PRNG state:
	s.state = state;

	// Create a new inspect stream:
	opts = {
		'objectMode': true
	};
	iStream = inspectStream( opts, inspect );

	// Replay previously generated values:
	i = 0;
	s.pipe( iStream );

	function inspect( v ) {
		t.equal( v, arr[ i ], 'returns expected value. i: ' + i + '.' );
		i += 1;
	}

	function onEnd() {
		t.end();
	}
});
