'use strict';
const EventEmitter = require('events');
const path = require('path');
const Bluebird = require('bluebird');
const optionChain = require('option-chain');
const matcher = require('matcher');
const snapshotManager = require('./snapshot-manager');
const TestCollection = require('./test-collection');
const validateTest = require('./validate-test');

const chainableMethods = {
	defaults: {
		type: 'test',
		serial: false,
		exclusive: false,
		skipped: false,
		todo: false,
		failing: false,
		callback: false,
		always: false
	},
	chainableMethods: {
		test: {},
		serial: {serial: true},
		before: {type: 'before'},
		after: {type: 'after'},
		skip: {skipped: true},
		todo: {todo: true},
		failing: {failing: true},
		only: {exclusive: true},
		beforeEach: {type: 'beforeEach'},
		afterEach: {type: 'afterEach'},
		cb: {callback: true},
		always: {always: true}
	}
};

function wrapFunction(fn, args) {
	return function (t) {
		return fn.apply(this, [t].concat(args));
	};
}

class Runner extends EventEmitter {
	constructor(options) {
		super();

		options = options || {};

		this.file = options.file;
		this.match = options.match || [];
		this.projectDir = options.projectDir;
		this.serial = options.serial;
		this.updateSnapshots = options.updateSnapshots;

		this.hasStarted = false;
		this.results = [];
		this.snapshots = null;
		this.tests = new TestCollection({
			bail: options.bail,
			failWithoutAssertions: options.failWithoutAssertions,
			compareTestSnapshot: this.compareTestSnapshot.bind(this)
		});

		this.chain = optionChain(chainableMethods, (opts, args) => {
			let title;
			let fn;
			let macroArgIndex;

			if (this.hasStarted) {
				throw new Error('All tests and hooks must be declared synchronously in your ' +
				'test file, and cannot be nested within other tests or hooks.');
			}

			if (typeof args[0] === 'string') {
				title = args[0];
				fn = args[1];
				macroArgIndex = 2;
			} else {
				fn = args[0];
				title = null;
				macroArgIndex = 1;
			}

			if (this.serial) {
				opts.serial = true;
			}

			if (args.length > macroArgIndex) {
				args = args.slice(macroArgIndex);
			} else {
				args = null;
			}

			if (Array.isArray(fn)) {
				fn.forEach(fn => {
					this.addTest(title, opts, fn, args);
				});
			} else {
				this.addTest(title, opts, fn, args);
			}
		});
	}

	addTest(title, metadata, fn, args) {
		if (args) {
			if (fn.title) {
				title = fn.title.apply(fn, [title || ''].concat(args));
			}

			fn = wrapFunction(fn, args);
		}

		if (metadata.type === 'test' && this.match.length > 0) {
			metadata.exclusive = title !== null && matcher([title], this.match).length === 1;
		}

		const validationError = validateTest(title, fn, metadata);
		if (validationError !== null) {
			throw new TypeError(validationError);
		}

		this.tests.add({
			metadata,
			fn,
			title
		});
	}

	addTestResult(result) {
		const test = result.result;
		const props = {
			duration: test.duration,
			title: test.title,
			error: result.reason,
			type: test.metadata.type,
			skip: test.metadata.skipped,
			todo: test.metadata.todo,
			failing: test.metadata.failing
		};

		this.results.push(result);
		this.emit('test', props);
	}

	buildStats() {
		const stats = {
			failCount: 0,
			knownFailureCount: 0,
			passCount: 0,
			skipCount: 0,
			testCount: 0,
			todoCount: 0
		};

		for (const result of this.results) {
			if (!result.passed) {
				// Includes hooks
				stats.failCount++;
			}

			const metadata = result.result.metadata;
			if (metadata.type === 'test') {
				stats.testCount++;

				if (metadata.skipped) {
					stats.skipCount++;
				} else if (metadata.todo) {
					stats.todoCount++;
				} else if (result.passed) {
					if (metadata.failing) {
						stats.knownFailureCount++;
					} else {
						stats.passCount++;
					}
				}
			}
		}

		return stats;
	}

	compareTestSnapshot(options) {
		if (!this.snapshots) {
			this.snapshots = snapshotManager.load({
				name: path.basename(this.file),
				projectDir: this.projectDir,
				relFile: path.relative(this.projectDir, this.file),
				testDir: path.dirname(this.file),
				updating: this.updateSnapshots
			});
			this.emit('dependency', this.snapshots.snapPath);
		}

		return this.snapshots.compare(options);
	}

	saveSnapshotState() {
		if (this.snapshots) {
			const files = this.snapshots.save();
			if (files) {
				this.emit('touched', files);
			}
		} else if (this.updateSnapshots) {
			// TODO: There may be unused snapshot files if no test caused the
			// snapshots to be loaded. Prune them. But not if tests (including hooks!)
			// were skipped. Perhaps emit a warning if this occurs?
		}
	}

	run(options) {
		if (options.runOnlyExclusive && !this.tests.hasExclusive) {
			return Promise.resolve(null);
		}

		this.hasStarted = true;
		this.tests.on('test', result => {
			this.addTestResult(result);
		});
		return Bluebird.try(() => this.tests.build().run());
	}
	attributeLeakedError(err) {
		return this.tests.attributeLeakedError(err);
	}
}

module.exports = Runner;