'use strict';

describe('register', () => {
	const register = require('../index').register;
	const Counter = require('../index').Counter;
	const Gauge = require('../index').Gauge;
	const Histogram = require('../index').Histogram;
	const Summary = require('../index').Summary;

	beforeEach(() => {
		register.clear();
	});

	describe('should output a counter metric', () => {
		let output;
		beforeEach(async () => {
			register.registerMetric(getMetric());
			output = (await register.metrics()).split('\n');
		});

		it('with help as first item', () => {
			expect(output[0]).toEqual('# HELP test_metric A test metric');
		});
		it('with type as second item', () => {
			expect(output[1]).toEqual('# TYPE test_metric counter');
		});
		it('with first value of the metric as third item', () => {
			expect(output[2]).toEqual('test_metric{label="hello",code="303"} 12');
		});
		it('with second value of the metric as fourth item', () => {
			expect(output[3]).toEqual('test_metric{label="bye",code="404"} 34');
		});
	});

	it('should throw on more than one metric', () => {
		register.registerMetric(getMetric());

		expect(() => {
			register.registerMetric(getMetric());
		}).toThrowError(
			'A metric with the name test_metric has already been registered.',
		);
	});

	it('should handle and output a metric with a NaN value', async () => {
		register.registerMetric({
			async get() {
				return {
					name: 'test_metric',
					type: 'gauge',
					help: 'A test metric',
					values: [
						{
							value: NaN,
						},
					],
				};
			},
		});
		const lines = (await register.metrics()).split('\n');
		expect(lines).toHaveLength(4);
		expect(lines[2]).toEqual('test_metric Nan');
	});

	it('should handle and output a metric with an +Infinity value', async () => {
		register.registerMetric({
			async get() {
				return {
					name: 'test_metric',
					type: 'gauge',
					help: 'A test metric',
					values: [
						{
							value: Infinity,
						},
					],
				};
			},
		});
		const lines = (await register.metrics()).split('\n');
		expect(lines).toHaveLength(4);
		expect(lines[2]).toEqual('test_metric +Inf');
	});

	it('should handle and output a metric with an -Infinity value', async () => {
		register.registerMetric({
			async get() {
				return {
					name: 'test_metric',
					type: 'gauge',
					help: 'A test metric',
					values: [
						{
							value: -Infinity,
						},
					],
				};
			},
		});
		const lines = (await register.metrics()).split('\n');
		expect(lines).toHaveLength(4);
		expect(lines[2]).toEqual('test_metric -Inf');
	});

	it('should handle a metric without labels', async () => {
		register.registerMetric({
			async get() {
				return {
					name: 'test_metric',
					type: 'counter',
					help: 'A test metric',
					values: [
						{
							value: 1,
						},
					],
				};
			},
		});
		expect((await register.metrics()).split('\n')).toHaveLength(4);
	});

	it('should handle a metric with default labels', async () => {
		register.setDefaultLabels({ testLabel: 'testValue' });
		register.registerMetric({
			async get() {
				return {
					name: 'test_metric',
					type: 'counter',
					help: 'A test metric',
					values: [{ value: 1 }],
				};
			},
		});

		const output = (await register.metrics()).split('\n')[2];
		expect(output).toEqual('test_metric{testLabel="testValue"} 1');
	});

	it('labeled metrics should take precidence over defaulted', async () => {
		register.setDefaultLabels({ testLabel: 'testValue' });
		register.registerMetric({
			async get() {
				return {
					name: 'test_metric',
					type: 'counter',
					help: 'A test metric',
					values: [
						{
							value: 1,
							labels: {
								testLabel: 'overlapped',
								anotherLabel: 'value123',
							},
						},
					],
				};
			},
		});

		expect((await register.metrics()).split('\n')[2]).toEqual(
			'test_metric{testLabel="overlapped",anotherLabel="value123"} 1',
		);
	});

	it('should output all initialized metrics at value 0', async () => {
		new Counter({ name: 'counter', help: 'help' });
		new Gauge({ name: 'gauge', help: 'help' });
		new Histogram({ name: 'histogram', help: 'help' });
		new Summary({ name: 'summary', help: 'help' });

		expect(await register.metrics()).toMatchSnapshot();
	});

	it('should not output all initialized metrics at value 0 if labels', async () => {
		new Counter({ name: 'counter', help: 'help', labelNames: ['label'] });
		new Gauge({ name: 'gauge', help: 'help', labelNames: ['label'] });
		new Histogram({ name: 'histogram', help: 'help', labelNames: ['label'] });
		new Summary({ name: 'summary', help: 'help', labelNames: ['label'] });

		expect(await register.metrics()).toMatchSnapshot();
	});

	describe('should escape', () => {
		let escapedResult;
		beforeEach(async () => {
			register.registerMetric({
				async get() {
					return {
						name: 'test_"_\\_\n_metric',
						help: 'help_help',
						type: 'counter',
					};
				},
			});
			escapedResult = await register.metrics();
		});
		it('backslash to \\\\', () => {
			expect(escapedResult).toMatch(/\\\\/);
		});
		it('newline to \\\\n', () => {
			expect(escapedResult).toMatch(/\n/);
		});
	});

	it('should escape " in label values', async () => {
		register.registerMetric({
			async get() {
				return {
					name: 'test_metric',
					type: 'counter',
					help: 'A test metric',
					values: [
						{
							value: 12,
							labels: {
								label: 'hello',
								code: '3"03',
							},
						},
					],
				};
			},
		});
		const escapedResult = await register.metrics();
		expect(escapedResult).toMatch(/\\"/);
	});

	describe('should output metrics as JSON', () => {
		it('should output metrics as JSON', async () => {
			register.registerMetric(getMetric());
			const output = await register.getMetricsAsJSON();

			expect(output.length).toEqual(1);
			expect(output[0].name).toEqual('test_metric');
			expect(output[0].type).toEqual('counter');
			expect(output[0].help).toEqual('A test metric');
			expect(output[0].values.length).toEqual(2);
		});

		it('should add default labels to JSON', async () => {
			register.registerMetric(getMetric());
			register.setDefaultLabels({
				defaultRegistryLabel: 'testValue',
			});
			const output = await register.getMetricsAsJSON();

			expect(output.length).toEqual(1);
			expect(output[0].name).toEqual('test_metric');
			expect(output[0].type).toEqual('counter');
			expect(output[0].help).toEqual('A test metric');
			expect(output[0].values.length).toEqual(2);
			expect(output[0].values[0].labels).toEqual({
				code: '303',
				label: 'hello',
				defaultRegistryLabel: 'testValue',
			});
		});
	});

	it('should allow removing single metrics', async () => {
		register.registerMetric(getMetric());
		register.registerMetric(getMetric('some other name'));

		let output = await register.getMetricsAsJSON();
		expect(output.length).toEqual(2);

		register.removeSingleMetric('test_metric');

		output = await register.getMetricsAsJSON();

		expect(output.length).toEqual(1);
		expect(output[0].name).toEqual('some other name');
	});

	it('should allow getting single metrics', () => {
		const metric = getMetric();
		register.registerMetric(metric);

		const output = register.getSingleMetric('test_metric');
		expect(output).toEqual(metric);
	});

	it('should allow gettings metrics', async () => {
		const metric = getMetric();
		register.registerMetric(metric);
		const metrics = await register.metrics();

		expect(metrics.split('\n')[3]).toEqual(
			'test_metric{label="bye",code="404"} 34',
		);
	});

	describe('resetting', () => {
		it('should allow resetting all metrics', async () => {
			const counter = new Counter({
				name: 'test_counter',
				help: 'test metric',
				labelNames: ['serial', 'active'],
			});
			const gauge = new Gauge({
				name: 'test_gauge',
				help: 'Another test metric',
				labelNames: ['level'],
			});
			const histo = new Histogram({
				name: 'test_histo',
				help: 'test',
			});
			const summ = new Summary({
				name: 'test_summ',
				help: 'test',
				percentiles: [0.5],
			});
			register.registerMetric(counter);
			register.registerMetric(gauge);
			register.registerMetric(histo);
			register.registerMetric(summ);

			counter.inc({ serial: '12345', active: 'yes' }, 12);
			gauge.set({ level: 'low' }, -12);
			histo.observe(1);
			summ.observe(100);

			register.resetMetrics();

			const same_counter = register.getSingleMetric('test_counter');
			expect((await same_counter.get()).values).toEqual([]);

			const same_gauge = register.getSingleMetric('test_gauge');
			expect((await same_gauge.get()).values).toEqual([]);

			const same_histo = register.getSingleMetric('test_histo');
			expect((await same_histo.get()).values).toEqual([]);

			const same_summ = register.getSingleMetric('test_summ');
			expect((await same_summ.get()).values[0].value).toEqual(0);
		});
	});

	describe('Registry with default labels', () => {
		const Registry = require('../lib/registry');

		describe('mutation tests', () => {
			describe('registry.metrics()', () => {
				it('should not throw with default labels (counter)', async () => {
					const r = new Registry();
					r.setDefaultLabels({
						env: 'development',
					});

					const counter = new Counter({
						name: 'my_counter',
						help: 'my counter',
						registers: [r],
						labelNames: ['type'],
					});

					const myCounter = counter.labels('myType');

					myCounter.inc();

					const metrics = await r.metrics();
					const lines = metrics.split('\n');
					expect(lines).toContain(
						'my_counter{type="myType",env="development"} 1',
					);

					myCounter.inc();

					const metrics2 = await r.metrics();
					const lines2 = metrics2.split('\n');
					expect(lines2).toContain(
						'my_counter{type="myType",env="development"} 2',
					);
				});

				it('should not throw with default labels (gauge)', async () => {
					const r = new Registry();
					r.setDefaultLabels({
						env: 'development',
					});

					const gauge = new Gauge({
						name: 'my_gauge',
						help: 'my gauge',
						registers: [r],
						labelNames: ['type'],
					});

					const myGauge = gauge.labels('myType');

					myGauge.inc(1);

					const metrics = await r.metrics();
					const lines = metrics.split('\n');
					expect(lines).toContain(
						'my_gauge{type="myType",env="development"} 1',
					);

					myGauge.inc(2);

					const metrics2 = await r.metrics();
					const lines2 = metrics2.split('\n');
					expect(lines2).toContain(
						'my_gauge{type="myType",env="development"} 3',
					);
				});

				it('should not throw with default labels (histogram)', async () => {
					const r = new Registry();
					r.setDefaultLabels({
						env: 'development',
					});

					const hist = new Histogram({
						name: 'my_histogram',
						help: 'my histogram',
						registers: [r],
						labelNames: ['type'],
					});

					const myHist = hist.labels('myType');

					myHist.observe(1);

					const metrics = await r.metrics();
					const lines = metrics.split('\n');
					expect(lines).toContain(
						'my_histogram_bucket{le="1",type="myType",env="development"} 1',
					);

					myHist.observe(1);

					const metrics2 = await r.metrics();
					const lines2 = metrics2.split('\n');
					expect(lines2).toContain(
						'my_histogram_bucket{le="1",type="myType",env="development"} 2',
					);
				});
			});

			describe('registry.getMetricsAsJSON()', () => {
				it('should not throw with default labels (counter)', async () => {
					const r = new Registry();
					r.setDefaultLabels({
						env: 'development',
					});

					const counter = new Counter({
						name: 'my_counter',
						help: 'my counter',
						registers: [r],
						labelNames: ['type'],
					});

					const myCounter = counter.labels('myType');

					myCounter.inc();

					const metrics = await r.getMetricsAsJSON();
					expect(metrics).toContainEqual({
						aggregator: 'sum',
						help: 'my counter',
						name: 'my_counter',
						type: 'counter',
						values: [
							{
								labels: { env: 'development', type: 'myType' },
								value: 1,
							},
						],
					});

					myCounter.inc();

					const metrics2 = await r.getMetricsAsJSON();
					expect(metrics2).toContainEqual({
						aggregator: 'sum',
						help: 'my counter',
						name: 'my_counter',
						type: 'counter',
						values: [
							{
								labels: { env: 'development', type: 'myType' },
								value: 2,
							},
						],
					});
				});

				it('should not throw with default labels (gauge)', async () => {
					const r = new Registry();
					r.setDefaultLabels({
						env: 'development',
					});

					const gauge = new Gauge({
						name: 'my_gauge',
						help: 'my gauge',
						registers: [r],
						labelNames: ['type'],
					});

					const myGauge = gauge.labels('myType');

					myGauge.inc(1);

					const metrics = await r.getMetricsAsJSON();
					expect(metrics).toContainEqual({
						aggregator: 'sum',
						help: 'my gauge',
						name: 'my_gauge',
						type: 'gauge',
						values: [
							{
								labels: { env: 'development', type: 'myType' },
								value: 1,
							},
						],
					});

					myGauge.inc(2);

					const metrics2 = await r.getMetricsAsJSON();
					expect(metrics2).toContainEqual({
						aggregator: 'sum',
						help: 'my gauge',
						name: 'my_gauge',
						type: 'gauge',
						values: [
							{
								labels: { env: 'development', type: 'myType' },
								value: 3,
							},
						],
					});
				});

				it('should not throw with default labels (histogram)', async () => {
					const r = new Registry();
					r.setDefaultLabels({
						env: 'development',
					});

					const hist = new Histogram({
						name: 'my_histogram',
						help: 'my histogram',
						registers: [r],
						labelNames: ['type'],
					});

					const myHist = hist.labels('myType');

					myHist.observe(1);

					const metrics = await r.getMetricsAsJSON();
					// NOTE: at this test we don't need to check exacte JSON schema
					expect(metrics[0].values).toContainEqual({
						labels: { env: 'development', le: 1, type: 'myType' },
						metricName: 'my_histogram_bucket',
						value: 1,
					});

					myHist.observe(1);

					const metrics2 = await r.getMetricsAsJSON();
					// NOTE: at this test we don't need to check exacte JSON schema
					expect(metrics2[0].values).toContainEqual({
						labels: { env: 'development', le: 1, type: 'myType' },
						metricName: 'my_histogram_bucket',
						value: 2,
					});
				});
			});
		});
	});

	describe('merging', () => {
		const Registry = require('../lib/registry');
		let registryOne;
		let registryTwo;

		beforeEach(() => {
			registryOne = new Registry();
			registryTwo = new Registry();
		});

		it('should merge all provided registers', async () => {
			registryOne.registerMetric(getMetric('one'));
			registryTwo.registerMetric(getMetric('two'));

			const merged = await Registry.merge([
				registryOne,
				registryTwo,
			]).getMetricsAsJSON();
			expect(merged).toHaveLength(2);
		});

		it('should throw if same name exists on both registers', () => {
			registryOne.registerMetric(getMetric());
			registryTwo.registerMetric(getMetric());

			const fn = function () {
				Registry.merge([registryOne, registryTwo]);
			};

			expect(fn).toThrowError(Error);
		});
	});

	it('should have the same contentType as the module', () => {
		const moduleWideContentType = require('../').contentType;
		expect(register.contentType).toEqual(moduleWideContentType);
	});

	function getMetric(name) {
		name = name || 'test_metric';
		return {
			name,
			async get() {
				return {
					name,
					type: 'counter',
					help: 'A test metric',
					values: [
						{
							value: 12,
							labels: {
								label: 'hello',
								code: '303',
							},
						},
						{
							value: 34,
							labels: {
								label: 'bye',
								code: '404',
							},
						},
					],
				};
			},
		};
	}
});
