import { expect, Locator, Page } from "@playwright/test";
import LoggingUtils from "./LoggingUtils";
import { DateTime } from "luxon";


export type TimeoutOpts = {
    /**
     * @description timeout Commonly referred to for an entire method; exists to allow developers to specify their own timeout whout overriding defaults
     */

    timeout: number
    /**
     * @description timeout_tiny use for exceedingly small waits, as in, waiting for label to contain the test you just typed into it.
     */
    timeoutTiny: number

    /**
     * @description timeout_short for relatively quick opperations, such as waiting for a dropdown to render
     */
    timeoutShort: number

    /**
     * @description timeout_medium for moderately slow operations, such as a new modal rendering, or a calculated field value being updated, or an API Call 
     */
    timeoutMedium: number

    /** 
     * @description timeout_long for high-risk, slow operations. Waiting for the minicart to load, waiting for login, or waiting for screen-to-screen navigation. 
     */
    timeoutLong: number

    /**
     * @description for when things are really, really bad. 
     */
    timeoutConga: number
}

export const timeoutOptDefaults: TimeoutOpts = {
    timeout: 60_000,
    timeoutTiny: +(process.env.TIMEOUT_TINY ?? 500),
    timeoutShort: +(process.env.TIMEOUT_SHORT ?? 5000),
    timeoutMedium: +(process.env.TIMEOUT_MEDIUM ?? 30_000),
    timeoutLong: +(process.env.TIMEOUT_LONG ?? 180_000),
    timeoutConga: +(process.env.TIMEOUT_CONGA ?? 500_000),
}

export type WaitUntilOpts = {
    delayBetweenChecks: number,
    continueOnTimeoutError: boolean,
    anticipatedConditionResult: boolean,
    conditionName: string,
    beginWaitingMessage: string,
    delayBetweenChecksMessage: string,
    timeoutErrorMessage: string,
    successMessage: string,
    ignoreErrorsFromConditionFunction: boolean,

}
export const waitUntilOptDefaults: WaitUntilOpts = {
    delayBetweenChecks: 3000,
    continueOnTimeoutError: false,
    anticipatedConditionResult: true,
    conditionName: "",
    beginWaitingMessage: "",
    delayBetweenChecksMessage: "",
    timeoutErrorMessage: "Timed Out",
    successMessage: "",
    ignoreErrorsFromConditionFunction: true,
}

/**
 * @description repeatedly execute an asynchronous conditional lambda until a given outcome occurs, or the method times-out. Useful for hedgning against GUI race conditions.
 * @param conditionFunction the condition lambda. Example: ()=>{await return myPage.someButton.isVisible()}
 * @param options standard Timeout and WaitUntil Options.
 * @returns true or false - the outcome of the waituntil.
 */

export async function waitUntil(conditionFunction: (...args: any[]) => Promise<boolean>, options: Partial<TimeoutOpts & WaitUntilOpts> = {}): Promise<boolean> {
    const opts = { ...waitUntilOptDefaults, ...timeoutOptDefaults, ...options }

    let timeoutAt = Date.now() + opts.timeout;
    let waitUntilHasTimedOut = false;
    let conditionHasBeenMet = false;
    do {
        try {
            const conditionResult = await conditionFunction()
            conditionHasBeenMet = conditionResult == opts.anticipatedConditionResult
        }
        catch (e) {
            if (!opts.ignoreErrorsFromConditionFunction) {
                throw e
            }
        }
        waitUntilHasTimedOut = Date.now() > timeoutAt
        if (!waitUntilHasTimedOut && !conditionHasBeenMet) {
            await delay(opts.delayBetweenChecks)
        }
        else if (waitUntilHasTimedOut && !opts.continueOnTimeoutError) {
            throw new Error(opts.timeoutErrorMessage)
        }
    }
    while (!conditionHasBeenMet || waitUntilHasTimedOut)
    return conditionHasBeenMet;
}

/**
 * @description Order Matters; WaitUntil each condition passes before moving to the next. All conditions must pass in the expected order. 
 * @param sequentialConditionFunctions an array of async, boolean lambdas to be executed in sqeuance until all have passed. 
 * @param options standard Timeout and WaitUntil options
 */
export async function waitUntilValueStopsChanging(mercurialValueFunction: (...args: any[]) => Promise<any>, options: Partial<TimeoutOpts & WaitUntilOpts> = {}): Promise<any> {
    const opts = { ...waitUntilOptDefaults, ...timeoutOptDefaults, ...options }

    let lastFoundValue: any = undefined;
    const valueHasStoppedChanging = async () => {
        const newFoundValue = await mercurialValueFunction();
        if (opts.delayBetweenChecksMessage.length > 0) console.log(`${opts.delayBetweenChecksMessage} - Last Value: ${lastFoundValue}`)
        const valueIsStable = (newFoundValue != undefined) && (newFoundValue == lastFoundValue);
        lastFoundValue = newFoundValue;
        return valueIsStable;
    }
    await waitUntil(valueHasStoppedChanging, opts)
    return lastFoundValue;
}

/**
 * @todo EXPIRIMENTAL! NO UNIT TESTS YET! TODO, add - DF, 5/23
 * @description Order Matters; WaitUntil each condition passes before moving to the next. All conditions must pass in the expected order. 
 * @param sequentialConditionFunctions an array of async, boolean lambdas to be executed in sqeuance until all have passed. 
 * @param options standard Timeout and WaitUntil options
 */
export async function waitUntilEach(sequentialConditionFunctions: ((...args: any[]) => Promise<boolean>)[], options: Partial<TimeoutOpts & WaitUntilOpts> = {}): Promise<any> {
    for (const conditionFunction of sequentialConditionFunctions) {
        await waitUntil(conditionFunction, options);
    }
}

/**
 * @todo EXPIRIMENTAL! NO UNIT TESTS YET! TODO, add - DF, 5/23
 * @description Order Matters; WaitUntil each condition passes before moving to the next. All conditions must pass in the expected order. 
 * @param sequentialConditionFunctions an array of async, boolean lambdas to be executed in sequence until all have passed.
 * @param options standard Timeout and WaitUntil options
 */
export async function waitUntilAll(sequentialConditionFunctions: ((...args: any[]) => Promise<boolean>)[], options: Partial<TimeoutOpts & WaitUntilOpts> = {}): Promise<any> {
    for (const conditionFunction of sequentialConditionFunctions) {
        await waitUntil(conditionFunction, options);
    }
}

/**
 * @todo EXPIRIMENTAL! NO UNIT TESTS YET! TODO, add - DF, 5/23
 * @description Order DOES NOT Matter; WaitUntil ANY condition passes before completing. Use when multiple conditions can give confidence that sufficient waiting has occured. 
 * @param sequentialConditionFunctions an array of async, boolean lambdas to be executed in psudo-parallel until at least one has passed.
 * @param options standard Timeout and WaitUntil options
 */
export async function waitUntilAny(multipleRequiredConditionFunctions: ((...args: any[]) => Promise<boolean>)[], options: Partial<TimeoutOpts & WaitUntilOpts> = {}): Promise<any> {
    const anyConditionMet = async (): Promise<boolean> => {
        return multipleRequiredConditionFunctions.filter(async (fun: (...args: any[]) => Promise<boolean>): Promise<boolean> => await Function.call(fun)).length > 0;
    };
    waitUntil(anyConditionMet, options)
}

/**
 * @param milliseconds delay duration
 * @param logDelay defaults to false; if true, logs a waiting message.
 */
export async function delay(milliseconds: number, logDelay = false): Promise<void> {
    if (logDelay) { console.log(`delaying ${milliseconds} milliseconds before continuing...`) }
    return new Promise((resolve) => setTimeout(resolve, milliseconds));
}

/**
 * Waits for the page url
 * @param partialUrl The string we are looking for in the url, to know we have transitioned to the correct page.
 * @param timeout the amount of milliseconds to wait before giving up.
 */
export async function waitForUrlPartialMatch(page: Page, firstPartialUrl: string, timeout = 120_000) {
    const startTime = Date.now();
    while (Date.now() - startTime < timeout) {
        if (page.url().includes(firstPartialUrl)) {
            return; // URL matches the partial string, exit the function
        }
        await page.waitForTimeout(100); // Wait for 100 milliseconds before checking again
    }
    throw new Error(`Timed out waiting for URL to match '${firstPartialUrl}'`);
}

/**
 * Run a function repeatedly until it returns true or the timeout is reached.
 *
 * This function executes the provided asynchronous block function in a loop until it returns true or the specified
 * timeout duration has elapsed. Between each attempt, it waits for a specified delay.
 *
 * @param block - An asynchronous function that returns a boolean value. This function will be executed repeatedly until it returns true.
 * @param timeout - The maximum duration to keep attempting to run the block function, in milliseconds. Default is 150000 (150 seconds).
 * @param delayMs - The delay duration between each attempt, in milliseconds. Default is 3000 (3 seconds).
 * @returns A promise that resolves to a boolean value indicating whether the block function eventually returned true.
 *
 * @example
 * // Example usage:
 * const blockFunction = async () => {
 *     // Some asynchronous condition check
 *     return await someConditionCheck();
 * };
 * const result = await runUntilTrue(blockFunction, 10000, 1000);
 * console.log(result); // Outputs true if blockFunction returned true within the timeout, otherwise false.
 */
export async function runUntilTrue(block: () => Promise<boolean>, timeout: number = 150000, delayMs: number = 3000){
    const startTime = DateTime.now();
    let attempts = 0;
    let evaluatesToTrue = false;

    do {
        // If timeout duration has elapsed, stop making attempts
        if (DateTime.now().diff(startTime).as('milliseconds') > timeout) {
            break;
        }

        attempts++;
        // If retrying, wait delay duration
        if (attempts > 1) await delay(delayMs);

        evaluatesToTrue = await block();

    } while (!evaluatesToTrue);

    return evaluatesToTrue;
}

// TODO move this to impl/utils/WaitingUtils when that related pr is available in devleop branch - T.S. 5/20/24
export async function waitForEither(block1: () => Promise<any>, block2: () => Promise<any>, timeOut: number = 180_000): Promise<void> {
    const startTime = Date.now();

    while (true) {
        try {
            const result1 = await block1();
            const result2 = await block2();

            // Check if either result is truthy (i.e., not falsy or undefined)
            if (result1 || result2) {
                // At least one block returned a truthy value, resolve the promise
                return;
            }
        } catch (error) {
            // Handle errors thrown by either block
            console.error("An error occurred:", error);
        }

        // Check if the timeout has been reached
        if (Date.now() - startTime >= timeOut) {
            throw new Error(`Timeout of ${timeOut} ms exceeded`);
        }

        // Add some delay before checking again
        await new Promise(resolve => setTimeout(resolve, 1000)); // Adjust delay as needed
    }
}

/**
 * Waits for a specific locator to show up on screen, then disappear. Typically used for things like progress bars.
 * @param locator The locator we want to become visible and then become hidden
 */
export async function waitToAppearAndDisappear(locator: Locator): Promise<void> {
    try {
        await expect(locator).toBeVisible({ timeout: 60000 });
        await expect(locator).toBeHidden({ timeout: 60000 });
    } catch (err) {
        if (err instanceof Error)
            console.log(LoggingUtils.logFunc(waitToAppearAndDisappear.name, err.message, false));
        else
            console.log(LoggingUtils.logFunc(waitToAppearAndDisappear.name, null, false));
    }
}