text: /*global Rule, Check, RuleResult, commons: true */
  /*eslint no-unused-vars: 0*/
  function getDefaultConfiguration(audit) {
  	'use strict';
  	var config;
  	if (audit) {
  		config = axe.utils.clone(audit);
  		// Commons are configured into axe like everything else,
  		// however because things go funky if we have multiple commons objects
  		// we're not using the copy of that.
  		config.commons = audit.commons;
  	} else {
  		config = {};
  	}
  
  	config.reporter = config.reporter || null;
  	config.rules = config.rules || [];
  	config.checks = config.checks || [];
  	config.data = { checks: {}, rules: {}, ...config.data };
  	return config;
  }
  
  function unpackToObject(collection, audit, method) {
  	'use strict';
  
  	var i, l;
  	for (i = 0, l = collection.length; i < l; i++) {
  		audit[method](collection[i]);
  	}
  }
  
  /**
   * Constructor which holds configured rules and information about the document under test
   */
  function Audit(audit) {
  	// defaults
  	this.brand = 'axe';
  	this.application = 'axeAPI';
  	this.tagExclude = ['experimental'];
  
  	this.defaultConfig = audit;
  	this._init();
  
  	// A copy of the "default" locale. This will be set if the user
  	// provides a new locale to `axe.configure()` and used to undo
  	// changes in `axe.reset()`.
  	this._defaultLocale = null;
  }
  
  /**
   * Build and set the previous locale. Will noop if a previous
   * locale was already set, as we want the ability to "reset"
   * to the default ("first") configuration.
   */
  
  Audit.prototype._setDefaultLocale = function() {
  	if (this._defaultLocale) {
  		return;
  	}
  
  	const locale = {
  		checks: {},
  		rules: {}
  	};
  
  	// XXX: unable to use `for-of` here, as doing so would
  	// require us to polyfill `Symbol`.
  	const checkIDs = Object.keys(this.data.checks);
  	for (let i = 0; i < checkIDs.length; i++) {
  		const id = checkIDs[i];
  		const check = this.data.checks[id];
  		const { pass, fail, incomplete } = check.messages;
  		locale.checks[id] = {
  			pass,
  			fail,
  			incomplete
  		};
  	}
  
  	const ruleIDs = Object.keys(this.data.rules);
  	for (let i = 0; i < ruleIDs.length; i++) {
  		const id = ruleIDs[i];
  		const rule = this.data.rules[id];
  		const { description, help } = rule;
  		locale.rules[id] = { description, help };
  	}
  
  	this._defaultLocale = locale;
  };
  
  /**
   * Reset the locale to the "default".
   */
  
  Audit.prototype._resetLocale = function() {
  	// If the default locale has not already been set, we can exit early.
  	const defaultLocale = this._defaultLocale;
  	if (!defaultLocale) {
  		return;
  	}
  
  	// Apply the default locale
  	this.applyLocale(defaultLocale);
  };
  
  /**
   * Merge two check locales (a, b), favoring `b`.
   *
   * Both locale `a` and the returned shape resemble:
   *
   *    {
   *      impact: string,
   *      messages: {
   *        pass: string | function,
   *        fail: string | function,
   *        incomplete: string | {
   *          [key: string]: string | function
   *        }
   *      }
   *    }
   *
   * Locale `b` follows the `axe.CheckLocale` shape and resembles:
   *
   *    {
   *      pass: string,
   *      fail: string,
   *      incomplete: string | { [key: string]: string }
   *    }
   */
  
  const mergeCheckLocale = (a, b) => {
  	let { pass, fail } = b;
  	// If the message(s) are Strings, they have not yet been run
  	// thru doT (which will return a Function).
  	if (typeof pass === 'string') {
  		pass = axe.imports.doT.compile(pass);
  	}
  	if (typeof fail === 'string') {
  		fail = axe.imports.doT.compile(fail);
  	}
  	return {
  		...a,
  		messages: {
  			pass: pass || a.messages.pass,
  			fail: fail || a.messages.fail,
  			incomplete:
  				typeof a.messages.incomplete === 'object'
  					? // TODO: for compleness-sake, we should be running
  					  // incomplete messages thru doT as well. This was
  					  // out-of-scope for runtime localization, but should
  					  // eventually be addressed.
  					  { ...a.messages.incomplete, ...b.incomplete }
  					: b.incomplete
  		}
  	};
  };
  
  /**
   * Merge two rule locales (a, b), favoring `b`.
   */
  
  const mergeRuleLocale = (a, b) => {
  	let { help, description } = b;
  	// If the message(s) are Strings, they have not yet been run
  	// thru doT (which will return a Function).
  	if (typeof help === 'string') {
  		help = axe.imports.doT.compile(help);
  	}
  	if (typeof description === 'string') {
  		description = axe.imports.doT.compile(description);
  	}
  	return {
  		...a,
  		help: help || a.help,
  		description: description || a.description
  	};
  };
  
  /**
   * Apply locale for the given `checks`.
   */
  
  Audit.prototype._applyCheckLocale = function(checks) {
  	const keys = Object.keys(checks);
  	for (let i = 0; i < keys.length; i++) {
  		const id = keys[i];
  		if (!this.data.checks[id]) {
  			throw new Error(`Locale provided for unknown check: "${id}"`);
  		}
  
  		this.data.checks[id] = mergeCheckLocale(this.data.checks[id], checks[id]);
  	}
  };
  
  /**
   * Apply locale for the given `rules`.
   */
  
  Audit.prototype._applyRuleLocale = function(rules) {
  	const keys = Object.keys(rules);
  	for (let i = 0; i < keys.length; i++) {
  		const id = keys[i];
  		if (!this.data.rules[id]) {
  			throw new Error(`Locale provided for unknown rule: "${id}"`);
  		}
  		this.data.rules[id] = mergeRuleLocale(this.data.rules[id], rules[id]);
  	}
  };
  
  /**
   * Apply the given `locale`.
   *
   * @param {axe.Locale}
   */
  
  Audit.prototype.applyLocale = function(locale) {
  	this._setDefaultLocale();
  
  	if (locale.checks) {
  		this._applyCheckLocale(locale.checks);
  	}
  
  	if (locale.rules) {
  		this._applyRuleLocale(locale.rules);
  	}
  };
  
  /**
   * Initializes the rules and checks
   */
  Audit.prototype._init = function() {
  	var audit = getDefaultConfiguration(this.defaultConfig);
  
  	axe.commons = commons = audit.commons;
  
  	this.reporter = audit.reporter;
  	this.commands = {};
  	this.rules = [];
  	this.checks = {};
  
  	unpackToObject(audit.rules, this, 'addRule');
  	unpackToObject(audit.checks, this, 'addCheck');
  
  	this.data = {};
  	this.data.checks = (audit.data && audit.data.checks) || {};
  	this.data.rules = (audit.data && audit.data.rules) || {};
  	this.data.failureSummaries =
  		(audit.data && audit.data.failureSummaries) || {};
  	this.data.incompleteFallbackMessage =
  		(audit.data && audit.data.incompleteFallbackMessage) || '';
  
  	this._constructHelpUrls(); // create default helpUrls
  };
  
  /**
   * Adds a new command to the audit
   */
  
  Audit.prototype.registerCommand = function(command) {
  	'use strict';
  	this.commands[command.id] = command.callback;
  };
  
  /**
   * Adds a new rule to the Audit.  If a rule with specified ID already exists, it will be overridden
   * @param {Object} spec Rule specification object
   */
  Audit.prototype.addRule = function(spec) {
  	'use strict';
  
  	if (spec.metadata) {
  		this.data.rules[spec.id] = spec.metadata;
  	}
  
  	let rule = this.getRule(spec.id);
  	if (rule) {
  		rule.configure(spec);
  	} else {
  		this.rules.push(new Rule(spec, this));
  	}
  };
  
  /**
   * Adds a new check to the Audit.  If a Check with specified ID already exists, it will be
   * reconfigured
   *
   * @param {Object} spec Check specification object
   */
  Audit.prototype.addCheck = function(spec) {
  	/*eslint no-eval: 0 */
  	'use strict';
  	let metadata = spec.metadata;
  
  	if (typeof metadata === 'object') {
  		this.data.checks[spec.id] = metadata;
  		// Transform messages into functions:
  		if (typeof metadata.messages === 'object') {
  			Object.keys(metadata.messages)
  				.filter(
  					prop =>
  						metadata.messages.hasOwnProperty(prop) &&
  						typeof metadata.messages[prop] === 'string'
  				)
  				.forEach(prop => {
  					if (metadata.messages[prop].indexOf('function') === 0) {
  						metadata.messages[prop] = new Function(
  							'return ' + metadata.messages[prop] + ';'
  						)();
  					}
  				});
  		}
  	}
  
  	if (this.checks[spec.id]) {
  		this.checks[spec.id].configure(spec);
  	} else {
  		this.checks[spec.id] = new Check(spec);
  	}
  };
  
  /**
   * Splits a given array of rules to two, with rules that can be run immediately and one's that are dependent on preloadedAssets
   * @method getRulesToRun
   * @param {Array<Object>} rules complete list of rules
   * @param {Object} context
   * @param {Object} options
   * @return {Object} out, an object containing two arrays, one being list of rules to run now and list of rules to run later
   * @private
   */
  function getRulesToRun(rules, context, options) {
  	// entry object for reduce function below
  	const base = {
  		now: [],
  		later: []
  	};
  
  	// iterate through rules and separate out rules that need to be run now vs later
  	const splitRules = rules.reduce((out, rule) => {
  		// ensure rule can run
  		if (!axe.utils.ruleShouldRun(rule, context, options)) {
  			return out;
  		}
  
  		// does rule require preload assets - push to later array
  		if (rule.preload) {
  			out.later.push(rule);
  			return out;
  		}
  
  		// default to now array
  		out.now.push(rule);
  
  		// return
  		return out;
  	}, base);
  
  	// return
  	return splitRules;
  }
  
  /**
   * Convenience method, that consturcts a rule `run` function that can be deferred
   * @param {Object} rule rule to be deferred
   * @param {Object} context context object essential to be passed into rule `run`
   * @param {Object} options normalised options to be passed into rule `run`
   * @param {Object} assets (optional) preloaded assets to be passed into rule and checks (if the rule is preload dependent)
   * @return {Function} a deferrable function for rule
   */
  function getDefferedRule(rule, context, options) {
  	// init performance timer of requested via options
  	if (options.performanceTimer) {
  		axe.utils.performanceTimer.mark('mark_rule_start_' + rule.id);
  	}
  
  	return (resolve, reject) => {
  		// invoke `rule.run`
  		rule.run(
  			context,
  			options,
  			// resolve callback for rule `run`
  			ruleResult => {
  				// resolve
  				resolve(ruleResult);
  			},
  			// reject callback for rule `run`
  			err => {
  				// if debug - construct error details
  				if (!options.debug) {
  					const errResult = Object.assign(new RuleResult(rule), {
  						result: axe.constants.CANTTELL,
  						description: 'An error occured while running this rule',
  						message: err.message,
  						stack: err.stack,
  						error: err,
  						// Add a serialized reference to the node the rule failed on for easier debugging.
  						// See https://github.com/dequelabs/axe-core/issues/1317.
  						errorNode: err.errorNode
  					});
  					// resolve
  					resolve(errResult);
  				} else {
  					// reject
  					reject(err);
  				}
  			}
  		);
  	};
  }
  
  /**
   * Runs the Audit; which in turn should call `run` on each rule.
   * @async
   * @param  {Context}   context The scope definition/context for analysis (include/exclude)
   * @param  {Object}    options Options object to pass into rules and/or disable rules or checks
   * @param  {Function} fn       Callback function to fire when audit is complete
   */
  Audit.prototype.run = function(context, options, resolve, reject) {
  	'use strict';
  
  	this.normalizeOptions(options);
  	axe._selectCache = [];
  
  	// get a list of rules to run NOW vs. LATER (later are preload assets dependent rules)
  	const allRulesToRun = getRulesToRun(this.rules, context, options);
  	const runNowRules = allRulesToRun.now;
  	const runLaterRules = allRulesToRun.later;
  
  	// init a NOW queue for rules to run immediately
  	const nowRulesQueue = axe.utils.queue();
  	// construct can run NOW rules into NOW queue
  	runNowRules.forEach(rule => {
  		nowRulesQueue.defer(getDefferedRule(rule, context, options));
  	});
  
  	// init a PRELOADER queue to start preloading assets
  	const preloaderQueue = axe.utils.queue();
  	// defer preload if preload dependent rules exist
  	if (runLaterRules.length) {
  		preloaderQueue.defer(resolve => {
  			// handle both success and fail of preload
  			// and resolve, to allow to run all checks
  			axe.utils
  				.preload(options)
  				.then(assets => resolve(assets))
  				.catch(err => {
  					/**
  					 * Note:
  					 * we do not reject, to allow other (non-preload) rules to `run`
  					 * -> instead we resolve as `undefined`
  					 */
  					console.warn(`Couldn't load preload assets: `, err);
  					resolve(undefined);
  				});
  		});
  	}
  
  	// defer now and preload queue to run immediately
  	const queueForNowRulesAndPreloader = axe.utils.queue();
  	queueForNowRulesAndPreloader.defer(nowRulesQueue);
  	queueForNowRulesAndPreloader.defer(preloaderQueue);
  
  	// invoke the now queue
  	queueForNowRulesAndPreloader
  		.then(nowRulesAndPreloaderResults => {
  			// interpolate results into separate variables
  			const assetsFromQueue = nowRulesAndPreloaderResults.pop();
  			if (assetsFromQueue && assetsFromQueue.length) {
  				// result is a queue (again), hence the index resolution
  				// assets is either an object of key value pairs of asset type and values
  				// eg: cssom: [stylesheets]
  				// or undefined if preload failed
  				const assets = assetsFromQueue[0];
  				// extend context with preloaded assets
  				if (assets) {
  					context = {
  						...context,
  						...assets
  					};
  				}
  			}
  
  			// the reminder of the results are RuleResults
  			const nowRulesResults = nowRulesAndPreloaderResults[0];
  
  			// if there are no rules to run LATER - resolve with rule results
  			if (!runLaterRules.length) {
  				// remove the cache
  				axe._selectCache = undefined;
  				// resolve
  				resolve(nowRulesResults.filter(result => !!result));
  				return;
  			}
  
  			// init a LATER queue for rules that are dependant on preloaded assets
  			const laterRulesQueue = axe.utils.queue();
  			runLaterRules.forEach(rule => {
  				const deferredRule = getDefferedRule(rule, context, options);
  				laterRulesQueue.defer(deferredRule);
  			});
  			// invoke the later queue
  			laterRulesQueue
  				.then(laterRuleResults => {
  					// remove the cache
  					axe._selectCache = undefined;
  					// resolve
  					resolve(
  						nowRulesResults.concat(laterRuleResults).filter(result => !!result)
  					);
  				})
  				.catch(reject);
  		})
  		.catch(reject);
  };
  
  /**
   * Runs Rule `after` post processing functions
   * @param  {Array} results  Array of RuleResults to postprocess
   * @param  {Mixed} options  Options object to pass into rules and/or disable rules or checks
   */
  Audit.prototype.after = function(results, options) {
  	'use strict';
  
  	var rules = this.rules;
  
  	return results.map(function(ruleResult) {
  		var rule = axe.utils.findBy(rules, 'id', ruleResult.id);
  		if (!rule) {
  			// If you see this, you're probably running the Mocha tests with the axe extension installed
  			throw new Error(
  				'Result for unknown rule. You may be running mismatch axe-core versions'
  			);
  		}
  
  		return rule.after(ruleResult, options);
  	});
  };
  
  /**
   * Get the rule with a given ID
   * @param  {string}
   * @return {Rule}
   */
  Audit.prototype.getRule = function(ruleId) {
  	return this.rules.find(rule => rule.id === ruleId);
  };
  
  /**
   * Ensure all rules that are expected to run exist
   * @throws {Error} If any tag or rule specified in options is unknown
   * @param  {Object} options  Options object
   * @return {Object}          Validated options object
   */
  Audit.prototype.normalizeOptions = function(options) {
  	'use strict';
  	var audit = this;
  
  	// Validate runOnly
  	if (typeof options.runOnly === 'object') {
  		if (Array.isArray(options.runOnly)) {
  			options.runOnly = {
  				type: 'tag',
  				values: options.runOnly
  			};
  		}
  		const only = options.runOnly;
  		if (only.value && !only.values) {
  			only.values = only.value;
  			delete only.value;
  		}
  
  		if (!Array.isArray(only.values) || only.values.length === 0) {
  			throw new Error('runOnly.values must be a non-empty array');
  		}
  
  		// Check if every value in options.runOnly is a known rule ID
  		if (['rule', 'rules'].includes(only.type)) {
  			only.type = 'rule';
  			only.values.forEach(function(ruleId) {
  				if (!audit.getRule(ruleId)) {
  					throw new Error('unknown rule `' + ruleId + '` in options.runOnly');
  				}
  			});
  
  			// Validate 'tags' (e.g. anything not 'rule')
  		} else if (['tag', 'tags', undefined].includes(only.type)) {
  			only.type = 'tag';
  			const unmatchedTags = audit.rules.reduce((unmatchedTags, rule) => {
  				return unmatchedTags.length
  					? unmatchedTags.filter(tag => !rule.tags.includes(tag))
  					: unmatchedTags;
  			}, only.values);
  
  			if (unmatchedTags.length !== 0) {
  				axe.log('Could not find tags `' + unmatchedTags.join('`, `') + '`');
  			}
  		} else {
  			throw new Error(`Unknown runOnly type '${only.type}'`);
  		}
  	}
  
  	if (typeof options.rules === 'object') {
  		Object.keys(options.rules).forEach(function(ruleId) {
  			if (!audit.getRule(ruleId)) {
  				throw new Error('unknown rule `' + ruleId + '` in options.rules');
  			}
  		});
  	}
  
  	return options;
  };
  
  /*
   * Updates the default options and then applies them
   * @param  {Mixed} options  Options object
   */
  
  Audit.prototype.setBranding = function(branding) {
  	'use strict';
  	let previous = {
  		brand: this.brand,
  		application: this.application
  	};
  	if (
  		branding &&
  		branding.hasOwnProperty('brand') &&
  		branding.brand &&
  		typeof branding.brand === 'string'
  	) {
  		this.brand = branding.brand;
  	}
  	if (
  		branding &&
  		branding.hasOwnProperty('application') &&
  		branding.application &&
  		typeof branding.application === 'string'
  	) {
  		this.application = branding.application;
  	}
  	this._constructHelpUrls(previous);
  };
  
  /**
   * For all the rules, create the helpUrl and add it to the data for that rule
   */
  function getHelpUrl({ brand, application }, ruleId, version) {
  	return (
  		axe.constants.helpUrlBase +
  		brand +
  		'/' +
  		(version || axe.version.substring(0, axe.version.lastIndexOf('.'))) +
  		'/' +
  		ruleId +
  		'?application=' +
  		application
  	);
  }
  
  Audit.prototype._constructHelpUrls = function(previous = null) {
  	var version = (axe.version.match(/^[1-9][0-9]*\.[0-9]+/) || ['x.y'])[0];
  	this.rules.forEach(rule => {
  		if (!this.data.rules[rule.id]) {
  			this.data.rules[rule.id] = {};
  		}
  		let metaData = this.data.rules[rule.id];
  		if (
  			typeof metaData.helpUrl !== 'string' ||
  			(previous && metaData.helpUrl === getHelpUrl(previous, rule.id, version))
  		) {
  			metaData.helpUrl = getHelpUrl(this, rule.id, version);
  		}
  	});
  };
  
  /**
   * Reset the default rules, checks and meta data
   */
  
  Audit.prototype.resetRulesAndChecks = function() {
  	'use strict';
  	this._init();
  	this._resetLocale();
  };
  