'use strict';
const EventEmitter = require('events');
const fnName = require('fn-name');
const Concurrent = require('./concurrent');
const Sequence = require('./sequence');
const Test = require('./test');

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

		this.bail = options.bail;
		this.failWithoutAssertions = options.failWithoutAssertions;
		this.getSnapshotState = options.getSnapshotState;
		this.hasExclusive = false;
		this.testCount = 0;

		this.tests = {
			concurrent: [],
			serial: []
		};

		this.hooks = {
			before: [],
			beforeEach: [],
			after: [],
			afterAlways: [],
			afterEach: [],
			afterEachAlways: []
		};

		this.pendingTestInstances = new Set();

		this._emitTestResult = this._emitTestResult.bind(this);
	}
	add(test) {
		const metadata = test.metadata;
		const type = metadata.type;

		if (!type) {
			throw new Error('Test type must be specified');
		}

		if (!test.title && test.fn) {
			test.title = fnName(test.fn);
		}

		// Workaround for Babel giving anonymous functions a name
		if (test.title === 'callee$0$0') {
			test.title = null;
		}

		if (!test.title) {
			if (type === 'test') {
				test.title = '[anonymous]';
			} else {
				test.title = type;
			}
		}

		if (metadata.always && type !== 'after' && type !== 'afterEach') {
			throw new Error('"always" can only be used with after and afterEach hooks');
		}

		// Add a hook
		if (type !== 'test') {
			if (metadata.exclusive) {
				throw new Error(`"only" cannot be used with a ${type} hook`);
			}

			this.hooks[type + (metadata.always ? 'Always' : '')].push(test);
			return;
		}

		this.testCount++;

		// Add `.only()` tests if `.only()` was used previously
		if (this.hasExclusive && !metadata.exclusive) {
			return;
		}

		if (metadata.exclusive && !this.hasExclusive) {
			this.tests.concurrent = [];
			this.tests.serial = [];
			this.hasExclusive = true;
		}

		if (metadata.serial) {
			this.tests.serial.push(test);
		} else {
			this.tests.concurrent.push(test);
		}
	}
	_skippedTest(test) {
		return {
			run: () => {
				this._emitTestResult({
					passed: true,
					result: test
				});

				return true;
			}
		};
	}
	_emitTestResult(result) {
		this.pendingTestInstances.delete(result.result);
		this.emit('test', result);
	}
	_buildHooks(hooks, testTitle, context) {
		return hooks.map(hook => {
			const test = this._buildHook(hook, testTitle, context);

			if (hook.metadata.skipped || hook.metadata.todo) {
				return this._skippedTest(test);
			}

			return test;
		});
	}
	_buildHook(hook, testTitle, contextRef) {
		let title = hook.title;

		if (testTitle) {
			title += ` for ${testTitle}`;
		}

		if (!contextRef) {
			contextRef = null;
		}

		const test = new Test({
			contextRef,
			failWithoutAssertions: false,
			fn: hook.fn,
			getSnapshotState: this.getSnapshotState,
			metadata: hook.metadata,
			onResult: this._emitTestResult,
			title
		});
		this.pendingTestInstances.add(test);
		return test;
	}
	_buildTest(test, contextRef) {
		if (!contextRef) {
			contextRef = null;
		}

		test = new Test({
			contextRef,
			failWithoutAssertions: this.failWithoutAssertions,
			fn: test.fn,
			getSnapshotState: this.getSnapshotState,
			metadata: test.metadata,
			onResult: this._emitTestResult,
			title: test.title
		});
		this.pendingTestInstances.add(test);
		return test;
	}
	_buildTestWithHooks(test) {
		if (test.metadata.skipped || test.metadata.todo) {
			return new Sequence([this._skippedTest(this._buildTest(test))], true);
		}

		const context = {context: {}};

		const beforeHooks = this._buildHooks(this.hooks.beforeEach, test.title, context);
		const afterHooks = this._buildHooks(this.hooks.afterEach, test.title, context);

		let sequence = new Sequence([].concat(beforeHooks, this._buildTest(test, context), afterHooks), true);
		if (this.hooks.afterEachAlways.length > 0) {
			const afterAlwaysHooks = new Sequence(this._buildHooks(this.hooks.afterEachAlways, test.title, context));
			sequence = new Sequence([sequence, afterAlwaysHooks], false);
		}
		return sequence;
	}
	_buildTests(tests) {
		return tests.map(test => this._buildTestWithHooks(test));
	}
	build() {
		const beforeHooks = new Sequence(this._buildHooks(this.hooks.before));
		const afterHooks = new Sequence(this._buildHooks(this.hooks.after));

		const serialTests = new Sequence(this._buildTests(this.tests.serial), this.bail);
		const concurrentTests = new Concurrent(this._buildTests(this.tests.concurrent), this.bail);
		const allTests = new Sequence([serialTests, concurrentTests]);

		let finalTests = new Sequence([beforeHooks, allTests, afterHooks], true);
		if (this.hooks.afterAlways.length > 0) {
			const afterAlwaysHooks = new Sequence(this._buildHooks(this.hooks.afterAlways));
			finalTests = new Sequence([finalTests, afterAlwaysHooks], false);
		}
		return finalTests;
	}
	attributeLeakedError(err) {
		for (const test of this.pendingTestInstances) {
			if (test.attributeLeakedError(err)) {
				return true;
			}
		}
		return false;
	}
}

module.exports = TestCollection;