describe("Retractions", function () {
	var userLibraryID;
	var win;
	var zp;
	var checkQueueItemsStub;
	var retractedDOI = '10.1056/NEJMoa1200303'; // mixed case
	
	before(async function () {
		userLibraryID = Zotero.Libraries.userLibraryID;
		win = await loadZoteroPane();
		zp = win.ZoteroPane;
		
		await Zotero.Retractions.updateFromServer();
		
		// Remove debouncing on checkQueuedItems()
		checkQueueItemsStub = sinon.stub(Zotero.Retractions, 'checkQueuedItems').callsFake(() => {
			return Zotero.Retractions._checkQueuedItemsInternal();
		});
	});
	
	beforeEach(async function () {
		var ids = await Zotero.DB.columnQueryAsync("SELECT itemID FROM retractedItems");
		if (ids.length) {
			await Zotero.Items.erase(ids);
		}
	});
	
	afterEach(async function () {
		win.document.getElementById('retracted-items-close').click();
		checkQueueItemsStub.resetHistory();
	});
	
	after(async function () {
		win.close();
		checkQueueItemsStub.restore();
		
		var ids = await Zotero.DB.columnQueryAsync("SELECT itemID FROM retractedItems");
		if (ids.length) {
			await Zotero.Items.erase(ids);
		}
	});
	
	async function createRetractedItem(options = {}) {
		var o = {
			itemType: 'journalArticle'
		};
		Object.assign(o, options);
		var item = createUnsavedDataObject('item', o);
		item.setField('DOI', retractedDOI);
		if (Zotero.DB.inTransaction()) {
			await item.save();
		}
		else {
			await item.saveTx();
		}
		
		while (!checkQueueItemsStub.called) {
			await Zotero.Promise.delay(50);
		}
		await checkQueueItemsStub.returnValues[0];
		checkQueueItemsStub.resetHistory();
		
		return item;
	}
	
	async function createRetractedItemWithExtraDOI(options = {}) {
		var o = {
			itemType: 'journalArticle'
		};
		Object.assign(o, options);
		var item = createUnsavedDataObject('item', o);
		item.setField('extra', 'DOI: ' + retractedDOI);
		if (Zotero.DB.inTransaction()) {
			await item.save();
		}
		else {
			await item.saveTx();
		}
		
		while (!checkQueueItemsStub.called) {
			await Zotero.Promise.delay(50);
		}
		await checkQueueItemsStub.returnValues[0];
		checkQueueItemsStub.resetHistory();
		
		return item;
	}
	
	function bannerShown() {
		var container = win.document.getElementById('retracted-items-container');
		if (container.getAttribute('collapsed') == 'true') {
			return false;
		}
		if (!container.hasAttribute('collapsed')) {
			return true;
		}
		throw new Error("'collapsed' attribute not found");
	}
	
	
	describe("#updateFromServer()", function () {
		var server;
		var baseURL;
		
		before(function () {
			Zotero.HTTP.mock = sinon.FakeXMLHttpRequest;
			baseURL = ZOTERO_CONFIG.API_URL + 'retractions/';
		});
		
		beforeEach(function () {
			server = sinon.fakeServer.create();
			server.autoRespond = true;
		});
		
		after(async function () {
			Zotero.HTTP.mock = null;
			// Restore the real list from the server. We could just mock it as part of the suite.
			await Zotero.Retractions.updateFromServer();
		});
		
		/*it("shouldn't show banner or virtual collection for already flagged items on list update", async function () {
			await Zotero.Retractions.updateFromServer();
		});*/
		
		it("should remove retraction flag from items that no longer match prefix list", async function () {
			var doi = '10.1234/abcde';
			var hash = Zotero.Utilities.Internal.sha1(doi);
			var prefix = hash.substr(0, 5);
			var lines = [
				Zotero.Retractions.TYPE_DOI + prefix + ' 12345\n',
				Zotero.Retractions.TYPE_DOI + 'aaaaa 23456\n'
			];
			
			var listCount = 0;
			var searchCount = 0;
			server.respond(function (req) {
				if (req.method == 'GET' && req.url == baseURL + 'list') {
					listCount++;
					if (listCount == 1) {
						req.respond(
							200,
							{
								'Content-Type': 'text/plain',
								'ETag': 'abcdefg'
							},
							lines.join('')
						);
					}
					else if (listCount == 2) {
						req.respond(
							200,
							{
								'Content-Type': 'text/plain',
								'ETag': 'bcdefgh'
							},
							lines[1]
						);
					}
				}
				else if (req.method == 'POST' && req.url == baseURL + 'search') {
					searchCount++;
					if (searchCount == 1) {
						req.respond(
							200,
							{
								'Content-Type': 'application/json'
							},
							JSON.stringify([
								{
									doi: hash,
									retractionDOI: '10.1234/bcdef',
									date: '2019-01-02',
									reasons: [
										"Error in Data"
									],
									urls: []
								}
							])
						);
					}
				}
			});
			
			await Zotero.Retractions.updateFromServer();
			
			// Create item with DOI from list
			var promise = waitForItemEvent('refresh');
			var item = createUnsavedDataObject('item', { itemType: 'journalArticle' });
			item.setField('DOI', doi);
			await item.saveTx();
			await promise;
			
			assert.isTrue(Zotero.Retractions.isRetracted(item));
			
			// Make a second request, with the entry removed
			promise = waitForItemEvent('refresh');
			await Zotero.Retractions.updateFromServer();
			await promise;
			
			assert.isFalse(Zotero.Retractions.isRetracted(item));
		});
	});
	
	
	describe("#shouldShowCitationWarning()", function () {
		it("should return false if citation warning is hidden", async function () {
			var item = await createRetractedItem();
			assert.isTrue(Zotero.Retractions.shouldShowCitationWarning(item));
			await Zotero.Retractions.disableCitationWarningsForItem(item);
			assert.isFalse(Zotero.Retractions.shouldShowCitationWarning(item));
		});
		
		it("should return false if retraction is hidden", async function () {
			var item = await createRetractedItem();
			assert.isTrue(Zotero.Retractions.shouldShowCitationWarning(item));
			await Zotero.Retractions.hideRetraction(item);
			assert.isFalse(Zotero.Retractions.shouldShowCitationWarning(item));
		});
	});
	
	
	describe("#getRetractionsFromJSON()", function () {
		it("should identify object with retracted DOI", async function () {
			var spy = sinon.spy(Zotero.HTTP, 'request');
			var json = [
				{
					
				},
				{
					DOI: retractedDOI
				},
				{
					DOI: '10.1234/abcd'
				}
			];
			
			var indexes = await Zotero.Retractions.getRetractionsFromJSON(json);
			assert.sameMembers(indexes, [1]);
			assert.equal(spy.callCount, 1);
			
			indexes = await Zotero.Retractions.getRetractionsFromJSON(json);
			assert.sameMembers(indexes, [1]);
			// Result should've been cached, so we should have it without another API request
			assert.equal(spy.callCount, 1);
			
			spy.restore();
		});
		
		
		it("should identify object with retracted DOI in Extra", async function () {
			var json = [
				{
					extra: `DOI: ${retractedDOI}`
				}
			];
			
			var indexes = await Zotero.Retractions.getRetractionsFromJSON(json);
			assert.sameMembers(indexes, [0]);
		});
		
		it("should identify object with retracted DOI on subsequent line in Extra", async function () {
			var json = [
				{
					extra: `Foo: Bar\nDOI: ${retractedDOI}`
				}
			];
			
			var indexes = await Zotero.Retractions.getRetractionsFromJSON(json);
			assert.sameMembers(indexes, [0]);
		});
	});
	
	
	describe("Notification Banner", function () {
		it("should show banner when retracted item is added", async function () {
			assert.isFalse(bannerShown());
			await createRetractedItem();
			do {
				await delay(10);
			}
			while (!bannerShown());
		});
		
		it("should show banner when retracted item with DOI in Extra is added", async function () {
			assert.isFalse(bannerShown());
			await createRetractedItemWithExtraDOI();
			do {
				await delay(10);
			}
			while (!bannerShown());
		});
		
		it("shouldn't show banner when item in trash is added", async function () {
			await createRetractedItem({ deleted: true });
			await delay(50);
			assert.isFalse(bannerShown());
		});
	});
	
	describe("virtual collection", function () {
		it("should show/hide Retracted Items collection when a retracted item is found/erased", async function () {
			// Create item
			var item = await createRetractedItem();
			assert.ok(zp.collectionsView.getRowIndexByID("R" + userLibraryID));
			
			// Erase item
			var promise = waitForItemEvent('refresh');
			await item.eraseTx();
			await promise;
			assert.isFalse(zp.collectionsView.getRowIndexByID("R" + userLibraryID));
		});
		
		it("should unhide Retracted Items collection when retracted item is found", async function () {
			await createRetractedItem();
			
			// Hide collection
			await zp.setVirtual(userLibraryID, 'retracted', false);
			
			// Add another retracted item, which should unhide it
			await createRetractedItem();
			assert.ok(zp.collectionsView.getRowIndexByID("R" + userLibraryID));
		});
		
		it("should hide Retracted Items collection when last retracted item is moved to trash", async function () {
			var rowID = "R" + userLibraryID;
			
			// Create item
			var item = await createRetractedItem();
			assert.ok(zp.collectionsView.getRowIndexByID(rowID));
			
			// Select Retracted Items collection
			await zp.collectionsView.selectByID(rowID);
			await waitForItemsLoad(win);
			
			// Erase item
			item.deleted = true;
			await item.saveTx();
			await Zotero.Promise.delay(50);
			// Retracted Items should be gone
			assert.isFalse(zp.collectionsView.getRowIndexByID(rowID));
			// And My Library should be selected
			assert.equal(zp.collectionsView.selectedTreeRow.id, "L" + userLibraryID);
		});
		
		it("should hide Retracted Items collection when last retracted item is marked as hidden", async function () {
			var rowID = "R" + userLibraryID;
			
			// Create item
			var item = await createRetractedItem();
			assert.ok(zp.collectionsView.getRowIndexByID(rowID));
			
			// Select Retracted Items collection
			await zp.collectionsView.selectByID(rowID);
			await waitForItemsLoad(win);
			
			await Zotero.Retractions.hideRetraction(item);
			
			await Zotero.Promise.delay(50);
			// Retracted Items should be gone
			assert.isFalse(zp.collectionsView.getRowIndexByID(rowID));
			// And My Library should be selected
			assert.equal(zp.collectionsView.selectedTreeRow.id, "L" + userLibraryID);
		});
		
		it("shouldn't hide Retracted Items collection when last retracted item is marked to not show a citation warning", async function () {
			var rowID = "R" + userLibraryID;
			
			// Create item
			var item = await createRetractedItem();
			assert.ok(zp.collectionsView.getRowIndexByID(rowID));
			
			// Select Retracted Items collection
			await zp.collectionsView.selectByID(rowID);
			await waitForItemsLoad(win);
			
			await Zotero.Retractions.disableCitationWarningsForItem(item);
			
			await Zotero.Promise.delay(50);
			// Should still be showing
			assert.ok(zp.collectionsView.getRowIndexByID("R" + userLibraryID));
		});
		
		it("should show Retracted Items collection when retracted item is restored from trash", async function () {
			// Create trashed item
			var item = await createRetractedItem({ deleted: true });
			await Zotero.Promise.delay(50);
			assert.isFalse(zp.collectionsView.getRowIndexByID("R" + userLibraryID));
			
			// Restore item
			item.deleted = false;
			await item.saveTx();
			await Zotero.Promise.delay(50);
			assert.ok(zp.collectionsView.getRowIndexByID("R" + userLibraryID));
		});
	});
	
	describe("retractions.enabled", function () {
		beforeEach(function () {
			Zotero.Prefs.clear('retractions.enabled');
		});
		
		it("should hide virtual collection and banner when false", async function () {
			var item = await createRetractedItem();
			await Zotero.Promise.delay(50);
			var itemRetractionBox = win.document.getElementById('retraction-box');
			assert.isFalse(itemRetractionBox.hidden);
			
			var spies = [
				sinon.spy(Zotero.Retractions, '_removeAllEntries'),
				sinon.spy(Zotero.Retractions, 'isRetracted')
			];
			Zotero.Prefs.set('retractions.enabled', false);
			
			while (!spies[0].called || !spies[1].called) {
				await Zotero.Promise.delay(50);
			}
			await spies[0].returnValues[0];
			await spies[1].returnValues[0];
			spies.forEach(spy => spy.restore());
			
			assert.isFalse(Zotero.Retractions.isRetracted(item));
			assert.isFalse(zp.collectionsView.getRowIndexByID("R" + userLibraryID));
			assert.isFalse(bannerShown());
			
			assert.isTrue(itemRetractionBox.hidden);
			
			await item.eraseTx();
		});
	});
});
