/**
 * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/* eslint-env commonjs, browser */

import Token from '../../src/token/token';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';

describe( 'Token', () => {
	let requests;

	beforeEach( () => {
		requests = [];

		const xhr = sinon.useFakeXMLHttpRequest();

		xhr.onCreate = request => {
			requests.push( request );
		};
	} );

	afterEach( () => {
		sinon.restore();
	} );

	describe( 'constructor()', () => {
		it( 'should throw error when no tokenUrl provided', () => {
			expect( () => new Token() ).to.throw(
				CKEditorError,
				'token-missing-token-url'
			);
		} );

		it( 'should set a init token value', () => {
			const token = new Token( 'http://token-endpoint', { initValue: 'initValue', autoRefresh: false } );

			expect( token.value ).to.equal( 'initValue' );
		} );

		it( 'should fire `change:value` event if the value of the token has changed', done => {
			const token = new Token( 'http://token-endpoint', { autoRefresh: false } );

			token.on( 'change:value', ( event, name, newValue ) => {
				expect( newValue ).to.equal( 'token-value' );

				done();
			} );

			token.init();

			requests[ 0 ].respond( 200, '', 'token-value' );
		} );

		it( 'should accept the callback in the constructor', () => {
			expect( () => {
				// eslint-disable-next-line
				const token = new Token( () => Promise.resolve( 'token' ) );
			} ).to.not.throw();
		} );
	} );

	describe( 'init()', () => {
		it( 'should get a token value from endpoint', done => {
			const token = new Token( 'http://token-endpoint', { autoRefresh: false } );

			token.init()
				.then( () => {
					expect( token.value ).to.equal( 'token-value' );

					done();
				} );

			requests[ 0 ].respond( 200, '', 'token-value' );
		} );

		it( 'should get a token from the refreshToken function when is provided', () => {
			const token = new Token( () => Promise.resolve( 'token-value' ), { autoRefresh: false } );

			return token.init()
				.then( () => {
					expect( token.value ).to.equal( 'token-value' );
				} );
		} );

		it( 'should start token refresh every 1 hour', done => {
			const clock = sinon.useFakeTimers( { toFake: [ 'setInterval' ] } );

			const token = new Token( 'http://token-endpoint', { initValue: 'initValue' } );

			token.init()
				.then( () => {
					clock.tick( 3600000 );
					clock.tick( 3600000 );
					clock.tick( 3600000 );
					clock.tick( 3600000 );
					clock.tick( 3600000 );

					expect( requests.length ).to.equal( 5 );

					clock.restore();

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

	describe( 'destroy', () => {
		it( 'should stop refreshing the token', () => {
			const clock = sinon.useFakeTimers( { toFake: [ 'setInterval', 'clearInterval' ] } );
			const token = new Token( 'http://token-endpoint', { initValue: 'initValue' } );

			return token.init()
				.then( () => {
					clock.tick( 3600000 );
					clock.tick( 3600000 );

					expect( requests.length ).to.equal( 2 );

					token.destroy();

					clock.tick( 3600000 );
					clock.tick( 3600000 );
					clock.tick( 3600000 );

					expect( requests.length ).to.equal( 2 );
				} );
		} );
	} );

	describe( 'refreshToken()', () => {
		it( 'should get a token from the specified address', done => {
			const token = new Token( 'http://token-endpoint', { initValue: 'initValue', autoRefresh: false } );

			token.refreshToken()
				.then( newToken => {
					expect( newToken.value ).to.equal( 'token-value' );

					done();
				} );

			requests[ 0 ].respond( 200, '', 'token-value' );
		} );

		it( 'should get a token from the specified callback function', () => {
			const token = new Token( () => Promise.resolve( 'token-value' ), { initValue: 'initValue', autoRefresh: false } );

			return token.refreshToken()
				.then( newToken => {
					expect( newToken.value ).to.equal( 'token-value' );
				} );
		} );

		it( 'should throw an error when cannot download new token', () => {
			const token = new Token( 'http://token-endpoint', { initValue: 'initValue', autoRefresh: false } );
			const promise = token._refresh();

			requests[ 0 ].respond( 401 );

			return promise.then( () => {
				throw new Error( 'Promise should be rejected' );
			}, error => {
				expect( error.constructor ).to.equal( CKEditorError );
				expect( error ).to.match( /token-cannot-download-new-token/ );
			} );
		} );

		it( 'should throw an error when the response is aborted', () => {
			const token = new Token( 'http://token-endpoint', { initValue: 'initValue', autoRefresh: false } );
			const promise = token._refresh();

			requests[ 0 ].abort();

			return promise.then( () => {
				throw new Error( 'Promise should be rejected' );
			}, error => {
				expect( error ).to.match( /Abort/ );
			} );
		} );

		it( 'should throw an error when network error occurs', () => {
			const token = new Token( 'http://token-endpoint', { initValue: 'initValue', autoRefresh: false } );
			const promise = token._refresh();

			requests[ 0 ].error();

			return promise.then( () => {
				throw new Error( 'Promise should be rejected' );
			}, error => {
				expect( error ).to.match( /Network Error/ );
			} );
		} );

		it( 'should throw an error when the callback throws error', () => {
			const token = new Token( () => Promise.reject( 'Custom error occurred' ), { initValue: 'initValue', autoRefresh: false } );

			token.refreshToken()
				.catch( error => {
					expect( error ).to.equal( 'Custom error occurred' );
				} );
		} );
	} );

	describe( '_startRefreshing()', () => {
		it( 'should start refreshing', () => {
			const clock = sinon.useFakeTimers( { toFake: [ 'setInterval' ] } );

			const token = new Token( 'http://token-endpoint', { initValue: 'initValue', autoRefresh: false } );

			token._startRefreshing();

			clock.tick( 3600000 );
			clock.tick( 3600000 );
			clock.tick( 3600000 );
			clock.tick( 3600000 );
			clock.tick( 3600000 );

			expect( requests.length ).to.equal( 5 );

			clock.restore();
		} );
	} );

	describe( '_stopRefreshing()', () => {
		it( 'should stop refreshing', done => {
			const clock = sinon.useFakeTimers( { toFake: [ 'setInterval', 'clearInterval' ] } );

			const token = new Token( 'http://token-endpoint', { initValue: 'initValue' } );

			token.init()
				.then( () => {
					clock.tick( 3600000 );
					clock.tick( 3600000 );
					clock.tick( 3600000 );

					token._stopRefreshing();

					clock.tick( 3600000 );
					clock.tick( 3600000 );

					expect( requests.length ).to.equal( 3 );

					clock.restore();

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

	describe( 'static create()', () => {
		it( 'should return a initialized token', done => {
			Token.create( 'http://token-endpoint', { autoRefresh: false } )
				.then( token => {
					expect( token.value ).to.equal( 'token-value' );

					done();
				} );

			requests[ 0 ].respond( 200, '', 'token-value' );
		} );

		it( 'should use default options when none passed', done => {
			const intervalSpy = sinon.spy( window, 'setInterval' );

			Token.create( 'http://token-endpoint' )
				.then( () => {
					expect( intervalSpy.args[ 0 ][ 1 ] ).to.equal( 3600000 );

					done();
				} );

			requests[ 0 ].respond( 200, '', 'token-value' );
		} );
	} );
} );
