describe('Chart.Controller', function() {

	function waitForResize(chart, callback) {
		var resizer = chart.chart.canvas.parentNode._chartjs.resizer;
		var content = resizer.contentWindow || resizer;
		var state = content.document.readyState || 'complete';
		var handler = function() {
			Chart.helpers.removeEvent(content, 'load', handler);
			Chart.helpers.removeEvent(content, 'resize', handler);
			setTimeout(callback, 50);
		};

		Chart.helpers.addEvent(content, state !== 'complete'? 'load' : 'resize', handler);
	}

	describe('context acquisition', function() {
		var canvasId = 'chartjs-canvas';

		beforeEach(function() {
			var canvas = document.createElement('canvas');
			canvas.setAttribute('id', canvasId);
			window.document.body.appendChild(canvas);
		});

		afterEach(function() {
			document.getElementById(canvasId).remove();
		});

		// see https://github.com/chartjs/Chart.js/issues/2807
		it('should gracefully handle invalid item', function() {
			var chart = new Chart('foobar');

			expect(chart).not.toBeValidChart();

			chart.destroy();
		});

		it('should accept a DOM element id', function() {
			var canvas = document.getElementById(canvasId);
			var chart = new Chart(canvasId);

			expect(chart).toBeValidChart();
			expect(chart.chart.canvas).toBe(canvas);
			expect(chart.chart.ctx).toBe(canvas.getContext('2d'));

			chart.destroy();
		});

		it('should accept a canvas element', function() {
			var canvas = document.getElementById(canvasId);
			var chart = new Chart(canvas);

			expect(chart).toBeValidChart();
			expect(chart.chart.canvas).toBe(canvas);
			expect(chart.chart.ctx).toBe(canvas.getContext('2d'));

			chart.destroy();
		});

		it('should accept a canvas context2D', function() {
			var canvas = document.getElementById(canvasId);
			var context = canvas.getContext('2d');
			var chart = new Chart(context);

			expect(chart).toBeValidChart();
			expect(chart.chart.canvas).toBe(canvas);
			expect(chart.chart.ctx).toBe(context);

			chart.destroy();
		});

		it('should accept an array containing canvas', function() {
			var canvas = document.getElementById(canvasId);
			var chart = new Chart([canvas]);

			expect(chart).toBeValidChart();
			expect(chart.chart.canvas).toBe(canvas);
			expect(chart.chart.ctx).toBe(canvas.getContext('2d'));

			chart.destroy();
		});
	});

	describe('config initialization', function() {
		it('should create missing config.data properties', function() {
			var chart = acquireChart({});
			var data = chart.data;

			expect(data instanceof Object).toBeTruthy();
			expect(data.labels instanceof Array).toBeTruthy();
			expect(data.labels.length).toBe(0);
			expect(data.datasets instanceof Array).toBeTruthy();
			expect(data.datasets.length).toBe(0);
		});

		it('should not alter config.data references', function() {
			var ds0 = {data: [10, 11, 12, 13]};
			var ds1 = {data: [20, 21, 22, 23]};
			var datasets = [ds0, ds1];
			var labels = [0, 1, 2, 3];
			var data = {labels: labels, datasets: datasets};

			var chart = acquireChart({
				type: 'line',
				data: data
			});

			expect(chart.data).toBe(data);
			expect(chart.data.labels).toBe(labels);
			expect(chart.data.datasets).toBe(datasets);
			expect(chart.data.datasets[0]).toBe(ds0);
			expect(chart.data.datasets[1]).toBe(ds1);
			expect(chart.data.datasets[0].data).toBe(ds0.data);
			expect(chart.data.datasets[1].data).toBe(ds1.data);
		});

		it('should initialize config with default options', function() {
			var callback = function() {};

			var defaults = Chart.defaults;
			defaults.global.responsiveAnimationDuration = 42;
			defaults.global.hover.onHover = callback;
			defaults.line.hover.mode = 'x-axis';
			defaults.line.spanGaps = true;

			var chart = acquireChart({
				type: 'line'
			});

			var options = chart.options;
			expect(options.defaultFontSize).toBe(defaults.global.defaultFontSize);
			expect(options.showLines).toBe(defaults.line.showLines);
			expect(options.spanGaps).toBe(true);
			expect(options.responsiveAnimationDuration).toBe(42);
			expect(options.hover.onHover).toBe(callback);
			expect(options.hover.mode).toBe('x-axis');
		});

		it('should override default options', function() {
			var defaults = Chart.defaults;
			defaults.global.responsiveAnimationDuration = 42;
			defaults.line.hover.mode = 'x-axis';
			defaults.line.spanGaps = true;

			var chart = acquireChart({
				type: 'line',
				options: {
					responsiveAnimationDuration: 4242,
					spanGaps: false,
					hover: {
						mode: 'dataset',
					},
					title: {
						position: 'bottom'
					}
				}
			});

			var options = chart.options;
			expect(options.responsiveAnimationDuration).toBe(4242);
			expect(options.spanGaps).toBe(false);
			expect(options.hover.mode).toBe('dataset');
			expect(options.title.position).toBe('bottom');
		});
	});

	describe('config.options.aspectRatio', function() {
		it('should use default "global" aspect ratio for render and display sizes', function() {
			var chart = acquireChart({
				options: {
					responsive: false
				}
			}, {
				canvas: {
					style: 'width: 620px'
				}
			});

			expect(chart).toBeChartOfSize({
				dw: 620, dh: 310,
				rw: 620, rh: 310,
			});
		});

		it('should use default "chart" aspect ratio for render and display sizes', function() {
			var chart = acquireChart({
				type: 'doughnut',
				options: {
					responsive: false
				}
			}, {
				canvas: {
					style: 'width: 425px'
				}
			});

			expect(chart).toBeChartOfSize({
				dw: 425, dh: 425,
				rw: 425, rh: 425,
			});
		});

		it('should use "user" aspect ratio for render and display sizes', function() {
			var chart = acquireChart({
				options: {
					responsive: false,
					aspectRatio: 3
				}
			}, {
				canvas: {
					style: 'width: 405px'
				}
			});

			expect(chart).toBeChartOfSize({
				dw: 405, dh: 135,
				rw: 405, rh: 135,
			});
		});

		it('should not apply aspect ratio when height specified', function() {
			var chart = acquireChart({
				options: {
					responsive: false,
					aspectRatio: 3
				}
			}, {
				canvas: {
					style: 'width: 400px; height: 410px'
				}
			});

			expect(chart).toBeChartOfSize({
				dw: 400, dh: 410,
				rw: 400, rh: 410,
			});
		});
	});

	describe('config.options.responsive: false', function() {
		it('should use default canvas size for render and display sizes', function() {
			var chart = acquireChart({
				options: {
					responsive: false
				}
			}, {
				canvas: {
					style: ''
				}
			});

			expect(chart).toBeChartOfSize({
				dw: 300, dh: 150,
				rw: 300, rh: 150,
			});
		});

		it('should use canvas attributes for render and display sizes', function() {
			var chart = acquireChart({
				options: {
					responsive: false
				}
			}, {
				canvas: {
					style: '',
					width: 305,
					height: 245,
				}
			});

			expect(chart).toBeChartOfSize({
				dw: 305, dh: 245,
				rw: 305, rh: 245,
			});
		});

		it('should use canvas style for render and display sizes (if no attributes)', function() {
			var chart = acquireChart({
				options: {
					responsive: false
				}
			}, {
				canvas: {
					style: 'width: 345px; height: 125px'
				}
			});

			expect(chart).toBeChartOfSize({
				dw: 345, dh: 125,
				rw: 345, rh: 125,
			});
		});

		it('should use attributes for the render size and style for the display size', function() {
			var chart = acquireChart({
				options: {
					responsive: false
				}
			}, {
				canvas: {
					style: 'width: 345px; height: 125px;',
					width: 165,
					height: 85,
				}
			});

			expect(chart).toBeChartOfSize({
				dw: 345, dh: 125,
				rw: 165, rh: 85,
			});
		});

		it('should not inject the resizer element', function() {
			var chart = acquireChart({
				options: {
					responsive: false
				}
			});

			var wrapper = chart.chart.canvas.parentNode;
			expect(wrapper.childNodes.length).toBe(1);
			expect(wrapper.firstChild.tagName).toBe('CANVAS');
		});
	});

	describe('config.options.responsive: true (maintainAspectRatio: false)', function() {
		it('should fill parent width and height', function() {
			var chart = acquireChart({
				options: {
					responsive: true,
					maintainAspectRatio: false
				}
			}, {
				canvas: {
					style: 'width: 150px; height: 245px'
				},
				wrapper: {
					style: 'width: 300px; height: 350px'
				}
			});

			expect(chart).toBeChartOfSize({
				dw: 300, dh: 350,
				rw: 300, rh: 350,
			});
		});

		it('should resize the canvas when parent width changes', function(done) {
			var chart = acquireChart({
				options: {
					responsive: true,
					maintainAspectRatio: false
				}
			}, {
				canvas: {
					style: ''
				},
				wrapper: {
					style: 'width: 300px; height: 350px; position: relative'
				}
			});

			expect(chart).toBeChartOfSize({
				dw: 300, dh: 350,
				rw: 300, rh: 350,
			});

			var wrapper = chart.chart.canvas.parentNode;
			wrapper.style.width = '455px';
			waitForResize(chart, function() {
				expect(chart).toBeChartOfSize({
					dw: 455, dh: 350,
					rw: 455, rh: 350,
				});

				wrapper.style.width = '150px';
				waitForResize(chart, function() {
					expect(chart).toBeChartOfSize({
						dw: 150, dh: 350,
						rw: 150, rh: 350,
					});

					done();
				});
			});
		});

		it('should resize the canvas when parent height changes', function(done) {
			var chart = acquireChart({
				options: {
					responsive: true,
					maintainAspectRatio: false
				}
			}, {
				canvas: {
					style: ''
				},
				wrapper: {
					style: 'width: 300px; height: 350px; position: relative'
				}
			});

			expect(chart).toBeChartOfSize({
				dw: 300, dh: 350,
				rw: 300, rh: 350,
			});

			var wrapper = chart.chart.canvas.parentNode;
			wrapper.style.height = '455px';
			waitForResize(chart, function() {
				expect(chart).toBeChartOfSize({
					dw: 300, dh: 455,
					rw: 300, rh: 455,
				});

				wrapper.style.height = '150px';
				waitForResize(chart, function() {
					expect(chart).toBeChartOfSize({
						dw: 300, dh: 150,
						rw: 300, rh: 150,
					});

					done();
				});
			});
		});

		it('should not include parent padding when resizing the canvas', function(done) {
			var chart = acquireChart({
				type: 'line',
				options: {
					responsive: true,
					maintainAspectRatio: false
				}
			}, {
				canvas: {
					style: ''
				},
				wrapper: {
					style: 'padding: 50px; width: 320px; height: 350px; position: relative'
				}
			});

			expect(chart).toBeChartOfSize({
				dw: 320, dh: 350,
				rw: 320, rh: 350,
			});

			var wrapper = chart.chart.canvas.parentNode;
			wrapper.style.height = '355px';
			wrapper.style.width = '455px';
			waitForResize(chart, function() {
				expect(chart).toBeChartOfSize({
					dw: 455, dh: 355,
					rw: 455, rh: 355,
				});

				done();
			});
		});

		it('should resize the canvas when the canvas display style changes from "none" to "block"', function(done) {
			var chart = acquireChart({
				options: {
					responsive: true,
					maintainAspectRatio: false
				}
			}, {
				canvas: {
					style: 'display: none;'
				},
				wrapper: {
					style: 'width: 320px; height: 350px'
				}
			});

			var canvas = chart.chart.canvas;
			canvas.style.display = 'block';
			waitForResize(chart, function() {
				expect(chart).toBeChartOfSize({
					dw: 320, dh: 350,
					rw: 320, rh: 350,
				});

				done();
			});
		});

		it('should resize the canvas when the wrapper display style changes from "none" to "block"', function(done) {
			var chart = acquireChart({
				options: {
					responsive: true,
					maintainAspectRatio: false
				}
			}, {
				canvas: {
					style: ''
				},
				wrapper: {
					style: 'display: none; width: 460px; height: 380px'
				}
			});

			var wrapper = chart.chart.canvas.parentNode;
			wrapper.style.display = 'block';
			waitForResize(chart, function() {
				expect(chart).toBeChartOfSize({
					dw: 460, dh: 380,
					rw: 460, rh: 380,
				});

				done();
			});
		});

		// https://github.com/chartjs/Chart.js/issues/3521
		it('should resize the canvas after the wrapper has been re-attached to the DOM', function(done) {
			var chart = acquireChart({
				options: {
					responsive: true,
					maintainAspectRatio: false
				}
			}, {
				canvas: {
					style: ''
				},
				wrapper: {
					style: 'width: 320px; height: 350px'
				}
			});

			expect(chart).toBeChartOfSize({
				dw: 320, dh: 350,
				rw: 320, rh: 350,
			});

			var wrapper = chart.chart.canvas.parentNode;
			var parent = wrapper.parentNode;
			parent.removeChild(wrapper);
			parent.appendChild(wrapper);
			wrapper.style.height = '355px';

			waitForResize(chart, function() {
				expect(chart).toBeChartOfSize({
					dw: 320, dh: 355,
					rw: 320, rh: 355,
				});

				parent.removeChild(wrapper);
				wrapper.style.width = '455px';
				parent.appendChild(wrapper);

				waitForResize(chart, function() {
					expect(chart).toBeChartOfSize({
						dw: 455, dh: 355,
						rw: 455, rh: 355,
					});

					done();
				});
			});
		});
	});

	describe('config.options.responsive: true (maintainAspectRatio: true)', function() {
		it('should fill parent width and use aspect ratio to calculate height', function() {
			var chart = acquireChart({
				options: {
					responsive: true,
					maintainAspectRatio: true
				}
			}, {
				canvas: {
					style: 'width: 150px; height: 245px'
				},
				wrapper: {
					style: 'width: 300px; height: 350px'
				}
			});

			expect(chart).toBeChartOfSize({
				dw: 300, dh: 490,
				rw: 300, rh: 490,
			});
		});

		it('should resize the canvas with correct aspect ratio when parent width changes', function(done) {
			var chart = acquireChart({
				type: 'line', // AR == 2
				options: {
					responsive: true,
					maintainAspectRatio: true
				}
			}, {
				canvas: {
					style: ''
				},
				wrapper: {
					style: 'width: 300px; height: 350px; position: relative'
				}
			});

			expect(chart).toBeChartOfSize({
				dw: 300, dh: 150,
				rw: 300, rh: 150,
			});

			var wrapper = chart.chart.canvas.parentNode;
			wrapper.style.width = '450px';
			waitForResize(chart, function() {
				expect(chart).toBeChartOfSize({
					dw: 450, dh: 225,
					rw: 450, rh: 225,
				});

				wrapper.style.width = '150px';
				waitForResize(chart, function() {
					expect(chart).toBeChartOfSize({
						dw: 150, dh: 75,
						rw: 150, rh: 75,
					});

					done();
				});
			});
		});

		it('should not resize the canvas when parent height changes', function(done) {
			var chart = acquireChart({
				options: {
					responsive: true,
					maintainAspectRatio: true
				}
			}, {
				canvas: {
					style: ''
				},
				wrapper: {
					style: 'width: 320px; height: 350px; position: relative'
				}
			});

			expect(chart).toBeChartOfSize({
				dw: 320, dh: 160,
				rw: 320, rh: 160,
			});

			var wrapper = chart.chart.canvas.parentNode;
			wrapper.style.height = '455px';
			waitForResize(chart, function() {
				expect(chart).toBeChartOfSize({
					dw: 320, dh: 160,
					rw: 320, rh: 160,
				});

				wrapper.style.height = '150px';
				waitForResize(chart, function() {
					expect(chart).toBeChartOfSize({
						dw: 320, dh: 160,
						rw: 320, rh: 160,
					});

					done();
				});
			});
		});
	});

	describe('Retina scale (a.k.a. device pixel ratio)', function() {
		beforeEach(function() {
			this.devicePixelRatio = window.devicePixelRatio;
			window.devicePixelRatio = 3;
		});

		afterEach(function() {
			window.devicePixelRatio = this.devicePixelRatio;
		});

		// see https://github.com/chartjs/Chart.js/issues/3575
		it ('should scale the render size but not the "implicit" display size', function() {
			var chart = acquireChart({
				options: {
					responsive: false
				}
			}, {
				canvas: {
					width: 320,
					height: 240,
				}
			});

			expect(chart).toBeChartOfSize({
				dw: 320, dh: 240,
				rw: 960, rh: 720,
			});
		});

		it ('should scale the render size but not the "explicit" display size', function() {
			var chart = acquireChart({
				options: {
					responsive: false
				}
			}, {
				canvas: {
					style: 'width: 320px; height: 240px'
				}
			});

			expect(chart).toBeChartOfSize({
				dw: 320, dh: 240,
				rw: 960, rh: 720,
			});
		});
	});

	describe('controller.destroy', function() {
		it('should reset context to default values', function() {
			var chart = acquireChart({});
			var context = chart.chart.ctx;

			chart.destroy();

			// https://www.w3.org/TR/2dcontext/#conformance-requirements
			Chart.helpers.each({
				fillStyle: '#000000',
				font: '10px sans-serif',
				lineJoin: 'miter',
				lineCap: 'butt',
				lineWidth: 1,
				miterLimit: 10,
				shadowBlur: 0,
				shadowColor: 'rgba(0, 0, 0, 0)',
				shadowOffsetX: 0,
				shadowOffsetY: 0,
				strokeStyle: '#000000',
				textAlign: 'start',
				textBaseline: 'alphabetic'
			}, function(value, key) {
				expect(context[key]).toBe(value);
			});
		});

		it('should restore canvas initial values', function(done) {
			var chart = acquireChart({
				options: {
					responsive: true,
					maintainAspectRatio: false
				}
			}, {
				canvas: {
					width: 180,
					style: 'width: 512px; height: 480px'
				},
				wrapper: {
					style: 'width: 450px; height: 450px; position: relative'
				}
			});

			var canvas = chart.chart.canvas;
			var wrapper = canvas.parentNode;
			wrapper.style.width = '475px';
			waitForResize(chart, function() {
				expect(chart).toBeChartOfSize({
					dw: 475, dh: 450,
					rw: 475, rh: 450,
				});

				chart.destroy();

				expect(canvas.getAttribute('width')).toBe('180');
				expect(canvas.getAttribute('height')).toBe(null);
				expect(canvas.style.width).toBe('512px');
				expect(canvas.style.height).toBe('480px');
				expect(canvas.style.display).toBe('');

				done();
			});
		});

		it('should remove the resizer element when responsive: true', function() {
			var chart = acquireChart({
				options: {
					responsive: true
				}
			});

			var wrapper = chart.chart.canvas.parentNode;
			var resizer = wrapper.firstChild;

			expect(wrapper.childNodes.length).toBe(2);
			expect(resizer.tagName).toBe('IFRAME');

			chart.destroy();

			expect(wrapper.childNodes.length).toBe(1);
			expect(wrapper.firstChild.tagName).toBe('CANVAS');
		});
	});

	describe('controller.reset', function() {
		it('should reset the chart elements', function() {
			var chart = acquireChart({
				type: 'line',
				data: {
					labels: ['A', 'B', 'C', 'D'],
					datasets: [{
						data: [10, 20, 30, 0]
					}]
				},
				options: {
					responsive: true
				}
			});

			var meta = chart.getDatasetMeta(0);

			// Verify that points are at their initial correct location,
			// then we will reset and see that they moved
			expect(meta.data[0]._model.y).toBe(333);
			expect(meta.data[1]._model.y).toBe(183);
			expect(meta.data[2]._model.y).toBe(32);
			expect(meta.data[3]._model.y).toBe(484);

			chart.reset();

			// For a line chart, the animation state is the bottom
			expect(meta.data[0]._model.y).toBe(484);
			expect(meta.data[1]._model.y).toBe(484);
			expect(meta.data[2]._model.y).toBe(484);
			expect(meta.data[3]._model.y).toBe(484);
		});
	});
});
