'use strict';

const defaultExclude = require('./default-exclude.js');

const nycCommands = {
	all: [null, 'check-coverage', 'instrument', 'merge', 'report'],
	testExclude: [null, 'instrument', 'report', 'check-coverage'],
	instrument: [null, 'instrument'],
	checkCoverage: [null, 'report', 'check-coverage'],
	report: [null, 'report'],
	main: [null],
	instrumentOnly: ['instrument']
};

const cwd = {
	description: 'working directory used when resolving paths',
	type: 'string',
	get default() {
		return process.cwd();
	},
	nycCommands: nycCommands.all
};

const nycrcPath = {
	description: 'specify an explicit path to find nyc configuration',
	nycCommands: nycCommands.all
};

const tempDir = {
	description: 'directory to output raw coverage information to',
	type: 'string',
	default: './.nyc_output',
	nycAlias: 't',
	nycHiddenAlias: 'temp-directory',
	nycCommands: [null, 'check-coverage', 'merge', 'report']
};

const testExclude = {
	exclude: {
		description: 'a list of specific files and directories that should be excluded from coverage, glob patterns are supported',
		type: 'array',
		items: {
			type: 'string'
		},
		default: defaultExclude,
		nycCommands: nycCommands.testExclude,
		nycAlias: 'x'
	},
	excludeNodeModules: {
		description: 'whether or not to exclude all node_module folders (i.e. **/node_modules/**) by default',
		type: 'boolean',
		default: true,
		nycCommands: nycCommands.testExclude
	},
	include: {
		description: 'a list of specific files that should be covered, glob patterns are supported',
		type: 'array',
		items: {
			type: 'string'
		},
		default: [],
		nycCommands: nycCommands.testExclude,
		nycAlias: 'n'
	},
	extension: {
		description: 'a list of extensions that nyc should handle in addition to .js',
		type: 'array',
		items: {
			type: 'string'
		},
		default: ['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx'],
		nycCommands: nycCommands.testExclude,
		nycAlias: 'e'
	}
};

const instrumentVisitor = {
	coverageVariable: {
		description: 'variable to store coverage',
		type: 'string',
		default: '__coverage__',
		nycCommands: nycCommands.instrument
	},
	coverageGlobalScope: {
		description: 'scope to store the coverage variable',
		type: 'string',
		default: 'this',
		nycCommands: nycCommands.instrument
	},
	coverageGlobalScopeFunc: {
		description: 'avoid potentially replaced `Function` when finding global scope',
		type: 'boolean',
		default: true,
		nycCommands: nycCommands.instrument
	},
	ignoreClassMethods: {
		description: 'class method names to ignore for coverage',
		type: 'array',
		items: {
			type: 'string'
		},
		default: [],
		nycCommands: nycCommands.instrument
	}
};

const instrumentParseGen = {
	autoWrap: {
		description: 'allow `return` statements outside of functions',
		type: 'boolean',
		default: true,
		nycCommands: nycCommands.instrument
	},
	esModules: {
		description: 'should files be treated as ES Modules',
		type: 'boolean',
		default: true,
		nycCommands: nycCommands.instrument
	},
	parserPlugins: {
		description: 'babel parser plugins to use when parsing the source',
		type: 'array',
		items: {
			type: 'string'
		},
		/* Babel parser plugins are to be enabled when the feature is stage 3 and
		 * implemented in a released version of node.js. */
		default: [
			'asyncGenerators',
			'bigInt',
			'classProperties',
			'classPrivateProperties',
			'dynamicImport',
			'importMeta',
			'objectRestSpread',
			'optionalCatchBinding'
		],
		nycCommands: nycCommands.instrument
	},
	compact: {
		description: 'should the output be compacted?',
		type: 'boolean',
		default: true,
		nycCommands: nycCommands.instrument
	},
	preserveComments: {
		description: 'should comments be preserved in the output?',
		type: 'boolean',
		default: true,
		nycCommands: nycCommands.instrument
	},
	produceSourceMap: {
		description: 'should source maps be produced?',
		type: 'boolean',
		default: true,
		nycCommands: nycCommands.instrument
	}
};

const checkCoverage = {
	excludeAfterRemap: {
		description: 'should exclude logic be performed after the source-map remaps filenames?',
		type: 'boolean',
		default: true,
		nycCommands: nycCommands.checkCoverage
	},
	branches: {
		description: 'what % of branches must be covered?',
		type: 'number',
		default: 0,
		minimum: 0,
		maximum: 100,
		nycCommands: nycCommands.checkCoverage
	},
	functions: {
		description: 'what % of functions must be covered?',
		type: 'number',
		default: 0,
		minimum: 0,
		maximum: 100,
		nycCommands: nycCommands.checkCoverage
	},
	lines: {
		description: 'what % of lines must be covered?',
		type: 'number',
		default: 90,
		minimum: 0,
		maximum: 100,
		nycCommands: nycCommands.checkCoverage
	},
	statements: {
		description: 'what % of statements must be covered?',
		type: 'number',
		default: 0,
		minimum: 0,
		maximum: 100,
		nycCommands: nycCommands.checkCoverage
	},
	perFile: {
		description: 'check thresholds per file',
		type: 'boolean',
		default: false,
		nycCommands: nycCommands.checkCoverage
	}
};

const report = {
	checkCoverage: {
		description: 'check whether coverage is within thresholds provided',
		type: 'boolean',
		default: false,
		nycCommands: nycCommands.report
	},
	reporter: {
		description: 'coverage reporter(s) to use',
		type: 'array',
		items: {
			type: 'string'
		},
		default: ['text'],
		nycCommands: nycCommands.report,
		nycAlias: 'r'
	},
	reportDir: {
		description: 'directory to output coverage reports in',
		type: 'string',
		default: 'coverage',
		nycCommands: nycCommands.report
	},
	showProcessTree: {
		description: 'display the tree of spawned processes',
		type: 'boolean',
		default: false,
		nycCommands: nycCommands.report
	},
	skipEmpty: {
		description: 'don\'t show empty files (no lines of code) in report',
		type: 'boolean',
		default: false,
		nycCommands: nycCommands.report
	},
	skipFull: {
		description: 'don\'t show files with 100% statement, branch, and function coverage',
		type: 'boolean',
		default: false,
		nycCommands: nycCommands.report
	}
};

const nycMain = {
	silent: {
		description: 'don\'t output a report after tests finish running',
		type: 'boolean',
		default: false,
		nycCommands: nycCommands.main,
		nycAlias: 's'
	},
	all: {
		description: 'whether or not to instrument all files of the project (not just the ones touched by your test suite)',
		type: 'boolean',
		default: false,
		nycCommands: nycCommands.main,
		nycAlias: 'a'
	},
	eager: {
		description: 'instantiate the instrumenter at startup (see https://git.io/vMKZ9)',
		type: 'boolean',
		default: false,
		nycCommands: nycCommands.main
	},
	cache: {
		description: 'cache instrumentation results for improved performance',
		type: 'boolean',
		default: true,
		nycCommands: nycCommands.main,
		nycAlias: 'c'
	},
	cacheDir: {
		description: 'explicitly set location for instrumentation cache',
		type: 'string',
		nycCommands: nycCommands.main
	},
	babelCache: {
		description: 'cache babel transpilation results for improved performance',
		type: 'boolean',
		default: false,
		nycCommands: nycCommands.main
	},
	useSpawnWrap: {
		description: 'use spawn-wrap instead of setting process.env.NODE_OPTIONS',
		type: 'boolean',
		default: false,
		nycCommands: nycCommands.main
	},
	hookRequire: {
		description: 'should nyc wrap require?',
		type: 'boolean',
		default: true,
		nycCommands: nycCommands.main
	},
	hookRunInContext: {
		description: 'should nyc wrap vm.runInContext?',
		type: 'boolean',
		default: false,
		nycCommands: nycCommands.main
	},
	hookRunInThisContext: {
		description: 'should nyc wrap vm.runInThisContext?',
		type: 'boolean',
		default: false,
		nycCommands: nycCommands.main
	},
	clean: {
		description: 'should the .nyc_output folder be cleaned before executing tests',
		type: 'boolean',
		default: true,
		nycCommands: nycCommands.main
	}
};

const instrumentOnly = {
	inPlace: {
		description: 'should nyc run the instrumentation in place?',
		type: 'boolean',
		default: false,
		nycCommands: nycCommands.instrumentOnly
	},
	exitOnError: {
		description: 'should nyc exit when an instrumentation failure occurs?',
		type: 'boolean',
		default: false,
		nycCommands: nycCommands.instrumentOnly
	},
	delete: {
		description: 'should the output folder be deleted before instrumenting files?',
		type: 'boolean',
		default: false,
		nycCommands: nycCommands.instrumentOnly
	},
	completeCopy: {
		description: 'should nyc copy all files from input to output as well as instrumented files?',
		type: 'boolean',
		default: false,
		nycCommands: nycCommands.instrumentOnly
	}
};

const nyc = {
	description: 'nyc configuration options',
	type: 'object',
	properties: {
		cwd,
		nycrcPath,
		tempDir,

		/* Test Exclude */
		...testExclude,

		/* Instrumentation settings */
		...instrumentVisitor,

		/* Instrumentation parser/generator settings */
		...instrumentParseGen,
		sourceMap: {
			description: 'should nyc detect and handle source maps?',
			type: 'boolean',
			default: true,
			nycCommands: nycCommands.instrument
		},
		require: {
			description: 'a list of additional modules that nyc should attempt to require in its subprocess, e.g., @babel/register, @babel/polyfill',
			type: 'array',
			items: {
				type: 'string'
			},
			default: [],
			nycCommands: nycCommands.instrument,
			nycAlias: 'i'
		},
		instrument: {
			description: 'should nyc handle instrumentation?',
			type: 'boolean',
			default: true,
			nycCommands: nycCommands.instrument
		},

		/* Check coverage */
		...checkCoverage,

		/* Report options */
		...report,

		/* Main command options */
		...nycMain,

		/* Instrument command options */
		...instrumentOnly
	}
};

const configs = {
	nyc,
	testExclude: {
		description: 'test-exclude options',
		type: 'object',
		properties: {
			cwd,
			...testExclude
		}
	},
	babelPluginIstanbul: {
		description: 'babel-plugin-istanbul options',
		type: 'object',
		properties: {
			cwd,
			...testExclude,
			...instrumentVisitor
		}
	},
	instrumentVisitor: {
		description: 'instrument visitor options',
		type: 'object',
		properties: instrumentVisitor
	},
	instrumenter: {
		description: 'stand-alone instrumenter options',
		type: 'object',
		properties: {
			...instrumentVisitor,
			...instrumentParseGen
		}
	}
};

function defaultsReducer(defaults, [name, {default: value}]) {
	/* Modifying arrays in defaults is safe, does not change schema. */
	if (Array.isArray(value)) {
		value = [...value];
	}

	return Object.assign(defaults, {[name]: value});
}

module.exports = {
	...configs,
	defaults: Object.keys(configs).reduce(
		(defaults, id) => {
			Object.defineProperty(defaults, id, {
				enumerable: true,
				get() {
					/* This defers `process.cwd()` until defaults are requested. */
					return Object.entries(configs[id].properties)
						.filter(([, info]) => 'default' in info)
						.reduce(defaultsReducer, {});
				}
			});

			return defaults;
		},
		{}
	)
};
