describe('Core helper tests', function() {

	var helpers;

	beforeAll(function() {
		helpers = window.Chart.helpers;
	});

	it('should merge a normal config without scales', function() {
		var baseConfig = {
			valueProp: 5,
			arrayProp: [1, 2, 3, 4, 5, 6],
			objectProp: {
				prop1: 'abc',
				prop2: 56
			}
		};

		var toMerge = {
			valueProp2: null,
			arrayProp: ['a', 'c'],
			objectProp: {
				prop1: 'c',
				prop3: 'prop3'
			}
		};

		var merged = helpers.configMerge(baseConfig, toMerge);
		expect(merged).toEqual({
			valueProp: 5,
			valueProp2: null,
			arrayProp: ['a', 'c'],
			objectProp: {
				prop1: 'c',
				prop2: 56,
				prop3: 'prop3'
			}
		});
	});

	it('should merge scale configs', function() {
		var baseConfig = {
			scales: {
				prop1: {
					abc: 123,
					def: '456'
				},
				prop2: 777,
				yAxes: [{
					type: 'linear',
				}, {
					type: 'log'
				}]
			}
		};

		var toMerge = {
			scales: {
				prop1: {
					def: 'bbb',
					ghi: 78
				},
				prop2: null,
				yAxes: [{
					type: 'linear',
					axisProp: 456
				}, {
					// pulls in linear default config since axis type changes
					type: 'linear',
					position: 'right'
				}, {
					// Pulls in linear default config since axis not in base
					type: 'linear'
				}]
			}
		};

		var merged = helpers.configMerge(baseConfig, toMerge);
		expect(merged).toEqual({
			scales: {
				prop1: {
					abc: 123,
					def: 'bbb',
					ghi: 78
				},
				prop2: null,
				yAxes: [{
					type: 'linear',
					axisProp: 456
				}, {
					display: true,

					gridLines: {
						color: 'rgba(0, 0, 0, 0.1)',
						drawBorder: true,
						drawOnChartArea: true,
						drawTicks: true, // draw ticks extending towards the label
						tickMarkLength: 10,
						lineWidth: 1,
						offsetGridLines: false,
						display: true,
						zeroLineColor: 'rgba(0,0,0,0.25)',
						zeroLineWidth: 1,
						zeroLineBorderDash: [],
						zeroLineBorderDashOffset: 0.0,
						borderDash: [],
						borderDashOffset: 0.0
					},
					position: 'right',
					offset: false,
					scaleLabel: Chart.defaults.scale.scaleLabel,
					ticks: {
						beginAtZero: false,
						minRotation: 0,
						maxRotation: 50,
						mirror: false,
						padding: 0,
						reverse: false,
						display: true,
						callback: merged.scales.yAxes[1].ticks.callback, // make it nicer, then check explicitly below
						autoSkip: true,
						autoSkipPadding: 0,
						labelOffset: 0,
						minor: {},
						major: {},
					},
					type: 'linear'
				}, {
					display: true,

					gridLines: {
						color: 'rgba(0, 0, 0, 0.1)',
						drawBorder: true,
						drawOnChartArea: true,
						drawTicks: true, // draw ticks extending towards the label,
						tickMarkLength: 10,
						lineWidth: 1,
						offsetGridLines: false,
						display: true,
						zeroLineColor: 'rgba(0,0,0,0.25)',
						zeroLineWidth: 1,
						zeroLineBorderDash: [],
						zeroLineBorderDashOffset: 0.0,
						borderDash: [],
						borderDashOffset: 0.0
					},
					position: 'left',
					offset: false,
					scaleLabel: Chart.defaults.scale.scaleLabel,
					ticks: {
						beginAtZero: false,
						minRotation: 0,
						maxRotation: 50,
						mirror: false,
						padding: 0,
						reverse: false,
						display: true,
						callback: merged.scales.yAxes[2].ticks.callback, // make it nicer, then check explicitly below
						autoSkip: true,
						autoSkipPadding: 0,
						labelOffset: 0,
						minor: {},
						major: {},
					},
					type: 'linear'
				}]
			}
		});

		// Are these actually functions
		expect(merged.scales.yAxes[1].ticks.callback).toEqual(jasmine.any(Function));
		expect(merged.scales.yAxes[2].ticks.callback).toEqual(jasmine.any(Function));
	});

	it('should filter an array', function() {
		var data = [-10, 0, 6, 0, 7];
		var callback = function(item) {
			return item > 2;
		};
		expect(helpers.where(data, callback)).toEqual([6, 7]);
		expect(helpers.findNextWhere(data, callback)).toEqual(6);
		expect(helpers.findNextWhere(data, callback, 2)).toBe(7);
		expect(helpers.findNextWhere(data, callback, 4)).toBe(undefined);
		expect(helpers.findPreviousWhere(data, callback)).toBe(7);
		expect(helpers.findPreviousWhere(data, callback, 3)).toBe(6);
		expect(helpers.findPreviousWhere(data, callback, 0)).toBe(undefined);
	});

	it('should get the correct sign', function() {
		expect(helpers.sign(0)).toBe(0);
		expect(helpers.sign(10)).toBe(1);
		expect(helpers.sign(-5)).toBe(-1);
	});

	it('should do a log10 operation', function() {
		expect(helpers.log10(0)).toBe(-Infinity);

		// Check all allowed powers of 10, which should return integer values
		var maxPowerOf10 = Math.floor(helpers.log10(Number.MAX_VALUE));
		for (var i = 0; i < maxPowerOf10; i += 1) {
			expect(helpers.log10(Math.pow(10, i))).toBe(i);
		}
	});

	it('should correctly determine if two numbers are essentially equal', function() {
		expect(helpers.almostEquals(0, Number.EPSILON, 2 * Number.EPSILON)).toBe(true);
		expect(helpers.almostEquals(1, 1.1, 0.0001)).toBe(false);
		expect(helpers.almostEquals(1e30, 1e30 + Number.EPSILON, 0)).toBe(false);
		expect(helpers.almostEquals(1e30, 1e30 + Number.EPSILON, 2 * Number.EPSILON)).toBe(true);
	});

	it('should correctly determine if a numbers are essentially whole', function() {
		expect(helpers.almostWhole(0.99999, 0.0001)).toBe(true);
		expect(helpers.almostWhole(0.9, 0.0001)).toBe(false);
	});

	it('should generate integer ids', function() {
		var uid = helpers.uid();
		expect(uid).toEqual(jasmine.any(Number));
		expect(helpers.uid()).toBe(uid + 1);
		expect(helpers.uid()).toBe(uid + 2);
		expect(helpers.uid()).toBe(uid + 3);
	});

	it('should detect a number', function() {
		expect(helpers.isNumber(123)).toBe(true);
		expect(helpers.isNumber('123')).toBe(true);
		expect(helpers.isNumber(null)).toBe(false);
		expect(helpers.isNumber(NaN)).toBe(false);
		expect(helpers.isNumber(undefined)).toBe(false);
		expect(helpers.isNumber('cbc')).toBe(false);
	});

	it('should convert between radians and degrees', function() {
		expect(helpers.toRadians(180)).toBe(Math.PI);
		expect(helpers.toRadians(90)).toBe(0.5 * Math.PI);
		expect(helpers.toDegrees(Math.PI)).toBe(180);
		expect(helpers.toDegrees(Math.PI * 3 / 2)).toBe(270);
	});

	it('should get an angle from a point', function() {
		var center = {
			x: 0,
			y: 0
		};

		expect(helpers.getAngleFromPoint(center, {
			x: 0,
			y: 10
		})).toEqual({
			angle: Math.PI / 2,
			distance: 10,
		});

		expect(helpers.getAngleFromPoint(center, {
			x: Math.sqrt(2),
			y: Math.sqrt(2)
		})).toEqual({
			angle: Math.PI / 4,
			distance: 2
		});

		expect(helpers.getAngleFromPoint(center, {
			x: -1.0 * Math.sqrt(2),
			y: -1.0 * Math.sqrt(2)
		})).toEqual({
			angle: Math.PI * 1.25,
			distance: 2
		});
	});

	it('should spline curves', function() {
		expect(helpers.splineCurve({
			x: 0,
			y: 0
		}, {
			x: 1,
			y: 1
		}, {
			x: 2,
			y: 0
		}, 0)).toEqual({
			previous: {
				x: 1,
				y: 1,
			},
			next: {
				x: 1,
				y: 1,
			}
		});

		expect(helpers.splineCurve({
			x: 0,
			y: 0
		}, {
			x: 1,
			y: 1
		}, {
			x: 2,
			y: 0
		}, 1)).toEqual({
			previous: {
				x: 0,
				y: 1,
			},
			next: {
				x: 2,
				y: 1,
			}
		});
	});

	it('should spline curves with monotone cubic interpolation', function() {
		var dataPoints = [
			{_model: {x: 0, y: 0, skip: false}},
			{_model: {x: 3, y: 6, skip: false}},
			{_model: {x: 9, y: 6, skip: false}},
			{_model: {x: 12, y: 60, skip: false}},
			{_model: {x: 15, y: 60, skip: false}},
			{_model: {x: 18, y: 120, skip: false}},
			{_model: {x: null, y: null, skip: true}},
			{_model: {x: 21, y: 180, skip: false}},
			{_model: {x: 24, y: 120, skip: false}},
			{_model: {x: 27, y: 125, skip: false}},
			{_model: {x: 30, y: 105, skip: false}},
			{_model: {x: 33, y: 110, skip: false}},
			{_model: {x: 33, y: 110, skip: false}},
			{_model: {x: 36, y: 170, skip: false}}
		];
		helpers.splineCurveMonotone(dataPoints);
		expect(dataPoints).toEqual([{
			_model: {
				x: 0,
				y: 0,
				skip: false,
				controlPointNextX: 1,
				controlPointNextY: 2
			}
		},
		{
			_model: {
				x: 3,
				y: 6,
				skip: false,
				controlPointPreviousX: 2,
				controlPointPreviousY: 6,
				controlPointNextX: 5,
				controlPointNextY: 6
			}
		},
		{
			_model: {
				x: 9,
				y: 6,
				skip: false,
				controlPointPreviousX: 7,
				controlPointPreviousY: 6,
				controlPointNextX: 10,
				controlPointNextY: 6
			}
		},
		{
			_model: {
				x: 12,
				y: 60,
				skip: false,
				controlPointPreviousX: 11,
				controlPointPreviousY: 60,
				controlPointNextX: 13,
				controlPointNextY: 60
			}
		},
		{
			_model: {
				x: 15,
				y: 60,
				skip: false,
				controlPointPreviousX: 14,
				controlPointPreviousY: 60,
				controlPointNextX: 16,
				controlPointNextY: 60
			}
		},
		{
			_model: {
				x: 18,
				y: 120,
				skip: false,
				controlPointPreviousX: 17,
				controlPointPreviousY: 100
			}
		},
		{
			_model: {
				x: null,
				y: null,
				skip: true
			}
		},
		{
			_model: {
				x: 21,
				y: 180,
				skip: false,
				controlPointNextX: 22,
				controlPointNextY: 160
			}
		},
		{
			_model: {
				x: 24,
				y: 120,
				skip: false,
				controlPointPreviousX: 23,
				controlPointPreviousY: 120,
				controlPointNextX: 25,
				controlPointNextY: 120
			}
		},
		{
			_model: {
				x: 27,
				y: 125,
				skip: false,
				controlPointPreviousX: 26,
				controlPointPreviousY: 125,
				controlPointNextX: 28,
				controlPointNextY: 125
			}
		},
		{
			_model: {
				x: 30,
				y: 105,
				skip: false,
				controlPointPreviousX: 29,
				controlPointPreviousY: 105,
				controlPointNextX: 31,
				controlPointNextY: 105
			}
		},
		{
			_model: {
				x: 33,
				y: 110,
				skip: false,
				controlPointPreviousX: 32,
				controlPointPreviousY: 110,
				controlPointNextX: 33,
				controlPointNextY: 110
			}
		},
		{
			_model: {
				x: 33,
				y: 110,
				skip: false,
				controlPointPreviousX: 33,
				controlPointPreviousY: 110,
				controlPointNextX: 34,
				controlPointNextY: 110
			}
		},
		{
			_model: {
				x: 36,
				y: 170,
				skip: false,
				controlPointPreviousX: 35,
				controlPointPreviousY: 150
			}
		}]);
	});

	it('should get the next or previous item in an array', function() {
		var testData = [0, 1, 2];

		expect(helpers.nextItem(testData, 0, false)).toEqual(1);
		expect(helpers.nextItem(testData, 2, false)).toEqual(2);
		expect(helpers.nextItem(testData, 2, true)).toEqual(0);
		expect(helpers.nextItem(testData, 1, true)).toEqual(2);
		expect(helpers.nextItem(testData, -1, false)).toEqual(0);

		expect(helpers.previousItem(testData, 0, false)).toEqual(0);
		expect(helpers.previousItem(testData, 0, true)).toEqual(2);
		expect(helpers.previousItem(testData, 2, false)).toEqual(1);
		expect(helpers.previousItem(testData, 1, true)).toEqual(0);
	});

	it('should return the width of the longest text in an Array and 2D Array', function() {
		var context = window.createMockContext();
		var font = "normal 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif";
		var arrayOfThings1D = ['FooBar', 'Bar'];
		var arrayOfThings2D = [['FooBar_1', 'Bar_2'], 'Foo_1'];


		// Regardless 'FooBar' is the longest label it should return (characters * 10)
		expect(helpers.longestText(context, font, arrayOfThings1D, {})).toEqual(60);
		expect(helpers.longestText(context, font, arrayOfThings2D, {})).toEqual(80);
		// We check to make sure we made the right calls to the canvas.
		expect(context.getCalls()).toEqual([{
			name: 'measureText',
			args: ['FooBar']
		}, {
			name: 'measureText',
			args: ['Bar']
		}, {
			name: 'measureText',
			args: ['FooBar_1']
		}, {
			name: 'measureText',
			args: ['Bar_2']
		}, {
			name: 'measureText',
			args: ['Foo_1']
		}]);
	});

	it('compare text with current longest and update', function() {
		var context = window.createMockContext();
		var data = {};
		var gc = [];
		var longest = 70;

		expect(helpers.measureText(context, data, gc, longest, 'foobar')).toEqual(70);
		expect(helpers.measureText(context, data, gc, longest, 'foobar_')).toEqual(70);
		expect(helpers.measureText(context, data, gc, longest, 'foobar_1')).toEqual(80);
		// We check to make sure we made the right calls to the canvas.
		expect(context.getCalls()).toEqual([{
			name: 'measureText',
			args: ['foobar']
		}, {
			name: 'measureText',
			args: ['foobar_']
		}, {
			name: 'measureText',
			args: ['foobar_1']
		}]);
	});

	it('count look at all the labels and return maximum number of lines', function() {
		window.createMockContext();
		var arrayOfThings1 = ['Foo', 'Bar'];
		var arrayOfThings2 = [['Foo', 'Bar'], 'Foo'];
		var arrayOfThings3 = [['Foo', 'Bar', 'Boo'], ['Foo', 'Bar'], 'Foo'];

		expect(helpers.numberOfLabelLines(arrayOfThings1)).toEqual(1);
		expect(helpers.numberOfLabelLines(arrayOfThings2)).toEqual(2);
		expect(helpers.numberOfLabelLines(arrayOfThings3)).toEqual(3);
	});

	it ('should get the maximum width and height for a node', function() {
		// Create div with fixed size as a test bed
		var div = document.createElement('div');
		div.style.width = '200px';
		div.style.height = '300px';

		document.body.appendChild(div);

		// Create the div we want to get the max size for
		var innerDiv = document.createElement('div');
		div.appendChild(innerDiv);

		expect(helpers.getMaximumWidth(innerDiv)).toBe(200);
		expect(helpers.getMaximumHeight(innerDiv)).toBe(300);

		document.body.removeChild(div);
	});

	it ('should get the maximum width of a node that has a max-width style', function() {
		// Create div with fixed size as a test bed
		var div = document.createElement('div');
		div.style.width = '200px';
		div.style.height = '300px';

		document.body.appendChild(div);

		// Create the div we want to get the max size for and set a max-width style
		var innerDiv = document.createElement('div');
		innerDiv.style.maxWidth = '150px';
		div.appendChild(innerDiv);

		expect(helpers.getMaximumWidth(innerDiv)).toBe(150);

		document.body.removeChild(div);
	});

	it ('should get the maximum height of a node that has a max-height style', function() {
		// Create div with fixed size as a test bed
		var div = document.createElement('div');
		div.style.width = '200px';
		div.style.height = '300px';

		document.body.appendChild(div);

		// Create the div we want to get the max size for and set a max-height style
		var innerDiv = document.createElement('div');
		innerDiv.style.maxHeight = '150px';
		div.appendChild(innerDiv);

		expect(helpers.getMaximumHeight(innerDiv)).toBe(150);

		document.body.removeChild(div);
	});

	it ('should get the maximum width of a node when the parent has a max-width style', function() {
		// Create div with fixed size as a test bed
		var div = document.createElement('div');
		div.style.width = '200px';
		div.style.height = '300px';

		document.body.appendChild(div);

		// Create an inner wrapper around our div we want to size and give that a max-width style
		var parentDiv = document.createElement('div');
		parentDiv.style.maxWidth = '150px';
		div.appendChild(parentDiv);

		// Create the div we want to get the max size for
		var innerDiv = document.createElement('div');
		parentDiv.appendChild(innerDiv);

		expect(helpers.getMaximumWidth(innerDiv)).toBe(150);

		document.body.removeChild(div);
	});

	it ('should get the maximum height of a node when the parent has a max-height style', function() {
		// Create div with fixed size as a test bed
		var div = document.createElement('div');
		div.style.width = '200px';
		div.style.height = '300px';

		document.body.appendChild(div);

		// Create an inner wrapper around our div we want to size and give that a max-height style
		var parentDiv = document.createElement('div');
		parentDiv.style.maxHeight = '150px';
		div.appendChild(parentDiv);

		// Create the div we want to get the max size for
		var innerDiv = document.createElement('div');
		innerDiv.style.height = '300px'; // make it large
		parentDiv.appendChild(innerDiv);

		expect(helpers.getMaximumHeight(innerDiv)).toBe(150);

		document.body.removeChild(div);
	});

	it ('should get the maximum width of a node that has a percentage max-width style', function() {
		// Create div with fixed size as a test bed
		var div = document.createElement('div');
		div.style.width = '200px';
		div.style.height = '300px';

		document.body.appendChild(div);

		// Create the div we want to get the max size for and set a max-width style
		var innerDiv = document.createElement('div');
		innerDiv.style.maxWidth = '50%';
		div.appendChild(innerDiv);

		expect(helpers.getMaximumWidth(innerDiv)).toBe(100);

		document.body.removeChild(div);
	});

	it ('should get the maximum height of a node that has a percentage max-height style', function() {
		// Create div with fixed size as a test bed
		var div = document.createElement('div');
		div.style.width = '200px';
		div.style.height = '300px';

		document.body.appendChild(div);

		// Create the div we want to get the max size for and set a max-height style
		var innerDiv = document.createElement('div');
		innerDiv.style.maxHeight = '50%';
		div.appendChild(innerDiv);

		expect(helpers.getMaximumHeight(innerDiv)).toBe(150);

		document.body.removeChild(div);
	});

	it ('should get the maximum width of a node when the parent has a percentage max-width style', function() {
		// Create div with fixed size as a test bed
		var div = document.createElement('div');
		div.style.width = '200px';
		div.style.height = '300px';

		document.body.appendChild(div);

		// Create an inner wrapper around our div we want to size and give that a max-width style
		var parentDiv = document.createElement('div');
		parentDiv.style.maxWidth = '50%';
		div.appendChild(parentDiv);

		// Create the div we want to get the max size for
		var innerDiv = document.createElement('div');
		parentDiv.appendChild(innerDiv);

		expect(helpers.getMaximumWidth(innerDiv)).toBe(100);

		document.body.removeChild(div);
	});

	it ('should get the maximum height of a node when the parent has a percentage max-height style', function() {
		// Create div with fixed size as a test bed
		var div = document.createElement('div');
		div.style.width = '200px';
		div.style.height = '300px';

		document.body.appendChild(div);

		// Create an inner wrapper around our div we want to size and give that a max-height style
		var parentDiv = document.createElement('div');
		parentDiv.style.maxHeight = '50%';
		div.appendChild(parentDiv);

		var innerDiv = document.createElement('div');
		innerDiv.style.height = '300px'; // make it large
		parentDiv.appendChild(innerDiv);

		expect(helpers.getMaximumHeight(innerDiv)).toBe(150);

		document.body.removeChild(div);
	});

	it ('should leave styled height and width on canvas if explicitly set', function() {
		var chart = window.acquireChart({}, {
			canvas: {
				height: 200,
				width: 200,
				style: 'height: 400px; width: 400px;'
			}
		});

		helpers.retinaScale(chart, true);

		var canvas = chart.canvas;

		expect(canvas.style.height).toBe('400px');
		expect(canvas.style.width).toBe('400px');
	});

	describe('Color helper', function() {
		function isColorInstance(obj) {
			return typeof obj === 'object' && obj.hasOwnProperty('values') && obj.values.hasOwnProperty('rgb');
		}

		it('should return a color when called with a color', function() {
			expect(isColorInstance(helpers.color('rgb(1, 2, 3)'))).toBe(true);
		});

		it('should return a color when called with a CanvasGradient instance', function() {
			var context = document.createElement('canvas').getContext('2d');
			var gradient = context.createLinearGradient(0, 1, 2, 3);

			expect(isColorInstance(helpers.color(gradient))).toBe(true);
		});
	});

	describe('Background hover color helper', function() {
		it('should return a CanvasPattern when called with a CanvasPattern', function(done) {
			var dots = new Image();
			dots.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAMAAAAolt3jAAAAD1BMVEUAAAD///////////////+PQt5oAAAABXRSTlMAHlFhZsfk/BEAAAAqSURBVHgBY2BgZGJmYmSAAUYWEIDzmcBcJhiXGcxlRpPFrhdmMiqgvX0AcGIBEUAo6UAAAAAASUVORK5CYII=';
			dots.onload = function() {
				var chartContext = document.createElement('canvas').getContext('2d');
				var patternCanvas = document.createElement('canvas');
				var patternContext = patternCanvas.getContext('2d');
				var pattern = patternContext.createPattern(dots, 'repeat');
				patternContext.fillStyle = pattern;

				var backgroundColor = helpers.getHoverColor(chartContext.createPattern(patternCanvas, 'repeat'));

				expect(backgroundColor instanceof CanvasPattern).toBe(true);

				done();
			};
		});

		it('should return a modified version of color when called with a color', function() {
			var originalColorRGB = 'rgb(70, 191, 189)';

			expect(helpers.getHoverColor('#46BFBD')).not.toEqual(originalColorRGB);
		});
	});
});
