import {
	createDeferredPromise,
	DeferredPromise,
} from "alcalzone-shared/deferred-promise";

/** Allows waiting for something for a given amount of time, after which the expectation will automatically be rejected. */
export class TimedExpectation<TResult = void, TPredicate = never>
	implements PromiseLike<TResult>
{
	public constructor(
		timeoutMs: number,
		public readonly predicate?: (input: TPredicate) => boolean,
		private readonly timeoutErrorMessage: string = "Expectation was not fulfilled within the timeout",
	) {
		this.promise = createDeferredPromise<TResult>();
		this.timeout = setTimeout(() => this.reject(), timeoutMs);

		// We need create the stack on a temporary object or the Error
		// class will try to print the message
		const tmp = { message: "" };
		Error.captureStackTrace(tmp, TimedExpectation);
		this.stack = (tmp as any).stack.replace(/^Error:?\s*\n/, "");
	}

	private promise: DeferredPromise<TResult>;
	private timeout?: NodeJS.Timeout;
	private _done: boolean = false;

	/** The stack trace where the timed expectation was created */
	public readonly stack: string;

	public resolve(result: TResult): void {
		if (this._done) return;

		if (this.timeout) {
			clearTimeout(this.timeout);
		}
		this.promise.resolve(result);
	}

	private reject(): void {
		if (this._done) return;

		if (this.timeout) {
			clearTimeout(this.timeout);
		}
		const err = new Error(this.timeoutErrorMessage);
		err.stack = this.stack;
		this.promise.reject(err);
	}

	// Make this await-able
	then<TResult1 = TResult, TResult2 = never>(
		onfulfilled?:
			| ((value: TResult) => TResult1 | PromiseLike<TResult1>)
			| null,
		onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,
	): PromiseLike<TResult1 | TResult2> {
		return this.promise.then(onfulfilled, onrejected);
	}
}
