"use strict";

describe("Zotero.DataDirectory", function () {
	var tmpDir, oldDir, newDir, dbFilename, oldDBFile, newDBFile, oldStorageDir, newStorageDir,
		oldTranslatorsDir, newTranslatorsDir, translatorName1, translatorName2,
		oldStorageDir1, newStorageDir1, storageFile1, oldStorageDir2, newStorageDir2, storageFile2,
		str1, str2, str3, str4, str5, str6,
		oldMigrationMarker, newMigrationMarker,
		stubs = {};
	
	before(function* () {
		tmpDir = yield getTempDirectory();
		oldDir = OS.Path.join(tmpDir, "old");
		newDir = OS.Path.join(tmpDir, "new");
		dbFilename = Zotero.DataDirectory.getDatabaseFilename();
		oldDBFile = OS.Path.join(oldDir, dbFilename);
		newDBFile = OS.Path.join(newDir, dbFilename);
		oldStorageDir = OS.Path.join(oldDir, "storage");
		newStorageDir = OS.Path.join(newDir, "storage");
		oldTranslatorsDir = OS.Path.join(oldDir, "translators");
		newTranslatorsDir = OS.Path.join(newDir, "translators");
		translatorName1 = 'a.js';
		translatorName2 = 'b.js';
		oldStorageDir1 = OS.Path.join(oldStorageDir, 'AAAAAAAA');
		newStorageDir1 = OS.Path.join(newStorageDir, 'AAAAAAAA');
		storageFile1 = 'test.pdf';
		oldStorageDir2 = OS.Path.join(oldStorageDir, 'BBBBBBBB');
		newStorageDir2 = OS.Path.join(newStorageDir, 'BBBBBBBB');
		storageFile2 = 'test.html';
		str1 = '1';
		str2 = '2';
		str3 = '3';
		str4 = '4';
		str5 = '5';
		str6 = '6';
		oldMigrationMarker = OS.Path.join(oldDir, Zotero.DataDirectory.MIGRATION_MARKER);
		newMigrationMarker = OS.Path.join(newDir, Zotero.DataDirectory.MIGRATION_MARKER);
		
		stubs.canMigrate = sinon.stub(Zotero.DataDirectory, "canMigrate").returns(true);
		// A pipe always exists during tests, since Zotero is running
		stubs.pipeExists = sinon.stub(Zotero.IPC, "pipeExists").returns(Zotero.Promise.resolve(false));
		stubs.setDataDir = sinon.stub(Zotero.DataDirectory, "set");
		stubs.isNewDirOnDifferentDrive = sinon.stub(Zotero.DataDirectory, 'isNewDirOnDifferentDrive').resolves(true);
	});
	
	beforeEach(function* () {
		stubs.setDataDir.reset();
	});
	
	afterEach(function* () {
		yield removeDir(oldDir);
		yield removeDir(newDir);
		Zotero.DataDirectory._cache(false);
		yield Zotero.DataDirectory.init();
	});
	
	after(function* () {
		for (let key in stubs) {
			try {
				stubs[key].restore();
			} catch(e) {}
		}
	});
	
	// Force non-mv mode
	var disableCommandMode = function () {
		if (!stubs.canMoveDirectoryWithCommand) {
			stubs.canMoveDirectoryWithCommand = sinon.stub(Zotero.File, "canMoveDirectoryWithCommand")
				.returns(false);
		}
	};
	
	// Force non-OS.File.move() mode
	var disableFunctionMode = function () {
		if (!stubs.canMoveDirectoryWithFunction) {
			stubs.canMoveDirectoryWithFunction = sinon.stub(Zotero.File, "canMoveDirectoryWithFunction")
				.returns(false);
		}
	};
	
	var resetCommandMode = function () {
		if (stubs.canMoveDirectoryWithCommand) {
			stubs.canMoveDirectoryWithCommand.restore();
			stubs.canMoveDirectoryWithCommand = undefined;
		}
	};
	
	var resetFunctionMode = function () {
		if (stubs.canMoveDirectoryWithFunction) {
			stubs.canMoveDirectoryWithFunction.restore();
			stubs.canMoveDirectoryWithFunction = undefined;
		}
	};
	
	var populateDataDirectory = Zotero.Promise.coroutine(function* (dir, srcDir, automatic = false) {
		yield OS.File.makeDir(dir, { unixMode: 0o755 });
		let storageDir = OS.Path.join(dir, 'storage');
		let storageDir1 = OS.Path.join(storageDir, 'AAAAAAAA');
		let storageDir2 = OS.Path.join(storageDir, 'BBBBBBBB');
		let translatorsDir = OS.Path.join(dir, 'translators');
		let migrationMarker = OS.Path.join(dir, Zotero.DataDirectory.MIGRATION_MARKER);
		
		// Database
		yield Zotero.File.putContentsAsync(OS.Path.join(dir, dbFilename), str1);
		// Database backup
		yield Zotero.File.putContentsAsync(OS.Path.join(dir, dbFilename + '.bak'), str2);
		// 'storage' directory
		yield OS.File.makeDir(storageDir, { unixMode: 0o755 });
		// 'storage' folders
		yield OS.File.makeDir(storageDir1, { unixMode: 0o755 });
		yield Zotero.File.putContentsAsync(OS.Path.join(storageDir1, storageFile1), str2);
		yield OS.File.makeDir(storageDir2, { unixMode: 0o755 });
		yield Zotero.File.putContentsAsync(OS.Path.join(storageDir2, storageFile2), str3);
		// 'translators' and some translators
		yield OS.File.makeDir(translatorsDir, { unixMode: 0o755 });
		yield Zotero.File.putContentsAsync(OS.Path.join(translatorsDir, translatorName1), str4);
		yield Zotero.File.putContentsAsync(OS.Path.join(translatorsDir, translatorName2), str5);
		// Migration marker
		yield Zotero.File.putContentsAsync(
			migrationMarker,
			JSON.stringify({
				sourceDir: srcDir || dir,
				automatic
			})
		);
	});
	
	var checkMigration = Zotero.Promise.coroutine(function* (options = {}) {
		if (!options.skipOldDir) {
			assert.isFalse(yield OS.File.exists(oldDir));
		}
		yield assert.eventually.equal(Zotero.File.getContentsAsync(newDBFile), str1);
		yield assert.eventually.equal(Zotero.File.getContentsAsync(newDBFile + '.bak'), str2);
		if (!options.skipStorageFile1) {
			yield assert.eventually.equal(
				Zotero.File.getContentsAsync(OS.Path.join(newStorageDir1, storageFile1)), str2
			);
		}
		yield assert.eventually.equal(
			Zotero.File.getContentsAsync(OS.Path.join(newStorageDir2, storageFile2)), str3
		);
		yield assert.eventually.equal(
			Zotero.File.getContentsAsync(OS.Path.join(newTranslatorsDir, translatorName1)), str4
		);
		yield assert.eventually.equal(
			Zotero.File.getContentsAsync(OS.Path.join(newTranslatorsDir, translatorName2)), str5
		);
		if (!options.skipNewMarker) {
			assert.isFalse(yield OS.File.exists(newMigrationMarker));
		}
		
		if (!options.skipSetDataDirectory) {
			assert.ok(stubs.setDataDir.calledOnce);
			assert.ok(stubs.setDataDir.calledWith(newDir));
		}
	});
	
	
	describe("#checkForMigration()", function () {
		let fileMoveStub;
		
		beforeEach(function () {
			disableCommandMode();
			disableFunctionMode();
		});
		
		after(function () {
			resetCommandMode();
			resetFunctionMode();
		});
		
		var tests = [];
		function add(desc, fn) {
			tests.push([desc, fn]);
		}
		
		it("should skip automatic migration if target directory exists and is non-empty", function* () {
			resetCommandMode();
			resetFunctionMode();
			
			yield populateDataDirectory(oldDir);
			yield OS.File.remove(oldMigrationMarker);
			yield OS.File.makeDir(newDir, { unixMode: 0o755 });
			yield Zotero.File.putContentsAsync(OS.Path.join(newDir, 'a'), '');
			
			yield assert.eventually.isFalse(Zotero.DataDirectory.checkForMigration(oldDir, newDir));
		});
		
		it("should skip automatic migration and show prompt if target directory is on a different drive", function* () {
			resetCommandMode();
			resetFunctionMode();
			
			yield populateDataDirectory(oldDir);
			yield OS.File.remove(oldMigrationMarker);
			
			stubs.isNewDirOnDifferentDrive.resolves(true);
			
			var promise = waitForDialog(function (dialog) {
				assert.include(
					dialog.document.documentElement.textContent,
					Zotero.getString(`dataDir.migration.failure.full.automatic.newDirOnDifferentDrive`, Zotero.clientName)
				);
			}, 'cancel');
			
			yield assert.eventually.isNotOk(Zotero.DataDirectory.checkForMigration(oldDir, newDir));
			yield promise;

			stubs.isNewDirOnDifferentDrive.resolves(false);
		});
		
		add("should show error on partial failure", function (automatic) {
			return function* () {
				yield populateDataDirectory(oldDir, null, automatic);
				
				let origFunc = OS.File.move;
				let fileMoveStub = sinon.stub(OS.File, "move").callsFake(function () {
					if (OS.Path.basename(arguments[0]) == storageFile1) {
						return Zotero.Promise.reject(new Error("Error"));
					}
					else {
						return origFunc(...arguments);
					}
				});
				let stub1 = sinon.stub(Zotero.File, "reveal").returns(Zotero.Promise.resolve());
				let stub2 = sinon.stub(Zotero.Utilities.Internal, "quitZotero");
				
				var promise2;
				// Click "Try Again" the first time, and then "Show Directories and Quit Zotero"
				var promise = waitForDialog(function (dialog) {
					promise2 = waitForDialog(null, 'extra1');
					
					// Make sure we're displaying the right message for this mode (automatic or manual)
					Components.utils.import("resource://zotero/config.js");
					assert.include(
						dialog.document.documentElement.textContent,
						Zotero.getString(
							`dataDir.migration.failure.partial.${automatic ? 'automatic' : 'manual'}.text`,
							[ZOTERO_CONFIG.CLIENT_NAME, Zotero.appName]
						)
					);
				});
				yield Zotero.DataDirectory.checkForMigration(oldDir, newDir);
				yield promise;
				yield promise2;
				
				assert.isTrue(stub1.calledTwice);
				assert.isTrue(stub1.getCall(0).calledWith(oldStorageDir));
				assert.isTrue(stub1.getCall(1).calledWith(newDBFile));
				assert.isTrue(stub2.called);
				
				fileMoveStub.restore();
				stub1.restore();
				stub2.restore();
			};
		});
		
		add("should show error on full failure", function (automatic) {
			return function* () {
				yield populateDataDirectory(oldDir, null, automatic);
				
				let origFunc = OS.File.move;
				let stub1 = sinon.stub(OS.File, "move").callsFake(function () {
					if (OS.Path.basename(arguments[0]) == dbFilename) {
						return Zotero.Promise.reject(new Error("Error"));
					}
					else {
						return origFunc(...arguments);
					}
				});
				let stub2 = sinon.stub(Zotero.File, "reveal").returns(Zotero.Promise.resolve());
				let stub3 = sinon.stub(Zotero.Utilities.Internal, "quitZotero");
				
				var promise = waitForDialog(function (dialog) {
					// Make sure we're displaying the right message for this mode (automatic or manual)
					Components.utils.import("resource://zotero/config.js");
					assert.include(
						dialog.document.documentElement.textContent,
						Zotero.getString(
							`dataDir.migration.failure.full.${automatic ? 'automatic' : 'manual'}.text1`,
							ZOTERO_CONFIG.CLIENT_NAME
						)
					);
				});
				yield Zotero.DataDirectory.checkForMigration(oldDir, newDir);
				yield promise;
				
				assert.isTrue(stub2.calledOnce);
				assert.isTrue(stub2.calledWith(oldDir));
				assert.isTrue(stub3.called);
				
				stub1.restore();
				stub2.restore();
				stub3.restore();
			};
		});
		
		describe("automatic mode", function () {
			tests.forEach(arr => {
				it(arr[0], arr[1](true));
			});
		});
		
		describe("manual mode", function () {
			tests.forEach(arr => {
				it(arr[0], arr[1](false));
			});
		});
		
		it("should remove marker if old directory doesn't exist", function* () {
			yield populateDataDirectory(newDir, oldDir);
			yield Zotero.DataDirectory.checkForMigration(newDir, newDir);
			yield checkMigration({
				skipSetDataDirectory: true
			});
		});
	});
	
	
	describe("#migrate()", function () {
		// Define tests and store for running in non-mv mode
		var tests = [];
		function add(desc, fn) {
			it(desc, fn);
			tests.push([desc, fn]);
		}
		
		add("should move all files and folders", function* () {
			yield populateDataDirectory(oldDir);
			yield Zotero.DataDirectory.migrate(oldDir, newDir);
			yield checkMigration();
		});
		
		add("should resume partial migration with just marker copied", function* () {
			yield populateDataDirectory(oldDir);
			yield OS.File.makeDir(newDir, { unixMode: 0o755 });
			
			yield OS.File.copy(oldMigrationMarker, newMigrationMarker);
			
			yield Zotero.DataDirectory.migrate(oldDir, newDir, true);
			yield checkMigration();
		});
		
		add("should resume partial migration with database moved", function* () {
			yield populateDataDirectory(oldDir);
			yield OS.File.makeDir(newDir, { unixMode: 0o755 });
			
			yield OS.File.copy(oldMigrationMarker, newMigrationMarker);
			yield OS.File.move(OS.Path.join(oldDir, dbFilename), OS.Path.join(newDir, dbFilename));
			
			yield Zotero.DataDirectory.migrate(oldDir, newDir, true);
			yield checkMigration();
		});
		
		add("should resume partial migration with some storage directories moved", function* () {
			yield populateDataDirectory(oldDir);
			yield populateDataDirectory(newDir, oldDir);
			
			// Moved: DB, DB backup, one storage dir
			// Not moved: one storage dir, translators dir
			yield OS.File.remove(oldDBFile);
			yield OS.File.remove(oldDBFile + '.bak');
			yield removeDir(oldStorageDir1);
			yield removeDir(newTranslatorsDir);
			yield removeDir(newStorageDir2);
			
			yield Zotero.DataDirectory.migrate(oldDir, newDir, true);
			yield checkMigration();
		});
		
		// Run all tests again without using mv
		//
		// On Windows these will just be duplicates of the above tests.
		describe("non-mv mode", function () {
			tests.forEach(arr => {
				it(arr[0] + " [non-mv]", arr[1]);
			});
			
			before(function () {
				disableCommandMode();
				disableFunctionMode();
			});
			
			after(function () {
				resetCommandMode();
				resetFunctionMode();
			});
			
			it("should handle partial failure", function* () {
				yield populateDataDirectory(oldDir);
				
				let origFunc = OS.File.move;
				let stub1 = sinon.stub(OS.File, "move").callsFake(function () {
					if (OS.Path.basename(arguments[0]) == storageFile1) {
						return Zotero.Promise.reject(new Error("Error"));
					}
					else {
						return origFunc(...arguments);
					}
				});
				
				yield Zotero.DataDirectory.migrate(oldDir, newDir);
				
				stub1.restore();
				
				yield checkMigration({
					skipOldDir: true,
					skipStorageFile1: true,
					skipNewMarker: true
				});
				
				assert.isTrue(yield OS.File.exists(OS.Path.join(oldStorageDir1, storageFile1)));
				assert.isFalse(yield OS.File.exists(OS.Path.join(oldStorageDir2, storageFile2)));
				assert.isFalse(yield OS.File.exists(oldTranslatorsDir));
				assert.isTrue(yield OS.File.exists(newMigrationMarker));
			});
		});
	});
});