Components.utils.import("resource://gre/modules/Services.jsm");
Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
Components.utils.import("resource://gre/modules/osfile.jsm");
var EventUtils = Components.utils.import("resource://zotero-unit/EventUtils.jsm");

var ZoteroUnit = Components.classes["@mozilla.org/commandlinehandler/general-startup;1?type=zotero-unit"].
                 getService(Components.interfaces.nsISupports).
                 wrappedJSObject;

var dump = ZoteroUnit.dump;

// Mocha HTML reporter doesn't show deepEqual diffs, so we change this.
chai.config.truncateThreshold = 0

function quit(failed) {
	// Quit with exit status
	if(!failed) {
		OS.File.writeAtomic(OS.Path.join(OS.Constants.Path.profileDir, "success"), new Uint8Array(0));
	}
	if(!ZoteroUnit.noquit) {
		setTimeout(function () {
			Components.classes['@mozilla.org/toolkit/app-startup;1']
				.getService(Components.interfaces.nsIAppStartup)
				.quit(Components.interfaces.nsIAppStartup.eForceQuit);
		}, 250);
	}
}

if (ZoteroUnit.makeTestData) {
	let dataPath = getTestDataDirectory().path;
	
	Zotero.Prefs.set("export.citePaperJournalArticleURL", true);
	
	let dataFiles = [
		{
			name: 'allTypesAndFields',
			func: generateAllTypesAndFieldsData
		},
		{
			name: 'itemJSON',
			func: generateItemJSONData,
			args: [null]
		},
		// {
		// 	name: 'citeProcJSExport',
		// 	func: generateCiteProcJSExportData
		// },
		{
			name: 'translatorExportLegacy',
			func: generateTranslatorExportData,
			args: [true]
		},
		{
			name: 'translatorExport',
			func: generateTranslatorExportData,
			args: [false]
		}
	];
	Zotero.Promise.coroutine(function* () {
		yield Zotero.initializationPromise;
		for (let i=0; i<dataFiles.length; i++) {
			let first = !i;
			let params = dataFiles[i];

			// Make sure to not run next loop if previous fails
			if (!first) dump('\n');
			dump('Generating data for ' + params.name + '...');

			let filePath = OS.Path.join(dataPath, params.name + '.js');
			let exists = yield OS.File.exists(filePath);
			let currentData;
			if (exists) {
				currentData = loadSampleData(params.name);
			}

			let args = params.args || [];
			args.push(currentData);
			let newData = params.func.apply(null, args);
			if (newData instanceof Zotero.Promise) {
				newData = yield newData;
			}
			let str = stableStringify(newData);

			yield OS.File.writeAtomic(OS.Path.join(dataPath, params.name + '.js'), str);
			dump("done.");
		}
	})()
	.catch(function(e) { dump('\n'); dump(Zotero.Utilities.varDump(e)) })
	.finally(function() { quit(false) });
}

function Reporter(runner) {
	var indents = 0, passed = 0, failed = 0, aborted = false;

	function indent() {
		return Array(indents).join('  ');
	}

	runner.on('start', function(){});

	runner.on('suite', function(suite){
		++indents;
		dump("\r"+indent()+suite.title+"\n");
	});

	runner.on('suite end', function(suite){
		--indents;
		if (1 == indents) dump("\n");
	});

	runner.on('pending', function(test){
		dump("\r"+indent()+"pending  -"+test.title+"\n");
	});

	runner.on('pass', function(test){
		passed++;
		var msg = "\r"+indent()+Mocha.reporters.Base.symbols.ok+" "+test.title;
		if ('fast' != test.speed) {
			msg += " ("+Math.round(test.duration)+" ms)";
		}
		dump(msg+"\n");
	});

	runner.on('fail', function(test, err){
		// Remove internal code references
		err.stack = err.stack.replace(/.+(?:zotero-unit\/|\/Task\.jsm|\/bluebird\.js).+\n?/g, "");
		
		// Strip "From previous event:" block if it's all internals
		if (err.stack.indexOf('From previous event:') != -1) {
			err.stack = err.stack
				// Drop first line, because it contains the error message
				.replace(/^.+\n/, '')
				// Drop "From previous event:" labels for empty blocks
				.replace(/.*From previous event:.*(?:\n(?=\s*From previous event:)|\s*$)/g, '');
		}
		
		// Make sure there's a blank line after all stack traces
		err.stack = err.stack.replace(/\s*$/, '\n\n');
		
		failed++;
		let indentStr = indent();
		dump("\r" + indentStr
			// Dark red X for errors
			+ "\x1B[31;40m" + Mocha.reporters.Base.symbols.err + " [FAIL]\x1B[0m"
			// Trigger bell if interactive
			+ (Zotero.automatedTest ? "" : "\x07")
			+ " " + test.title + "\n"
			+ indentStr + "  " + err.toString() + " at\n"
			+ err.stack.replace(/^/gm, indentStr + "    "));
		
		if (ZoteroUnit.bail) {
			aborted = true;
			runner.abort();
		}
	});

	runner.on('end', function() {
		dump(passed + "/" + (passed + failed) + " tests passed"
			+ (aborted ? " -- aborting" : "") + "\n");
		quit(failed != 0);
	});
}

// Monkey-patch Mocha to check instanceof Error using compartment-local
// Error object
Mocha.Runner.prototype.fail = function(test, err){
	++this.failures;
	test.state = 'failed';

	if ('string' == typeof err) {
		err = new Error('the string "' + err + '" was thrown, throw an Error :)');
	} else if (!(err instanceof Components.utils.getGlobalForObject(err).Error)) {
		err = new Error('the ' + Mocha.utils.type(err) + ' ' + Mocha.utils.stringify(err) + ' was thrown, throw an Error :)');
	}

	this.emit('fail', test, err);
};

// Setup Mocha
mocha.setup({
	ui: "bdd",
	reporter: Reporter,
	timeout: ZoteroUnit.timeout || 10000,
	grep: ZoteroUnit.grep
});

coMocha(Mocha);

before(function () {
	// Store all prefs set in runtests.sh
	Components.utils.import("resource://zotero/config.js");
	var prefBranch = Services.prefs.getBranch(ZOTERO_CONFIG.PREF_BRANCH);
	ZoteroUnit.customPrefs = {};
	prefBranch.getChildList("", {})
		.filter(key => prefBranch.prefHasUserValue(key))
		.forEach(key => ZoteroUnit.customPrefs[key] = Zotero.Prefs.get(key));
});

/**
 * Clear all prefs, and reset those set in runtests.sh to original values
 */
function resetPrefs() {
	Components.utils.import("resource://zotero/config.js");
	var prefBranch = Services.prefs.getBranch(ZOTERO_CONFIG.PREF_BRANCH);
	prefBranch.getChildList("", {}).forEach(key => {
		var origVal = ZoteroUnit.customPrefs[key];
		if (origVal !== undefined) {
			if (origVal != Zotero.Prefs.get(key)) {
				Zotero.Prefs.set(key, ZoteroUnit.customPrefs[key]);
			}
		}
		else if (prefBranch.prefHasUserValue(key)) {
			Zotero.Prefs.clear(key)
		}
	});
}

afterEach(function () {
	resetPrefs();
});


var assert = chai.assert,
    expect = chai.expect;

// Set up tests to run
var run = ZoteroUnit.runTests;
if(run && ZoteroUnit.tests) {
	function getTestFilename(test) {
		// Allow foo, fooTest, fooTest.js, and tests/fooTest.js
		test = test.replace(/\.js$/, "");
		test = test.replace(/Test$/, "");
		test = test.replace(/^tests[/\\]/, "");
		return test + "Test.js";
	}
	
	var testDirectory = getTestDataDirectory().parent,
	    testFiles = [];
	if(ZoteroUnit.tests == "all") {
		var enumerator = testDirectory.directoryEntries;
		let startFile = ZoteroUnit.startAt ? getTestFilename(ZoteroUnit.startAt) : false;
		let started = !startFile;
		let stopFile = ZoteroUnit.stopAt ? getTestFilename(ZoteroUnit.stopAt) : false;
		while(enumerator.hasMoreElements()) {
			var file = enumerator.getNext().QueryInterface(Components.interfaces.nsIFile);
			if(file.leafName.endsWith(".js")) {
				if (started || file.leafName == startFile) {
					testFiles.push(file.leafName);
					started = true;
				}
				if (file.leafName == stopFile) {
					break;
				}
			}
		}
		if (!started) {
			dump(`Invalid start file ${startFile}\n`);
		}
		testFiles.sort();
	} else {
		var specifiedTests = ZoteroUnit.tests.split(",");
		for (let test of specifiedTests) {
			let fname = getTestFilename(test);
			let file = testDirectory.clone();
			file.append(fname);
			if (!file.exists()) {
				dump("Invalid test file "+test+"\n");
				run = false;
				quit(true);
			}
			testFiles.push(fname);
		}
	}

	for(var fname of testFiles) {
		var el = document.createElement("script");
		el.type = "application/javascript;version=1.8";
		el.src = "resource://zotero-unit-tests/"+fname;
		el.async = false;
		document.body.appendChild(el);
	}
}

if(run) {
	window.onload = function() {
		Zotero.spawn(function* () {
			yield Zotero.Schema.schemaUpdatePromise;
			
			// Download and cache PDF tools for this platform
			//
			// To reset, delete test/tests/data/pdf/ directory
			var cachePDFTools = Zotero.Promise.coroutine(function* () {
				var path = OS.Path.join(getTestDataDirectory().path, 'pdf');
				yield OS.File.makeDir(path, { ignoreExisting: true });
				
				var baseURL = Zotero.Fulltext.pdfToolsDownloadBaseURL;
				// Point full-text code to the cache directory, so downloads come from there
				Zotero.Fulltext.pdfToolsDownloadBaseURL = OS.Path.toFileURI(path) + "/";
				
				// Get latest tools version for the current platform
				yield Zotero.File.download(baseURL + 'latest.json', OS.Path.join(path, 'latest.json'));
				
				var platform = Zotero.platform.replace(/\s/g, '-');
				var version = yield Zotero.Fulltext.getLatestPDFToolsVersion();
				
				// Create version directory (e.g., data/pdf/3.04) and download tools to it if
				// they don't exist
				yield OS.File.makeDir(OS.Path.join(path, version), { ignoreExisting: true });
				
				var fileName = "pdfinfo-" + platform + (Zotero.isWin ? ".exe" : "");
				var execPath = OS.Path.join(path, version, fileName);
				if (!(yield OS.File.exists(execPath))) {
					yield Zotero.File.download(baseURL + version + "/" + fileName, execPath);
				}
				fileName = "pdftotext-" + platform + (Zotero.isWin ? ".exe" : "");;
				execPath = OS.Path.join(path, version, fileName);
				if (!(yield OS.File.exists(execPath))) {
					yield Zotero.File.download(baseURL + version + "/" + fileName, execPath);
				}
			});
			
			try {
				yield cachePDFTools();
			}
			catch (e) {
				Zotero.logError(e);
			}
			
			return mocha.run();
		})
	};
}