/* @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<any>;
		(value: () => mixed, error?: ErrorValidator, message?: string): any;
	};
	// 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 ContextualTestContext         = TestContext & { context: any; };
type ContextualCallbackTestContext = TestContext & { context: any; end(): void; };

/**
 * 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 ContextualTest         = TestImplementation<ContextualTestContext, SpecialReturnTypes | void>;
type ContextualCallbackTest = TestImplementation<ContextualCallbackTestContext, void>;


/**
 * Method Types
 */

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, ...args: any): void;
	(name: string, run: ContextualTest, ...args: any): void;

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