const path = require("path");
const chai = require("chai");
const { expect } = chai;
const PizZip = require("pizzip");
const fs = require("fs");
const { get, unset, omit, uniq } = require("lodash");
const errorLogger = require("../error-logger");
const diff = require("diff");
const AssertionModule = require("./assertion-module.js");

const Docxtemplater = require("../docxtemplater.js");
const { first } = require("../utils.js");
const xmlPrettify = require("./xml-prettify");
let countFiles = 1;
let allStarted = false;
let examplesDirectory;
const documentCache = {};
const imageData = {};
const emptyNamespace = /xmlns:[a-z0-9]+=""/;

function unifiedDiff(actual, expected) {
	const indent = "      ";
	function cleanUp(line) {
		const firstChar = first(line);
		if (firstChar === "+") {
			return indent + line;
		}
		if (firstChar === "-") {
			return indent + line;
		}
		if (line.match(/@@/)) {
			return "--";
		}
		if (line.match(/\\ No newline/)) {
			return null;
		}
		return indent + line;
	}
	function notBlank(line) {
		return typeof line !== "undefined" && line !== null;
	}
	const msg = diff.createPatch("string", actual, expected);
	const lines = msg.split("\n").splice(5);
	return (
		"\n      " +
		"+ expected" +
		" " +
		"- actual" +
		"\n\n" +
		lines.map(cleanUp).filter(notBlank).join("\n")
	);
}

function isNode12() {
	return process && process.version && process.version.indexOf("v12") === 0;
}

function walk(dir) {
	let results = [];
	const list = fs.readdirSync(dir);
	list.forEach(function (file) {
		if (file.indexOf(".") === 0) {
			return;
		}
		file = dir + "/" + file;
		const stat = fs.statSync(file);
		if (stat && stat.isDirectory()) {
			results = results.concat(walk(file));
		} else {
			results.push(file);
		}
	});
	return results;
}

function createXmlTemplaterDocxNoRender(content, options = {}) {
	const doc = makeDocx("temporary.docx", content);
	doc.setOptions(options);
	doc.setData(options.tags);
	return doc;
}

function createXmlTemplaterDocx(content, options = {}) {
	const doc = makeDocx("temporary.docx", content);
	doc.setOptions(options);
	doc.setData(options.tags);
	doc.render();
	return doc;
}

function writeFile(expectedName, zip) {
	const writeFile = path.resolve(examplesDirectory, "..", expectedName);
	if (fs.writeFileSync) {
		fs.writeFileSync(
			writeFile,
			zip.generate({ type: "nodebuffer", compression: "DEFLATE" })
		);
	}
	if (typeof window !== "undefined" && window.saveAs) {
		const out = zip.generate({
			type: "blob",
			mimeType:
				"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
			compression: "DEFLATE",
		});
		saveAs(out, expectedName); // comment to see the error
	}
}
function unlinkFile(expectedName) {
	const writeFile = path.resolve(examplesDirectory, "..", expectedName);
	if (fs.unlinkSync) {
		try {
			fs.unlinkSync(writeFile);
		} catch (e) {
			if (e.code !== "ENOENT") {
				throw e;
			}
		}
	}
}

/* eslint-disable no-console */
function shouldBeSame(options) {
	const zip = options.doc.getZip();
	const { expectedName } = options;
	let expectedZip;

	try {
		expectedZip = documentCache[expectedName].zip;
	} catch (e) {
		writeFile(expectedName, zip);
		console.log(
			JSON.stringify({ msg: "Expected file does not exists", expectedName })
		);
		throw e;
	}

	try {
		uniq(Object.keys(zip.files).concat(Object.keys(expectedZip.files))).map(
			function (filePath) {
				const suffix = `for "${filePath}"`;
				expect(expectedZip.files[filePath]).to.be.an(
					"object",
					`The file ${filePath} doesn't exist on ${expectedName}`
				);
				expect(zip.files[filePath]).to.be.an(
					"object",
					`The file ${filePath} doesn't exist on generated file`
				);
				expect(zip.files[filePath].name).to.be.equal(
					expectedZip.files[filePath].name,
					`Name differs ${suffix}`
				);
				expect(zip.files[filePath].options.dir).to.be.equal(
					expectedZip.files[filePath].options.dir,
					`IsDir differs ${suffix}`
				);
				const text1 = zip.files[filePath].asText().replace(/\n|\t/g, "");
				const text2 = expectedZip.files[filePath]
					.asText()
					.replace(/\n|\t/g, "");
				if (endsWith(filePath, "/")) {
					return;
				}
				if (filePath.indexOf(".png") !== -1) {
					expect(text1.length).to.be.equal(
						text2.length,
						`Content differs ${suffix}`
					);
					expect(text1).to.be.equal(text2, `Content differs ${suffix}`);
				} else {
					expect(text1).to.not.match(
						emptyNamespace,
						`The file ${filePath} has empty namespaces`
					);
					expect(text2).to.not.match(
						emptyNamespace,
						`The file ${filePath} has empty namespaces`
					);
					if (text1 === text2) {
						return;
					}
					const pText1 = xmlPrettify(text1, options);
					const pText2 = xmlPrettify(text2, options);

					if (pText1 !== pText2) {
						const pd = unifiedDiff(pText1, pText2);
						expect(pText1).to.be.equal(
							pText2,
							"Content differs \n" + suffix + "\n" + pd
						);
					}
				}
			}
		);
	} catch (e) {
		writeFile(expectedName, zip);
		console.log(
			JSON.stringify({
				msg: "Expected file differs from actual file",
				expectedName,
			})
		);
		throw e;
	}
	unlinkFile(expectedName);
}
/* eslint-enable no-console */

function checkLength(e, expectedError, propertyPath) {
	const propertyPathLength = propertyPath + "Length";
	const property = get(e, propertyPath);
	const expectedPropertyLength = get(expectedError, propertyPathLength);
	if (property && expectedPropertyLength) {
		expect(expectedPropertyLength).to.be.a(
			"number",
			JSON.stringify(expectedError.properties)
		);
		expect(expectedPropertyLength).to.equal(property.length);
		unset(e, propertyPath);
		unset(expectedError, propertyPathLength);
	}
}

function cleanRecursive(arr) {
	arr.forEach(function (p) {
		delete p.lIndex;
		delete p.endLindex;
		delete p.offset;
		delete p.raw;
		if (p.subparsed) {
			cleanRecursive(p.subparsed);
		}
		if (p.value && p.value.forEach) {
			p.value.forEach(cleanRecursive);
		}
		if (p.expanded) {
			p.expanded.forEach(cleanRecursive);
		}
	});
}

function cleanError(e, expectedError) {
	const message = e.message;
	e = omit(e, ["line", "sourceURL", "stack"]);
	e.message = message;
	if (expectedError.properties && e.properties) {
		if (expectedError.properties.explanation != null) {
			const e1 = e.properties.explanation;
			const e2 = expectedError.properties.explanation;
			expect(e1).to.be.deep.equal(
				e2,
				`Explanations differ '${e1}' != '${e2}': for ${JSON.stringify(
					expectedError
				)}`
			);
		}
		delete e.properties.explanation;
		delete expectedError.properties.explanation;
		if (e.properties.postparsed) {
			e.properties.postparsed.forEach(function (p) {
				delete p.lIndex;
				delete p.endLindex;
				delete p.offset;
			});
		}
		if (e.properties.rootError) {
			expect(
				e.properties.rootError,
				JSON.stringify(e.properties)
			).to.be.instanceOf(Error);
			expect(
				expectedError.properties.rootError,
				JSON.stringify(expectedError.properties)
			).to.be.instanceOf(Object, "expectedError doesn't have a rootError");
			if (expectedError) {
				expect(e.properties.rootError.message).to.equal(
					expectedError.properties.rootError.message,
					"rootError.message"
				);
			}
			delete e.properties.rootError;
			delete expectedError.properties.rootError;
		}
		if (expectedError.properties.offset != null) {
			const o1 = e.properties.offset;
			const o2 = expectedError.properties.offset;
			// offset can be arrays, so deep compare
			expect(o1).to.be.deep.equal(
				o2,
				`Offset differ ${o1} != ${o2}: for ${JSON.stringify(expectedError)}`
			);
		}
		delete expectedError.properties.offset;
		delete e.properties.offset;
		checkLength(e, expectedError, "properties.paragraphParts");
		checkLength(e, expectedError, "properties.postparsed");
		checkLength(e, expectedError, "properties.parsed");
	}
	if (e.stack && expectedError) {
		expect(e.stack).to.contain("Error: " + expectedError.message);
	}
	delete e.stack;
	return e;
}

function wrapMultiError(error) {
	const type = Object.prototype.toString.call(error);
	let errors;
	if (type === "[object Array]") {
		errors = error;
	} else {
		errors = [error];
	}

	return {
		name: "TemplateError",
		message: "Multi error",
		properties: {
			id: "multi_error",
			errors,
		},
	};
}

function jsonifyError(e) {
	return JSON.parse(
		JSON.stringify(e, function (key, value) {
			if (value instanceof Promise) {
				return {};
			}
			return value;
		})
	);
}

function errorVerifier(e, type, expectedError) {
	expect(e, "No error has been thrown").not.to.be.equal(null);
	const toShowOnFail = e.stack;
	expect(e, toShowOnFail).to.be.instanceOf(Error);
	expect(e, toShowOnFail).to.be.instanceOf(type);
	expect(e, toShowOnFail).to.be.an("object");
	expect(e, toShowOnFail).to.have.property("properties");
	expect(e.properties, toShowOnFail).to.be.an("object");
	if (type.name && type.name !== "XTInternalError") {
		expect(e.properties, toShowOnFail).to.have.property("explanation");
		expect(e.properties.explanation, toShowOnFail).to.be.a("string");
		expect(e.properties.explanation, toShowOnFail).to.be.a("string");
	}
	expect(e.properties, toShowOnFail).to.have.property("id");
	expect(e.properties.id, toShowOnFail).to.be.a("string");
	e = cleanError(e, expectedError);
	if (e.properties.errors) {
		const msg =
			"expected : \n" +
			JSON.stringify(expectedError.properties.errors) +
			"\nactual : \n" +
			JSON.stringify(e.properties.errors);
		expect(expectedError.properties.errors).to.be.an("array", msg);
		const l1 = e.properties.errors.length;
		const l2 = expectedError.properties.errors.length;
		expect(l1).to.equal(
			l2,
			`Expected to have the same amount of e.properties.errors ${l1} !== ${l2} ` +
				msg
		);
		e.properties.errors = e.properties.errors.map(function (suberror, i) {
			const cleaned = cleanError(suberror, expectedError.properties.errors[i]);
			const jsonified = jsonifyError(cleaned);
			return jsonified;
		});
	}

	const realError = jsonifyError(e);
	expect(realError).to.be.deep.equal(expectedError);
}

function expectToThrowAsync(fn, type, expectedError) {
	return Promise.resolve(null)
		.then(function () {
			const r = fn();
			return r.then(function () {
				return null;
			});
		})
		.catch(function (error) {
			return error;
		})
		.then(function (e) {
			return errorVerifier(e, type, expectedError);
		});
}

function expectToThrow(fn, type, expectedError) {
	let err = null;
	try {
		fn();
	} catch (e) {
		err = e;
	}
	errorVerifier(err, type, expectedError);
	return err;
}

function load(name, content, obj) {
	const zip = new PizZip(content);
	obj[name] = new Docxtemplater();
	obj[name].loadZip(zip);
	obj[name].loadedName = name;
	obj[name].loadedContent = content;
	return obj[name];
}
function loadDocument(name, content) {
	return load(name, content, documentCache);
}

function cacheDocument(name, content) {
	const zip = new PizZip(content);
	documentCache[name] = { loadedName: name, loadedContent: content, zip };
	return documentCache[name];
}
function loadImage(name, content) {
	imageData[name] = content;
}

function loadFile(name, callback) {
	if (fs.readFileSync) {
		const path = require("path");
		const buffer = fs.readFileSync(
			path.join(examplesDirectory, name),
			"binary"
		);
		return callback(null, name, buffer);
	}
	return PizZipUtils.getBinaryContent("../examples/" + name, function (
		err,
		data
	) {
		if (err) {
			return callback(err);
		}
		return callback(null, name, data);
	});
}

function unhandledRejectionHandler(reason) {
	throw reason;
}

let startFunction;
function setStartFunction(sf) {
	allStarted = false;
	countFiles = 1;
	startFunction = sf;

	if (typeof window !== "undefined" && window.addEventListener) {
		window.addEventListener("unhandledrejection", unhandledRejectionHandler);
	} else {
		process.on("unhandledRejection", unhandledRejectionHandler);
	}
}

function endLoadFile(change) {
	change = change || 0;
	countFiles += change;
	if (countFiles === 0 && allStarted === true) {
		const result = startFunction();
		if (typeof window !== "undefined") {
			return window.mocha.run(() => {
				const elemDiv = window.document.getElementById("status");
				elemDiv.textContent = "FINISHED";
				document.body.appendChild(elemDiv);
			});
		}
		return result;
	}
}

function endsWith(str, suffix) {
	return str.indexOf(suffix, str.length - suffix.length) !== -1;
}
function endsWithOne(str, suffixes) {
	return suffixes.some(function (suffix) {
		return endsWith(str, suffix);
	});
}
function startsWith(str, suffix) {
	return str.indexOf(suffix) === 0;
}

/* eslint-disable no-console */
function start() {
	afterEach(function () {
		if (
			this.currentTest.state === "failed" &&
			this.currentTest.err.properties
		) {
			errorLogger(this.currentTest.err);
		}
	});
	/* eslint-disable import/no-unresolved */
	const fileNames = require("./filenames.js");
	/* eslint-enable import/no-unresolved */
	fileNames.forEach(function (fullFileName) {
		const fileName = fullFileName.replace(examplesDirectory + "/", "");
		let callback;
		if (startsWith(fileName, ".") || startsWith(fileName, "~")) {
			return;
		}
		if (
			endsWithOne(fileName, [
				".dotx",
				".dotm",
				".docx",
				".docm",
				".pptm",
				".pptx",
				".xlsx",
			])
		) {
			callback = cacheDocument;
		}
		if (!callback) {
			callback = loadImage;
		}
		countFiles++;
		loadFile(fileName, (e, name, buffer) => {
			if (e) {
				console.log(e);
				throw e;
			}
			endLoadFile(-1);
			callback(name, buffer);
		});
	});
	allStarted = true;
	endLoadFile(-1);
}
/* eslint-disable no-console */

function setExamplesDirectory(ed) {
	examplesDirectory = ed;
	if (fs && fs.writeFileSync) {
		const fileNames = walk(examplesDirectory).map(function (f) {
			return f.replace(examplesDirectory + "/", "");
		});
		fs.writeFileSync(
			path.resolve(__dirname, "filenames.js"),
			"module.exports=" + JSON.stringify(fileNames)
		);
	}
}

function removeSpaces(text) {
	return text.replace(/\n|\t/g, "");
}

const contentTypeContent = `<?xml version="1.0" encoding="utf-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
  <Default Extension="xml" ContentType="application/xml"/>
  <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
</Types>`;

function makeDocx(name, content) {
	const zip = new PizZip();
	zip.file("word/document.xml", content, { createFolders: true });
	zip.file("[Content_Types].xml", contentTypeContent);
	return load(name, zip.generate({ type: "string" }), documentCache);
}

function createDoc(name) {
	const doc = loadDocument(name, documentCache[name].loadedContent);
	/* eslint-disable-next-line no-process-env */
	if (!process.env.FAST) {
		doc.attachModule(new AssertionModule());
	}
	return doc;
}

function createDocV4(name, options) {
	const zip = getZip(name);
	/* eslint-disable-next-line no-process-env */
	if (!process.env.FAST) {
		options = options || {};
		if (!options.modules || options.modules instanceof Array) {
			options.modules = options.modules || [];
			options.modules.push(new AssertionModule());
		}
	}
	return new Docxtemplater(zip, options);
}

function getZip(name) {
	return new PizZip(documentCache[name].loadedContent);
}

function getLoadedContent(name) {
	return documentCache[name].loadedContent;
}

function getContent(doc) {
	return doc.getZip().files["word/document.xml"].asText();
}

function resolveSoon(data) {
	return new Promise(function (resolve) {
		setTimeout(function () {
			resolve(data);
		}, 1);
	});
}

function rejectSoon(data) {
	return new Promise(function (resolve, reject) {
		setTimeout(function () {
			reject(data);
		}, 1);
	});
}

function getParameterByName(name) {
	if (typeof window === "undefined") {
		return null;
	}
	const url = window.location.href;
	name = name.replace(/[\[\]]/g, "\\$&");
	const regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
		results = regex.exec(url);
	if (!results) {
		return null;
	}
	if (!results[2]) {
		return "";
	}
	return decodeURIComponent(results[2].replace(/\+/g, " "));
}

function browserMatches(regex) {
	const currentBrowser = getParameterByName("browser");
	if (currentBrowser === null) {
		return false;
	}
	return regex.test(currentBrowser);
}

module.exports = {
	chai,
	cleanError,
	cleanRecursive,
	createDoc,
	getLoadedContent,
	createXmlTemplaterDocx,
	createXmlTemplaterDocxNoRender,
	expect,
	expectToThrow,
	expectToThrowAsync,
	getContent,
	imageData,
	loadDocument,
	loadFile,
	loadImage,
	makeDocx,
	removeSpaces,
	setExamplesDirectory,
	setStartFunction,
	shouldBeSame,
	resolveSoon,
	rejectSoon,
	start,
	wrapMultiError,
	isNode12,
	createDocV4,
	getZip,
	getParameterByName,
	browserMatches,
};
