const { MultimediaViewer } = require( 'mmv' );
const { getMultimediaViewer } = require( './mmv.testhelpers.js' );
const { MultimediaViewerBootstrap, LightboxImage } = require( 'mmv.bootstrap' );
const router = require( 'mediawiki.router' );
const config = require( 'mmv/mmv/config.json' );

QUnit.module( 'mmv', QUnit.newMwEnvironment( {
	beforeEach: function () {
		// prevent a real "back" navigation from taking place
		this.sandbox.stub( router, 'back' );

		// T277470: Apply consistent $wgMediaViewerUseThumbnailGuessing=false default
		sinon.replace( config, 'useThumbnailGuessing', false );
	},
	afterEach: function () {
		router.resetForTest();
	}
} ) );

QUnit.test( 'Progress', function ( assert ) {
	const imageDeferred = $.Deferred();
	const viewer = getMultimediaViewer();
	const fakeImage = {
		filePageTitle: new mw.Title( 'File:Stuff.jpg' )
	};
	// custom clock ensures progress handlers execute in correct sequence
	const clock = this.sandbox.useFakeTimers();

	viewer.thumbs = [];
	viewer.displayPlaceholderThumbnail = function () {};
	viewer.setImage = function () {};
	viewer.scroll = function () {};
	viewer.fetchSizeIndependentLightboxInfo = () => $.Deferred().resolve( {} );
	viewer.ui = {
		setFileReuseData: function () {},
		setupForLoad: function () {},
		canvas: { set: function () {},
			getCurrentImageWidths: function () {
				return { real: 0 };
			},
			getDimensions: function () {
				return {};
			}
		},
		panel: {
			setImageInfo: function () {},
			scroller: {
				animateMetadataOnce: function () {}
			},
			progressBar: {
				hide: this.sandbox.stub(),
				animateTo: this.sandbox.stub(),
				jumpTo: this.sandbox.stub()
			}
		},
		open: function () {} };

	viewer.imageProvider.get = () => imageDeferred.promise();
	viewer.imageInfoProvider.get = () => $.Deferred().resolve( {} );
	viewer.thumbnailInfoProvider.get = () => $.Deferred().resolve( {} );

	// loadImage will call setupProgressBar, which will attach done, fail &
	// progress handlers
	viewer.loadImage( fakeImage );
	clock.tick( 10 );
	assert.strictEqual( viewer.ui.panel.progressBar.jumpTo.lastCall.calledWith( 0 ), true,
		'Percentage correctly reset by loadImage' );
	assert.strictEqual( viewer.ui.panel.progressBar.animateTo.firstCall.calledWith( 5 ), true,
		'Percentage correctly animated to 5 by loadImage' );

	imageDeferred.notify( 'response', 45 );
	clock.tick( 10 );
	assert.strictEqual( viewer.ui.panel.progressBar.animateTo.secondCall.calledWith( 45 ), true,
		'Percentage correctly funneled to panel UI' );

	imageDeferred.resolve( {}, {} );
	clock.tick( 10 );
	assert.strictEqual( viewer.ui.panel.progressBar.animateTo.thirdCall.calledWith( 100 ), true,
		'Percentage correctly funneled to panel UI' );

	clock.restore();

	viewer.close();
} );

QUnit.test( 'Progress when switching images', function ( assert ) {
	const firstImageDeferred = $.Deferred();
	const secondImageDeferred = $.Deferred();
	const firstImage = {
		index: 1,
		filePageTitle: new mw.Title( 'File:First.jpg' )
	};
	const secondImage = {
		index: 2,
		filePageTitle: new mw.Title( 'File:Second.jpg' )
	};
	const viewer = getMultimediaViewer();
	// custom clock ensures progress handlers execute in correct sequence
	const clock = this.sandbox.useFakeTimers();

	viewer.thumbs = [];
	viewer.displayPlaceholderThumbnail = function () {};
	viewer.setImage = function () {};
	viewer.scroll = function () {};
	viewer.preloadImagesMetadata = function () {};
	viewer.preloadThumbnails = function () {};
	viewer.fetchSizeIndependentLightboxInfo = () => $.Deferred().resolve( {} );
	viewer.ui = {
		setFileReuseData: function () {},
		setupForLoad: function () {},
		canvas: { set: function () {},
			getCurrentImageWidths: function () {
				return { real: 0 };
			},
			getDimensions: function () {
				return {};
			}
		},
		panel: {
			setImageInfo: function () {},
			scroller: {
				animateMetadataOnce: function () {}
			},
			progressBar: {
				hide: this.sandbox.stub(),
				animateTo: this.sandbox.stub(),
				jumpTo: this.sandbox.stub()
			}
		},
		open: function () {},
		empty: function () {} };

	viewer.imageInfoProvider.get = () => $.Deferred().resolve( {} );
	viewer.thumbnailInfoProvider.get = () => $.Deferred().resolve( {} );

	// load some image
	viewer.imageProvider.get = this.sandbox.stub().returns( firstImageDeferred );
	viewer.loadImage( firstImage );
	clock.tick( 10 );
	assert.strictEqual( viewer.ui.panel.progressBar.jumpTo.getCall( 0 ).calledWith( 0 ), true,
		'Percentage correctly reset for new first image' );
	assert.strictEqual( viewer.ui.panel.progressBar.animateTo.getCall( 0 ).calledWith( 5 ), true,
		'Percentage correctly animated to 5 for first new image' );

	// progress on active image
	firstImageDeferred.notify( 'response', 20 );
	clock.tick( 10 );
	assert.strictEqual( viewer.ui.panel.progressBar.animateTo.getCall( 1 ).calledWith( 20 ), true,
		'Percentage correctly animated when active image is loading' );

	// change to another image
	viewer.imageProvider.get = this.sandbox.stub().returns( secondImageDeferred );
	viewer.loadImage( secondImage );
	clock.tick( 10 );
	assert.strictEqual( viewer.ui.panel.progressBar.jumpTo.getCall( 1 ).calledWith( 0 ), true,
		'Percentage correctly reset for second new image' );
	assert.strictEqual( viewer.ui.panel.progressBar.animateTo.getCall( 2 ).calledWith( 5 ), true,
		'Percentage correctly animated to 5 for second new image' );

	// progress on active image
	secondImageDeferred.notify( 'response', 30 );
	clock.tick( 10 );
	assert.strictEqual( viewer.ui.panel.progressBar.animateTo.getCall( 3 ).calledWith( 30 ), true,
		'Percentage correctly animated when active image is loading' );

	// progress on inactive image
	firstImageDeferred.notify( 'response', 40 );
	clock.tick( 10 );
	assert.strictEqual( viewer.ui.panel.progressBar.animateTo.callCount, 4,
		'Percentage not animated when inactive image is loading' );

	// progress on active image
	secondImageDeferred.notify( 'response', 50 );
	clock.tick( 10 );
	assert.strictEqual( viewer.ui.panel.progressBar.animateTo.getCall( 4 ).calledWith( 50 ), true,
		'Percentage correctly ignored inactive image & only animated when active image is loading' );

	// change back to first image
	viewer.imageProvider.get = this.sandbox.stub().returns( firstImageDeferred );
	viewer.loadImage( firstImage );
	clock.tick( 10 );
	assert.strictEqual( viewer.ui.panel.progressBar.jumpTo.getCall( 2 ).calledWith( 40 ), true,
		'Percentage jumps to right value when changing images' );

	secondImageDeferred.resolve( {}, {} );
	clock.tick( 10 );
	assert.strictEqual( viewer.ui.panel.progressBar.hide.called, false,
		'Progress bar not hidden when something finishes in the background' );

	// change back to second image, which has finished loading
	viewer.imageProvider.get = this.sandbox.stub().returns( secondImageDeferred );
	viewer.loadImage( secondImage );
	clock.tick( 10 );
	assert.strictEqual( viewer.ui.panel.progressBar.hide.called, true,
		'Progress bar hidden when switching to finished image' );

	clock.restore();

	viewer.close();
} );

QUnit.test( 'resetThumbnailStates', ( assert ) => {
	const viewer = getMultimediaViewer();

	assert.strictEqual( viewer.realThumbnailShown, false, 'Real thumbnail state is correct' );

	viewer.realThumbnailShown = true;

	viewer.resetThumbnailStates();

	assert.strictEqual( viewer.realThumbnailShown, false, 'Real thumbnail state is correct' );
} );

QUnit.test( 'Placeholder first, then real thumbnail', function ( assert ) {
	const viewer = getMultimediaViewer();

	viewer.setImage = function () {};
	viewer.ui = { canvas: {
		maybeDisplayPlaceholder: function () {
		}
	} };
	viewer.imageInfoProvider.get = this.sandbox.stub();

	viewer.displayPlaceholderThumbnail( { originalWidth: 100, originalHeight: 100 }, undefined, undefined );

	assert.strictEqual( viewer.realThumbnailShown, false, 'Real thumbnail state is correct' );

	viewer.displayRealThumbnail( { url: undefined } );

	assert.strictEqual( viewer.realThumbnailShown, true, 'Real thumbnail state is correct' );
} );

QUnit.test( 'Placeholder first, then real thumbnail - missing size', function ( assert ) {
	const viewer = getMultimediaViewer();

	viewer.currentIndex = 1;
	viewer.setImage = function () {};
	viewer.ui = { canvas: {
		maybeDisplayPlaceholder: function () {
		}
	} };
	viewer.imageInfoProvider.get = this.sandbox.stub().returns( $.Deferred().resolve( { width: 100, height: 100 } ) );

	viewer.displayPlaceholderThumbnail( { index: 1 }, undefined, undefined );

	assert.strictEqual( viewer.realThumbnailShown, false, 'Real thumbnail state is correct' );

	viewer.displayRealThumbnail( { url: undefined } );

	assert.strictEqual( viewer.realThumbnailShown, true, 'Real thumbnail state is correct' );
} );

QUnit.test( 'Real thumbnail first, then placeholder', ( assert ) => {
	const viewer = getMultimediaViewer();

	viewer.setImage = function () {};
	viewer.ui = {
		showImage: function () {},
		canvas: {}
	};

	viewer.displayRealThumbnail( { url: undefined } );

	assert.strictEqual( viewer.realThumbnailShown, true, 'Real thumbnail state is correct' );

	viewer.displayPlaceholderThumbnail( {}, undefined, undefined );

	assert.strictEqual( viewer.realThumbnailShown, true, 'Real thumbnail state is correct' );
} );

QUnit.test( 'New image loaded while another one is loading', function ( assert ) {
	const viewer = getMultimediaViewer();
	const firstImageDeferred = $.Deferred();
	const secondImageDeferred = $.Deferred();
	const firstLigthboxInfoDeferred = $.Deferred();
	const secondLigthboxInfoDeferred = $.Deferred();
	const firstImage = {
		filePageTitle: new mw.Title( 'File:Foo.jpg' ),
		index: 0
	};
	const secondImage = {
		filePageTitle: new mw.Title( 'File:Bar.jpg' ),
		index: 1
	};
	// custom clock ensures progress handlers execute in correct sequence
	const clock = this.sandbox.useFakeTimers();

	viewer.fetchSizeIndependentLightboxInfo = this.sandbox.stub();
	viewer.ui = {
		setFileReuseData: function () {},
		setupForLoad: function () {},
		canvas: {
			set: function () {},
			getCurrentImageWidths: function () {
				return { real: 0 };
			},
			getDimensions: function () {
				return {};
			}
		},
		panel: {
			setImageInfo: this.sandbox.stub(),
			scroller: {
				animateMetadataOnce: function () {}
			},
			progressBar: {
				hide: this.sandbox.stub(),
				animateTo: this.sandbox.stub(),
				jumpTo: this.sandbox.stub()
			},
			empty: function () {}
		},
		open: function () {},
		empty: function () {} };
	viewer.displayRealThumbnail = this.sandbox.stub();
	viewer.pushLightboxImagesIntoQueue = function () {};
	viewer.animateMetadataDivOnce = this.sandbox.stub().returns( $.Deferred().reject() );
	viewer.imageProvider.get = this.sandbox.stub();
	viewer.imageInfoProvider.get = () => $.Deferred().reject( {} );
	viewer.thumbnailInfoProvider.get = () => $.Deferred().resolve( {} );

	viewer.imageProvider.get.returns( firstImageDeferred.promise() );
	viewer.fetchSizeIndependentLightboxInfo.returns( firstLigthboxInfoDeferred.promise() );
	viewer.loadImage( firstImage );
	clock.tick( 10 );
	assert.strictEqual( viewer.animateMetadataDivOnce.called, false, 'Metadata of the first image should not be animated' );
	assert.strictEqual( viewer.ui.panel.setImageInfo.called, false, 'Metadata of the first image should not be shown' );

	viewer.imageProvider.get.returns( secondImageDeferred.promise() );
	viewer.fetchSizeIndependentLightboxInfo.returns( secondLigthboxInfoDeferred.promise() );
	viewer.loadImage( secondImage );
	clock.tick( 10 );

	viewer.ui.panel.progressBar.animateTo.reset();
	firstImageDeferred.notify( undefined, 45 );
	clock.tick( 10 );
	assert.strictEqual( viewer.ui.panel.progressBar.animateTo.reset.called, undefined, 'Progress of the first image should not be shown' );

	firstImageDeferred.resolve( {}, {} );
	firstLigthboxInfoDeferred.resolve( {} );
	clock.tick( 10 );
	assert.strictEqual( viewer.displayRealThumbnail.called, false, 'The first image being done loading should have no effect' );

	viewer.displayRealThumbnail = this.sandbox.spy( () => viewer.close() );
	secondImageDeferred.resolve( {}, {} );
	secondLigthboxInfoDeferred.resolve( {} );
	clock.tick( 10 );
	assert.strictEqual( viewer.displayRealThumbnail.called, true, 'The second image being done loading should result in the image being shown' );

	clock.restore();
} );

QUnit.test( 'Events are not trapped after the viewer is closed', function ( assert ) {
	const viewer = getMultimediaViewer();
	const $document = $( document );
	const $qf = $( '#qunit-fixture' );
	const eventTypes = [ 'keydown', 'keyup', 'keypress', 'click', 'mousedown', 'mouseup' ];
	const modifiers = [ undefined, 'altKey', 'ctrlKey', 'shiftKey', 'metaKey' ];
	// Events are async, we need to wait for the last event to be caught before ending the test
	const done = assert.async();
	const oldScrollTo = $.scrollTo;

	assert.expect( 0 );

	// animation would keep running, conflict with other tests
	this.sandbox.stub( $.fn, 'animate' ).returnsThis();

	$.scrollTo = function () {
		return { scrollTop: () => {}, on: () => {}, off: () => {} };
	};

	viewer.setupEventHandlers();

	viewer.imageProvider.get = () => $.Deferred().reject();
	viewer.imageInfoProvider.get = () => $.Deferred().reject();
	viewer.thumbnailInfoProvider.get = () => $.Deferred().reject();

	viewer.initWithThumbs( [] );

	viewer.loadImage(
		{
			filePageTitle: new mw.Title( 'File:Stuff.jpg' ),
			thumbnail: new Image( 10, 10 )
		},
		new Image()
	);

	viewer.ui.$closeButton.trigger( 'click' );

	function eventHandler( e ) {
		if ( e.isDefaultPrevented() ) {
			assert.true( false, 'Event was incorrectly trapped: ' + e.which );
		}

		e.preventDefault();

		// Wait for the last event
		if ( e.which === 32 && e.type === 'mouseup' ) {
			$document.off( '.mmvtest' );
			viewer.cleanupEventHandlers();
			$.scrollTo = oldScrollTo;
			done();
		}
	}

	for ( let j = 0; j < eventTypes.length; j++ ) {
		$document.on( eventTypes[ j ] + '.mmvtest', eventHandler );

		eventloop:
		for ( let i = 0; i < 256; i++ ) {
			// Save some time by not testing unlikely values for mouse events
			if ( i > 32 ) {
				switch ( eventTypes[ j ] ) {
					case 'click':
					case 'mousedown':
					case 'mouseup':
						break eventloop;
				}
			}

			for ( let k = 0; k < modifiers.length; k++ ) {
				const eventParameters = { which: i };
				if ( modifiers[ k ] !== undefined ) {
					eventParameters[ modifiers[ k ] ] = true;
				}
				$qf.trigger( $.Event( eventTypes[ j ], eventParameters ) );
			}
		}
	}
} );

QUnit.test( 'Viewer is closed then navigating to #foo/bar/baz (page section including slash)', ( assert ) => {
	const done = assert.async();

	const bootstrap = new MultimediaViewerBootstrap();
	bootstrap.setupEventHandlers();
	location.hash = '#/media/File:Foo.jpg';
	setTimeout( () => {
		bootstrap.viewerPromise.then( ( viewer ) => {
			viewer.isOpen = true;
			// viewer.ui = undefined;
			// const closeSpy = this.sandbox.spy( viewer.close );
			// this.sandbox.stub( viewer, 'close' );
			location.hash = '#foo/bar/baz';

			// Wait for route event handler to execute
			setTimeout( () => {
				assert.false( viewer.isOpen, 'The viewer was closed' );
				location.hash = '#';
				bootstrap.cleanupEventHandlers();
				done();
			}, 100 );

		} );
	} );
} );

QUnit.test.each( 'Refuse to load too-big thumbnail', {
	'thumb for non-vector image should be capped to original size': {
		thumb: $( '<img>' ).attr( {
			src: 'https://example.test/images/0/0a/Foobar.png',
			width: 200,
			height: 150,
			'data-file-width': '800',
			'data-file-height': '600'
		} )[ 0 ],
		expected: 800
	},
	'thumb for vector image can be larger': {
		thumb: $( '<img>' ).attr( {
			src: 'https://example.test/images/thumb/1/1b/Foobar.svg/200px-Foobar.svg',
			width: 200,
			height: 150,
			'data-file-width': '800',
			'data-file-height': '600'
		} )[ 0 ],
		expected: 1000
	}

}, ( assert, { thumb, expected } ) => {
	const viewer = getMultimediaViewer();

	let actualWidth;
	viewer.thumbnailInfoProvider.get = function ( fileTitle, sampleUrl, width ) {
		actualWidth = width;
		return $.Deferred().reject();
	};

	const image = new LightboxImage(
		thumb.src,
		mw.Title.newFromImg( thumb ),
		0,
		1,
		thumb,
		'My caption'
	);
	viewer.fetchThumbnail( image, 1000 );
	assert.strictEqual( actualWidth, expected, 'actual width' );
} );

QUnit.test.each( 'fetchThumbnail()', {
	'cannot guess falls back to API': {
		sampleURL: 'https://example.test/thumb/8/8b/Copyleft.svg/300_guess_fail_no_px.png',
		callCount: {
			guessedThumbnailInfo: 1,
			imageinfoApi: 1,
			imageProvider: 1
		},
		imageProviderArgs: [ [ 'apiURL' ] ]
	},
	'guessed URL that loads is used': {
		sampleURL: 'https://example.test/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png',
		callCount: {
			guessedThumbnailInfo: 1,
			imageinfoApi: 0,
			imageProvider: 1
		},
		imageProviderArgs: [ [ 'https://example.test/thumb/8/8b/Copyleft.svg/600px-Copyleft.svg.png' ] ]
	},
	'guessed URL that 404s falls back to API': {
		sampleURL: 'https://example.test/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png',
		imageLoadGuess: $.Deferred().reject(),
		callCount: {
			guessedThumbnailInfo: 1,
			imageinfoApi: 1,
			imageProvider: 2
		},
		imageProviderArgs: [
			[ 'https://example.test/thumb/8/8b/Copyleft.svg/600px-Copyleft.svg.png' ],
			[ 'apiURL' ]
		]
	},
	'when the retry fallback fails fetchThumbnail should reject': {
		sampleURL: 'https://example.test/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png',
		imageLoadGuess: $.Deferred().reject(),
		imageLoadApi: $.Deferred().reject(),
		callCount: {
			guessedThumbnailInfo: 1,
			imageinfoApi: 1,
			imageProvider: 2
		},
		imageProviderArgs: [
			[ 'https://example.test/thumb/8/8b/Copyleft.svg/600px-Copyleft.svg.png' ],
			[ 'apiURL' ]
		],
		promiseState: 'rejected'
	},
	'with useThumbnailGuessing=false the API is used directly': {
		useThumbnailGuessing: false,
		sampleURL: 'https://example.test/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png',
		callCount: {
			guessedThumbnailInfo: 0,
			imageinfoApi: 1,
			imageProvider: 1
		},
		imageProviderArgs: [
			[ 'apiURL' ]
		]
	}
}, ( assert, fixture ) => {
	config.useThumbnailGuessing = fixture.useThumbnailGuessing ?? true;
	const clock = sinon.useFakeTimers();
	const viewer = new MultimediaViewer( {
		language: function () {}
	} );
	const guessedThumbnailInfoStub = sinon.spy( viewer.guessedThumbnailInfoProvider, 'get' );
	const thumbnailInfoStub = viewer.thumbnailInfoProvider.get = sinon.stub()
		.returns( $.Deferred().resolve( { url: 'apiURL' } ) );
	const imageStub = viewer.imageProvider.get = fixture.imageLoader ?? sinon.stub();
	imageStub.returns( fixture.imageLoadGuess ?? $.Deferred().resolve( {} ) );
	imageStub.withArgs( 'apiURL' ).returns( fixture.imageLoadApi ?? $.Deferred().resolve( {} ) );

	const thumb = $( '<img>' ).attr( {
		src: fixture.sampleURL,
		width: 300,
		height: 300,
		'data-file-width': '1000',
		'data-file-height': '1000'
	} )[ 0 ];
	const image = new LightboxImage(
		thumb.getAttribute( 'src' ),
		new mw.Title( 'File:Copyleft.svg' ),
		0,
		1,
		thumb,
		'My caption'
	);
	const promise = viewer.fetchThumbnail( image, 600 );

	clock.tick( 10 );
	assert.propEqual( {
		guessedThumbnailInfo: guessedThumbnailInfoStub.callCount,
		imageinfoApi: thumbnailInfoStub.callCount,
		imageProvider: imageStub.callCount
	}, fixture.callCount, 'call counts' );
	assert.deepEqual( imageStub.args, fixture.imageProviderArgs, 'ImageProvider arg' );
	assert.strictEqual( promise.state(), fixture.promiseState ?? 'resolved', 'fetchThumbnail promise state' );
} );

QUnit.test( 'document.title', function ( assert ) {
	const viewer = getMultimediaViewer();
	const bootstrap = new MultimediaViewerBootstrap();
	const title = new mw.Title( 'File:This_should_show_up_in_document_title.png' );
	const oldDocumentTitle = document.title;

	this.sandbox.stub( mw.loader, 'using' ).returns( $.Deferred().resolve( viewer ) );
	viewer.currentImage = { filePageTitle: title };
	bootstrap.setupEventHandlers();
	viewer.setTitle();

	assert.notStrictEqual( document.title.match( title.getNameText() ), null, 'File name is visible in title' );

	viewer.close();
	bootstrap.cleanupEventHandlers();

	assert.strictEqual( document.title, oldDocumentTitle, 'Original title restored after viewer is closed' );
} );
