/**
* @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 assert = require( 'chai' ).assert;
var PINF = require( '@stdlib/constants/float64/pinf' );
var hasSetSupport = require( '@stdlib/assert/has-set-support' );
var hasMapSupport = require( '@stdlib/assert/has-map-support' );
var defineProperty = require( './../../define-property' );
var copy = require( './../lib/deep_copy.js' );
var fixtures = require( './fixtures' );


// VARIABLES //

var LEVEL = PINF;


// TESTS //

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

tape( 'copy primitives', function test( t ) {
	var values;
	var actual;
	var val;
	var cp;
	var i;

	values = [
		'beep',
		5,
		null,
		void 0,
		true
	];

	for ( i = 0; i < values.length; i++ ) {
		val = values[ i ];
		cp = {};
		actual = copy( val, cp, [val], [cp], LEVEL );
		t.equal( actual, val, 'copies a ' + (typeof val) + ' primitive' );
	}
	t.end();
});

tape( 'copy `NaN`', function test( t ) {
	var actual;
	var val;
	var cp;

	val = NaN;
	cp = {};
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, actual, 'copies a NaN primitive' );
	t.end();
});

tape( 'copy typed arrays', function test( t ) {
	var actual;
	var val;
	var cp;

	val = fixtures.int8arr;
	cp = {};
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, val );
	t.ok( true, 'deep equal' );

	val = fixtures.uint8arr;
	cp = {};
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, val );
	t.ok( true, 'deep equal' );

	val = fixtures.uint8carr;
	cp = {};
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, val );
	t.ok( true, 'deep equal' );

	val = fixtures.int16arr;
	cp = {};
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, val );
	t.ok( true, 'deep equal' );

	val = fixtures.uint16arr;
	cp = {};
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, val );
	t.ok( true, 'deep equal' );

	val = fixtures.int32arr;
	cp = {};
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, val );
	t.ok( true, 'deep equal' );

	val = fixtures.uint32arr;
	cp = {};
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, val );
	t.ok( true, 'deep equal' );

	val = fixtures.float32arr;
	cp = {};
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, val );
	t.ok( true, 'deep equal' );

	val = fixtures.float64arr;
	cp = {};
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, val );
	t.ok( true, 'deep equal' );

	t.end();
});

tape( 'copy `Boolean` objects to boolean primitives', function test( t ) {
	var actual;
	var val;
	var cp;

	val = fixtures.bool;
	cp = {};
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( typeof actual, 'object', 'not an object' );
	t.equal( actual, true, 'equal values' );

	t.end();
});

tape( 'copy `String` objects to string primitives', function test( t ) {
	var actual;
	var val;
	var cp;

	val = fixtures.str;
	cp = {};
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( typeof actual, 'object', 'not an object' );
	t.equal( actual, 'beep', 'equal values' );

	t.end();
});

tape( 'copy `Number` objects to number primitives', function test( t ) {
	var actual;
	var val;
	var cp;

	val = fixtures.num;
	cp = {};
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( typeof actual, 'object', 'not an object' );
	t.equal( actual, 3.14, 'equal values' );

	t.end();
});

tape( 'copy `Date` objects', function test( t ) {
	var actual;
	var val;
	var cp;

	val = fixtures.date;
	cp = {};
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, val, 'distinct references' );
	t.equal( +actual, +val, 'equal values' );

	t.end();
});

tape( 'copy `RegExp` objects', function test( t ) {
	var actual;
	var val;
	var cp;

	val = fixtures.re;
	cp = {};
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, val, 'distinct references' );
	t.equal( actual.toString(), val.toString(), 'equal values' );

	t.end();
});

tape( 'copy `Error` objects', function test( t ) {
	var actual;
	var val;
	var cp;

	val = fixtures.err;
	cp = {};
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, val );
	t.ok( true, 'deep equal' );

	t.end();
});

tape( 'copy special `Error` objects (e.g., `TypeError`, etc)', function test( t ) {
	var actual;
	var val;
	var cp;

	val = fixtures.terr;
	cp = {};
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, val );
	t.ok( true, 'deep equal' );

	t.end();
});

tape( 'copy Node.js buffers', function test( t ) {
	var actual;
	var val;
	var cp;

	val = fixtures.buffer;
	cp = {};
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, val );
	t.ok( true, 'deep equal' );

	t.end();
});

tape( 'clone (simple) class instances', function test( t ) {
	var actual;
	var val;
	var cp;

	val = fixtures.foo;
	cp = {};
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, val );
	t.ok( true, 'deep equal' );

	t.end();
});

tape( 'if an environment does not support at least ES5, the function will return an empty object for class instances', function test( t ) {
	var actual;
	var freeze;
	var val;
	var cp;

	val = fixtures.bar;
	cp = {};

	freeze = Object.freeze;
	Object.freeze = void 0;

	actual = copy( val, cp, [val], [cp], LEVEL );
	assert.deepEqual( actual, {} );
	t.ok( true, 'deep equal' );

	Object.freeze = freeze;
	t.end();
});

tape( 'copy empty arrays', function test( t ) {
	var actual;
	var val;
	var cp;

	val = fixtures.emptyArr;
	cp = [];
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, fixtures.expectedEmptyArray );
	t.ok( true, 'deep equal' );

	t.end();
});

tape( 'copy sparse arrays', function test( t ) {
	var actual;
	var val;
	var cp;

	val = fixtures.sparseArr;
	cp = [];
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, fixtures.expectedSparseArray );
	t.ok( true, 'deep equal' );

	t.end();
});

tape( 'copy complex arrays', function test( t ) {
	var actual;
	var val;
	var cp;

	val = fixtures.arr;
	cp = [];
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, fixtures.expectedArray );
	t.ok( true, 'deep equal' );

	t.end();
});

tape( 'copy `Set` objects', function test( t ) {
	var actual;
	var val;
	var cp;
	if ( !hasSetSupport() ) {
		t.ok( true, 'sets are not supported' );
		t.end();
		return;
	}
	val = fixtures.set;
	cp = {};

	actual = copy( val, cp, [val], [cp], LEVEL );
	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, val );
	t.ok( true, 'deep equal' );

	val = {
		'set': val
	};
	cp = {};

	actual = copy( val, cp, [val], [cp], LEVEL );
	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, val );
	t.ok( true, 'deep equal' );

	t.end();
});

tape( 'copy `Map` objects', function test( t ) {
	var actual;
	var val;
	var cp;
	if ( !hasMapSupport() ) {
		t.ok( true, 'maps are not supported' );
		t.end();
		return;
	}
	val = fixtures.map;
	cp = {};

	actual = copy( val, cp, [val], [cp], LEVEL );
	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, val );
	t.ok( true, 'deep equal' );

	val = [ val ];
	cp = [];

	actual = copy( val, cp, [val], [cp], LEVEL );
	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, val );
	t.ok( true, 'deep equal' );

	t.end();
});

tape( 'object which cannot be extended', function test( t ) {
	var actual;
	var val;
	var cp;

	val = fixtures.cantExtend;
	cp = {};
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, val );
	t.ok( true, 'deep equal' );

	t.ok( !Object.isExtensible( actual ), 'cannot be extended' );

	t.end();
});

tape( 'object which is sealed', function test( t ) {
	var actual;
	var val;
	var cp;

	val = fixtures.sealed;
	cp = [];
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, val );
	t.ok( true, 'deep equal' );

	t.ok( Object.isSealed( actual ), 'is sealed' );

	t.end();
});

tape( 'object which is frozen', function test( t ) {
	var actual;
	var val;
	var cp;

	val = fixtures.frozen;
	cp = {};
	actual = copy( val, cp, [val], [cp], LEVEL );

	t.notEqual( actual, val, 'distinct references' );
	assert.deepEqual( actual, val );
	t.ok( true, 'deep equal' );

	t.ok( Object.isFrozen( actual ), 'is frozen' );

	t.end();
});

tape( 'circular references', function test( t ) {
	var actual;
	var val;
	var cp;

	val = {};
	cp = {};

	val.to = val;
	actual = copy( val, cp, [val], [cp], LEVEL );

	assert.deepEqual( actual, val );
	t.ok( true, 'deep equal' );
	t.equal( actual, actual.to, 'circular reference' );

	t.end();
});

tape( 'arbitrary depth', function test( t ) {
	var actual;
	var val;
	var cp;

	val = fixtures.obj;
	cp = {};

	actual = copy( val, cp, [val], [cp], 1 );
	assert.deepEqual( actual, fixtures.expectedObj1 );
	t.ok( true, 'level 1' );

	t.equal( actual.bool, fixtures.expectedObj1.bool, 'boolean primitive' );

	t.equal( actual.uint8carr, fixtures.expectedObj1.uint8carr, 'share typed array ref' );

	t.equal( actual.arr, fixtures.expectedObj1.arr, 'share array ref' );

	t.equal( actual.obj, fixtures.expectedObj1.obj, 'share object ref' );

	t.end();
});

tape( 'arbitrary depth', function test( t ) {
	var actual;
	var val;
	var cp;

	val = fixtures.obj;
	cp = {};

	actual = copy( val, cp, [val], [cp], 2 );

	assert.deepEqual( actual, fixtures.expectedObj2 );
	t.ok( true, 'deep equal' );

	t.equal( actual.bool, fixtures.expectedObj2.bool, 'boolean primitive' );

	t.notEqual( actual.uint8carr, fixtures.expectedObj2.uint8carr, 'distinct typed arrays' );

	t.notEqual( actual.arr, fixtures.expectedObj2.arr, 'distinct arrays' );

	t.notEqual( actual.obj, fixtures.expectedObj2.obj, 'distinct objects' );

	t.equal( actual.obj.a, fixtures.expectedObj2.obj.a, 'shared nested objects' );

	t.end();
});

tape( 'property descriptors (primitives)', function test( t ) {
	var actual;
	var desc;
	var obj;
	var cp;

	obj = {};
	desc = {
		'value': 'b',
		'writable': false,
		'enumerable': true,
		'configurable': false
	};

	defineProperty( obj, 'a', desc );

	cp = {};
	actual = copy( obj, cp, [obj], [cp], LEVEL );

	assert.deepEqual( actual, obj );
	t.ok( true, 'deep equal' );

	assert.deepEqual( Object.getOwnPropertyDescriptor( actual, 'a' ), desc );
	t.ok( true, 'property descriptor' );

	t.end();
});

tape( 'data property descriptors', function test( t ) {
	var actual;
	var desc;
	var obj;
	var cp;

	obj = {};
	desc = {
		'value': 'b',
		'writable': false,
		'enumerable': true,
		'configurable': false
	};

	defineProperty( obj, 'a', desc );

	cp = {};
	actual = copy( obj, cp, [obj], [cp], LEVEL );

	assert.deepEqual( actual, obj );
	t.ok( true, 'deep equal' );

	assert.deepEqual( Object.getOwnPropertyDescriptor( actual, 'a' ), desc );
	t.ok( true, 'property descriptor' );

	t.end();
});

tape( 'accessor property descriptors', function test( t ) {
	var actual;
	var desc;
	var obj;
	var cp;

	obj = {};
	desc = {
		'enumerable': true,
		'configurable': false,
		'get': function get() { return 5; },
		'set': function set() {}
	};

	defineProperty( obj, 'a', desc );

	cp = {};
	actual = copy( obj, cp, [obj], [cp], LEVEL );

	assert.deepEqual( actual, obj );
	t.ok( true, 'deep equal' );

	assert.deepEqual( Object.getOwnPropertyDescriptor( actual, 'a' ), desc );
	t.ok( true, 'property descriptor' );

	t.end();
});

tape( 'data property descriptors (nested)', function test( t ) {
	var actual;
	var desc1;
	var desc2;
	var obj1;
	var obj2;
	var cp;

	obj2 = {};
	desc2 = {
		'configurable': true,
		'enumerable': true,
		'get': function get() { return 5; },
		'set': function set() {}
	};

	defineProperty( obj2, 'b', desc2 );

	obj1 = {};
	desc1 = {
		'value': obj2,
		'writable': false,
		'enumerable': true,
		'configurable': false
	};

	defineProperty( obj1, 'a', desc1 );

	cp = {};
	actual = copy( obj1, cp, [obj1], [cp], LEVEL );

	assert.deepEqual( actual, obj1 );
	t.ok( true, 'deep equal' );

	assert.deepEqual( Object.getOwnPropertyDescriptor( actual, 'a' ), desc1 );
	t.ok( true, 'property descriptor `a`' );

	assert.deepEqual( Object.getOwnPropertyDescriptor( actual.a, 'b' ), desc2 );
	t.ok( true, 'property descriptor `b`' );

	t.end();
});

tape( 'accessor property descriptors (deep)', function test( t ) {
	var actual;
	var desc1;
	var desc2;
	var obj1;
	var obj2;
	var cp;

	obj2 = {};
	desc2 = {
		'value': obj2,
		'writable': false,
		'enumerable': true,
		'configurable': false
	};
	defineProperty( obj2, 'b', desc2 );

	obj1 = {};
	desc1 = {
		'configurable': true,
		'enumerable': true,
		'get': function get() { return obj2; },
		'set': function set() {}
	};
	defineProperty( obj1, 'a', desc1 );

	cp = {};
	actual = copy( obj1, cp, [obj1], [cp], LEVEL );

	assert.deepEqual( actual, obj1 );
	t.ok( true, 'deep equal' );

	assert.deepEqual( Object.getOwnPropertyDescriptor( actual, 'a' ), desc1 );
	t.ok( true, 'property descriptor `a`' );

	assert.deepEqual( Object.getOwnPropertyDescriptor( actual.a, 'b' ), desc2 );
	t.ok( true, 'property descriptor `b`' );

	t.end();
});
