/* @flow */

/**
 * Misc Setup Types
 */

type PromiseLike<R> = {
	then<U>(
		onFulfill?: (value: R) => Promise<U> | U,
		onReject?: (error: any) => Promise<U> | U
	): Promise<U>;
}

type ObservableLike = {
	subscribe(observer: (value: {}) => void): void;
};

type SpecialReturnTypes =
	| PromiseLike<any>
	| Iterator<any>
	| ObservableLike;

type Constructor = Class<{
	constructor(...args: Array<any>): any
}>;

type ErrorValidator =
	| Constructor
	| RegExp
	| string
	| ((error: any) => boolean);

/**
 * Assertion Types
 */

type AssertContext = {
	// Passing assertion.
	pass(message?: string): void;
	// Failing assertion.
	fail(message?: string): void;
	// Assert that value is truthy.
	truthy(value: mixed, message?: string): void;
	// Assert that value is falsy.
	falsy(value: mixed, message?: string): void;
	// Assert that value is true.
	true(value: mixed, message?: string): void;
	// Assert that value is false.
	false(value: mixed, message?: string): void;
	// Assert that value is equal to expected.
	is<U>(value: U, expected: U, message?: string): void;
	// Assert that value is not equal to expected.
	not<U>(value: U, expected: U, message?: string): void;
	// Assert that value is deep equal to expected.
	deepEqual<U>(value: U, expected: U, message?: string): void;
	// Assert that value is not deep equal to expected.
	notDeepEqual<U>(value: U, expected: U, message?: string): void;
	// Assert that function throws an error or promise rejects.
	// @param error Can be a constructor, regex, error message or validation function.
	throws: {
		(value: PromiseLike<mixed>, error?: ErrorValidator, message?: string): Promise<Error>;
		(value: () => mixed, error?: ErrorValidator, message?: string): Error;
	};
	// Assert that function doesn't throw an error or promise resolves.
	notThrows: {
		(value: PromiseLike<mixed>, message?: string): Promise<void>;
		(value: () => mixed, message?: string): void;
	};
	// Assert that contents matches regex.
	regex(contents: string, regex: RegExp, message?: string): void;
	// Assert that contents matches a snapshot.
	snapshot(contents: any, message?: string): void;
	// Assert that contents does not match regex.
	notRegex(contents: string, regex: RegExp, message?: string): void;
	// Assert that error is falsy.
	ifError(error: any, message?: string): void;
};

/**
 * Context Types
 */

type TestContext = AssertContext & {
	plan(count: number): void;
	skip: AssertContext;
};
type CallbackTestContext           = TestContext         & { end(): void; };
type ContextualTestContext         = TestContext         & { context: any; };
type ContextualCallbackTestContext = CallbackTestContext & { context: any; };

/**
 * Test Implementations
 */

type TestFunction<T, R> = {
	(t: T, ...args: Array<any>): R;
	title?: (providedTitle: string, ...args: Array<any>) => string;
};

type TestImplementation<T, R> =
	| TestFunction<T, R>
	| Array<TestFunction<T, R>>;

type Test                   = TestImplementation<TestContext, SpecialReturnTypes | void>;
type CallbackTest           = TestImplementation<CallbackTestContext, void>;
type ContextualTest         = TestImplementation<ContextualTestContext, SpecialReturnTypes | void>;
type ContextualCallbackTest = TestImplementation<ContextualCallbackTestContext, void>;


/**
 * Method Types
 */

type TestMethod = {
	(              implementation: Test): void;
	(name: string, implementation: Test): void;

	serial     : TestMethod;
	before     : TestMethod;
	after      : TestMethod;
	skip       : TestMethod;
	todo       : TestMethod;
	failing    : TestMethod;
	only       : TestMethod;
	beforeEach : TestMethod;
	afterEach  : TestMethod;
	cb         : CallbackTestMethod;
	always     : TestMethod;
};

type CallbackTestMethod = {
	(              implementation: CallbackTest): void;
	(name: string, implementation: CallbackTest): void;

	serial     : CallbackTestMethod;
	before     : CallbackTestMethod;
	after      : CallbackTestMethod;
	skip       : CallbackTestMethod;
	todo       : CallbackTestMethod;
	failing    : CallbackTestMethod;
	only       : CallbackTestMethod;
	beforeEach : CallbackTestMethod;
	afterEach  : CallbackTestMethod;
	cb         : CallbackTestMethod;
	always     : CallbackTestMethod;
};

type ContextualTestMethod = {
	(              implementation: ContextualTest): void;
	(name: string, implementation: ContextualTest): void;

	serial     : ContextualTestMethod;
	before     : ContextualTestMethod;
	after      : ContextualTestMethod;
	skip       : ContextualTestMethod;
	todo       : ContextualTestMethod;
	failing    : ContextualTestMethod;
	only       : ContextualTestMethod;
	beforeEach : ContextualTestMethod;
	afterEach  : ContextualTestMethod;
	cb         : ContextualCallbackTestMethod;
	always     : ContextualTestMethod;
};

type ContextualCallbackTestMethod = {
	(              implementation: ContextualCallbackTest): void;
	(name: string, implementation: ContextualCallbackTest): void;

	serial     : ContextualCallbackTestMethod;
	before     : ContextualCallbackTestMethod;
	after      : ContextualCallbackTestMethod;
	skip       : ContextualCallbackTestMethod;
	todo       : ContextualCallbackTestMethod;
	failing    : ContextualCallbackTestMethod;
	only       : ContextualCallbackTestMethod;
	beforeEach : ContextualCallbackTestMethod;
	afterEach  : ContextualCallbackTestMethod;
	cb         : ContextualCallbackTestMethod;
	always     : ContextualCallbackTestMethod;
};

/**
 * Public API
 */

declare module.exports: {
	(              run: ContextualTest): void;
	(name: string, run: ContextualTest): void;

	beforeEach : ContextualTestMethod;
	afterEach  : ContextualTestMethod;
	serial     : ContextualTestMethod;
	before     : ContextualTestMethod;
	after      : ContextualTestMethod;
	skip       : ContextualTestMethod;
	todo       : ContextualTestMethod;
	failing    : ContextualTestMethod;
	only       : ContextualTestMethod;
	cb         : ContextualCallbackTestMethod;
	always     : ContextualTestMethod;
};