'use strict';

/* eslint-disable max-nested-callbacks */

import EdsCCopyToClipboard from './eds-c-copy-to-clipboard';

const fixture = `
<div class="eds-c-copy-to-clipboard" data-test="copy-to-clipboard" data-component-copytoclipboard-wrapper="copy-to-clipboard-example-2">
	<div class="eds-c-form__field" data-test="copy-to-clipboard-form-field" style="--copy-to-clipboard-field-max-width: 200px;">
		<label for="copy-to-clipboard-example-2-field" id="copy-to-clipboard-example-2-input-label" class="eds-c-label u-visually-hidden">
				<span>Shareable link</span>
				<span data-test="copy-to-clipboard-live-region" data-component-copytoclipboard-live-region="copy-to-clipboard-example-2" role="status" aria-live="polite" data-live-region-text-on-success=" (copied)" data-live-region-text-on-error=" (not copied, please copy manually)">
				</span>
		</label>
		<input type="text" id="copy-to-clipboard-example-2-field" value="http://example-2.com" readonly="" class="eds-c-form__input" data-component-copytoclipboard-text-to-copy="copy-to-clipboard-example-2">
	</div>
	<div class="eds-c-copy-to-clipboard__control" data-test="copy-to-clipboard-control" data-component-copytoclipboard-control="copy-to-clipboard-example-2" data-button-text-on-success="Shareable link copied" data-button-text-on-error="Shareable link not copied">
	</div>
	<template data-button-template="">
		<button data-test="copy-to-clipboard-shareable-link" data-track="copy_event" data-track-context="application-name" type="button" class="eds-c-button eds-c-button--primary">
		<svg aria-hidden="true" focusable="false"><use xlink:href="/static/images/icons/eds-i-copy-medium.svg#eds-i-copy-medium"></use></svg><span>Copy shareable link</span>
		</button>
	</template>
</div>
`;

describe('Enhanced copyToClipboard component', () => {
	describe('Happy paths', () => {
		beforeEach(() => {
			document.body.innerHTML = fixture;
		});

		afterEach(() => {
			jest.clearAllMocks();
			document.body.innerHTML = '';
		});

		describe('`navigator.clipboard.writeText` is supported AND succeeds', () => {
			beforeEach(() => {
				const mockClipboard = {
					writeText: jest.fn().mockResolvedValue(true)
				};

				global.navigator.clipboard = mockClipboard;
			});

			/* eslint-disable-next-line jest/expect-expect */
			it(`enhances the core HTML with all the needed elements for the enhanced experience`, () => {
				const wrapper = document.querySelector(`[data-component-copytoclipboard-wrapper]`);

				new EdsCCopyToClipboard(wrapper);

				assertComponentInitialization();
			});

			testMouseAndKeyboardInteractions();
		});

		describe('`navigator.clipboard.writeText` is supported AND it fails AND `document.execCommand` succeeds', () => {
			beforeEach(() => {
				const mockClipboard = {
					writeText: jest.fn().mockRejectedValue(new Error('Failed copy'))
				};

				global.navigator.clipboard = mockClipboard;

				document.execCommand = jest.fn().mockReturnValue(true);
			});

			it(`enhances the core HTML with all the needed elements for the enhanced experience`, () => {
				const wrapper = document.querySelector(`[data-component-copytoclipboard-wrapper]`);

				new EdsCCopyToClipboard(wrapper);

				assertComponentInitialization();
			});

			testMouseAndKeyboardInteractions();
		});

		describe('`navigator.clipboard.writeText` is not supported AND `document.execCommand` is supported AND it succeeds', () => {
			const originExecCommand = document.execCommand;

			beforeEach(() => {
				global.navigator.clipboard = undefined;

				document.execCommand = jest.fn().mockReturnValue(true);
			});

			afterEach(() => {
				document.execCommand = originExecCommand;
			});

			/* eslint-disable-next-line jest/expect-expect */
			it(`enhances the core HTML with all the needed elements for the enhanced experience`, () => {
				const wrapper = document.querySelector(`[data-component-copytoclipboard-wrapper]`);

				new EdsCCopyToClipboard(wrapper);

				assertComponentInitialization();
			});

			testMouseAndKeyboardInteractions();
		});

		describe('waits for the expected duration before resetting the feedback when data-ui-feedback-duration is set', () => {
			const originExecCommand = document.execCommand;

			beforeEach(() => {
				global.navigator.clipboard = undefined;

				document.execCommand = jest.fn().mockReturnValue(true);

				const wrapper = document.querySelector(`[data-component-copytoclipboard-wrapper]`);

				const tenSecondsMs = 10000;
				wrapper.setAttribute('data-ui-feedback-duration', tenSecondsMs);

				new EdsCCopyToClipboard(wrapper);
			});

			afterEach(() => {
				document.execCommand = originExecCommand;
			});

			testMouseAndKeyboardInteractions();
		});
	});

	describe('Grumpy paths', () => {
		const originalClipboard = {...global.navigator.clipboard};		
		let wrapper;

		beforeEach(() => {
			document.body.innerHTML = fixture;
			wrapper = document.querySelector(`[data-component-copytoclipboard-wrapper]`);
		});

		afterEach(() => {
			document.body.innerHTML = '';
			global.navigator.clipboard = originalClipboard;
			jest.clearAllMocks();
		});

		describe('Necessary Web API not supported', () => {
			it('returns an empty object if `navigator.clipboard` is not supported', () => {
				global.navigator.clipboard = undefined;

				expect(new EdsCCopyToClipboard(wrapper)).toEqual({});
			});

			it('returns an empty object if `navigator.clipboard.writeText` is not supported', () => {
				global.navigator.clipboard.writeText = undefined;
				
				expect(new EdsCCopyToClipboard(wrapper)).toEqual({});
			});

			it('returns an empty object if both `navigator.clipboard.writeText` AND `document.execCommand` are not supported', () => {
				global.navigator.clipboard.writeText = undefined;
				document.execCommand = undefined;

				expect(new EdsCCopyToClipboard(wrapper)).toEqual({});
			});
		});

		describe('Core element(s) missing', () => {
			let errorSpy;

			beforeEach(() => {
				const mockClipboard = {
					writeText: jest.fn()
				};

				global.navigator.clipboard = mockClipboard;
				errorSpy = jest.spyOn(global.console, 'error').mockImplementation(() => {});
			});

			afterEach(() => {
				errorSpy.mockRestore();
			});

			it('logs an error if the wrapper is `undefined`', () => {
				new EdsCCopyToClipboard();

				expect(errorSpy).toHaveBeenCalledWith('Could not init copyToClipboard component. Wrapper not found.');
			});

			it('logs an error if the wrapper is `null`', () => {
				new EdsCCopyToClipboard(null);

				expect(errorSpy).toHaveBeenCalledWith('Could not init copyToClipboard component. Wrapper not found.');
			});

			it('logs an error if component `componentUID` is a blank string', () => {
				wrapper.dataset.componentCopytoclipboardWrapper = '';

				new EdsCCopyToClipboard(wrapper);

				expect(errorSpy).toHaveBeenCalledWith('Could not init copyToClipboard component. Missing `componentUID`.');
			});

			it('logs an error if live region is missing', () => {
				const liveRegion = wrapper.querySelector('[data-component-copytoclipboard-live-region]');
				liveRegion.parentNode.removeChild(liveRegion);

				new EdsCCopyToClipboard(wrapper);

				expect(errorSpy).toHaveBeenCalledWith('Could not init copyToClipboard component. Missing core elements.');
			});

			it('logs an error if input is missing', () => {
				const input = wrapper.querySelector('[data-component-copytoclipboard-text-to-copy]');
				input.parentNode.removeChild(input);

				new EdsCCopyToClipboard(wrapper);

				expect(errorSpy).toHaveBeenCalledWith('Could not init copyToClipboard component. Missing core elements.');
			});

			it('logs an error if control is missing', () => {
				const control = wrapper.querySelector('[data-component-copytoclipboard-control]');
				control.parentNode.removeChild(control);

				new EdsCCopyToClipboard(wrapper);

				expect(errorSpy).toHaveBeenCalledWith('Could not init copyToClipboard component. Missing core elements.');
			});

			it('logs an error if template is missing', () => {
				const template = wrapper.querySelector('[data-button-template]');
				template.parentNode.removeChild(template);

				new EdsCCopyToClipboard(wrapper);

				expect(errorSpy).toHaveBeenCalledWith('Could not init copyToClipboard component. Missing core elements.');
			});
		});

		describe('`navigator.clipboard` is not supported AND `document.execCommand` is supported AND it fails', () => {
			const originExecCommand = document.execCommand;

			beforeEach(() => {
				global.navigator.clipboard = undefined;
				document.execCommand = jest.fn().mockReturnValue(false);
			});

			afterEach(() => {
				document.execCommand = originExecCommand;
			});

			testForFailedCopy();
		});

		describe('`navigator.clipboard` is not supported AND `document.execCommand` is supported AND it throws an error', () => {
			const originExecCommand = document.execCommand;

			beforeEach(() => {
				global.navigator.clipboard = undefined;

				document.execCommand = jest.fn().mockImplementation(() => {
					throw new Error('Failed copy');
				});
			});

			afterEach(() => {
				document.execCommand = originExecCommand;
			});

			testForFailedCopy();
		});

		describe('`navigator.clipboard.writeText` is supported AND it fails AND `document.execCommand` is not supported', () => {
			const originExecCommand = document.execCommand;

			beforeEach(() => {
				const mockClipboard = {
					writeText: jest.fn().mockRejectedValue(new Error('Clipboard `writeText` failed'))
				};

				global.navigator.clipboard = mockClipboard;
				document.execCommand = undefined;
			});

			afterEach(() => {
				document.execCommand = originExecCommand;
			});

			testForFailedCopy();
		});

		describe('`navigator.clipboard.writeText` is supported AND it fails AND `document.execCommand` is supported AND it fails', () => {
			const originExecCommand = document.execCommand;

			beforeEach(() => {
				const mockClipboard = {
					writeText: jest.fn().mockRejectedValue(new Error('Clipboard `writeText` failed'))
				};

				global.navigator.clipboard = mockClipboard;
				document.execCommand = jest.fn().mockReturnValue(false);
			});

			afterEach(() => {
				document.execCommand = originExecCommand;
			});

			testForFailedCopy();
		});
	});
});

// -----------------
// UTILITY FUNCTIONS
// -----------------

/**
 * Create a keyboard event.
 *
 * @param {string} key - Keyboard key for the event.
 * @param {string} eventType - Type of key event.
 * @returns {object} Event object.
 */
function createKeyboardEvent(key, eventType) {
	const event = new Event(eventType, {
		bubbles: true
	});

	switch (key) {
		case 'Enter': {
			event.key = 'Enter';
			break;
		}
		case 'Space': {
			event.key = ' ';
			break;
		}
		// Older browsers may return "Spacebar"
		case 'Spacebar': {
			event.key = 'Spacebar';
			break;
		}
		default: {
			throw new Error('key should be "Enter", "Space", "Spacebar"');
		}
	}

	return event;
}

function assertComponentInitialization(wrapperContext = '') {
	const wrapper = document.querySelector(`[data-component-copytoclipboard-wrapper${wrapperContext ? `="${wrapperContext}"` : ''}]`);

	const button = wrapper.querySelector('button');
	const buttonIcon = button.querySelector('svg > [xlink:href]');

	expect(button).not.toBe(null);
	expect(buttonIcon.getAttribute('xlink:href')).toEqual('/static/images/icons/eds-i-copy-medium.svg#eds-i-copy-medium');
}

function assertUIFeedbackDuration(edsCopyToClipboard, liveRegion) {
	const {uiFeedbackDuration} = edsCopyToClipboard;
	const {
		liveRegionTextOnSuccess,
	} = liveRegion.dataset;

	const halfUiFeedbackDuration = uiFeedbackDuration / 2;

	// initial state, button has just been clicked, so feedback should be shown
	expect(liveRegion.textContent).toEqual(liveRegionTextOnSuccess);

	// fast forward to HALF of the expected duration, to ensure that feedback has not been reset yet
	jest.advanceTimersByTime(halfUiFeedbackDuration);

	expect(liveRegion.textContent).toEqual(liveRegionTextOnSuccess);

	// fast forward AGAIN by half of the expected duration (to the end), to ensure that feedback HAS been reset
	jest.advanceTimersByTime(halfUiFeedbackDuration);

	expect(liveRegion.textContent).toEqual('');
}

function testMouseAndKeyboardInteractions() {
	it('upon mouse click, writes to the clipboard, updates the text-to-copy field label with a success message AND reverts it after some time', async () => {
		jest.useFakeTimers();

		const wrapper = document.querySelector('[data-component-copytoclipboard-wrapper]');
		const liveRegion = wrapper.querySelector('[data-component-copytoclipboard-live-region]');

		const edsCopyToClipboard = new EdsCCopyToClipboard(wrapper);

		const button = wrapper.querySelector('button');

		button.click();

		// Guarantee all promises initiated by this test are complete
		// source: https://gist.github.com/apieceofbart/e6dea8d884d29cf88cdb54ef14ddbcc4#file-test-js-L65
		await new Promise(jest.requireActual('timers').setImmediate);

		assertUIFeedbackDuration(edsCopyToClipboard, liveRegion);
	});

	const events = [
		{key: 'Enter', eventType: 'keydown'},
		{key: 'Space', eventType: 'keyup'},
		{key: 'Spacebar', eventType: 'keyup'}
	];

	for (const event of events) {
		const {key, eventType} = event;

		it(`upon keyboard "${eventType}" event with key "${key}", updates the text-to-copy field label with a success message AND reverts it after some time`, async () => {
			jest.useFakeTimers();

			const wrapper = document.querySelector('[data-component-copytoclipboard-wrapper]');
			const liveRegion = wrapper.querySelector('[data-component-copytoclipboard-live-region]');

			const edsCopyToClipboard = new EdsCCopyToClipboard(wrapper);

			const button = wrapper.querySelector('button');

			const keyEvent = createKeyboardEvent(key, eventType);
			button.dispatchEvent(keyEvent);

			// Guarantee all promises initiated by this test are complete
			// source: https://gist.github.com/apieceofbart/e6dea8d884d29cf88cdb54ef14ddbcc4#file-test-js-L65
			await new Promise(jest.requireActual('timers').setImmediate);

			assertUIFeedbackDuration(edsCopyToClipboard, liveRegion);
		});
	}
}

function testForFailedCopy() {
	it('updates the text-to-copy field label with an error message AND then reverts it after some time', async () => {
		jest.useFakeTimers();

		const wrapper = document.querySelector('[data-component-copytoclipboard-wrapper]');
		const liveRegion = wrapper.querySelector('[data-component-copytoclipboard-live-region]');

		const {
			liveRegionTextOnError,
		} = liveRegion.dataset;

		new EdsCCopyToClipboard(wrapper);

		const button = wrapper.querySelector('button');

		button.click();

		// Guarantee all promises initiated by this test are complete
		// source: https://gist.github.com/apieceofbart/e6dea8d884d29cf88cdb54ef14ddbcc4#file-test-js-L65
		await new Promise(jest.requireActual('timers').setImmediate);

		expect(liveRegion.textContent).toEqual(liveRegionTextOnError);

		jest.runOnlyPendingTimers();

		expect(liveRegion.textContent).toEqual('');
	});
}

