"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FindMobileElement = void 0;
exports.isWebViewElementHandle = isWebViewElementHandle;
const mablApi_1 = require("@mablhq-internal/runtime/mablApi");
const mablscriptFind_1 = require("@mablhq/test-model/mablscriptFind");
const execution_1 = require("@mablhq-internal/runtime/execution");
const AndroidStrategy_1 = __importDefault(require("./find/android/AndroidStrategy"));
const iOSStrategy_1 = __importDefault(require("./find/ios/iOSStrategy"));
const FindStrategy_1 = require("./find/strategies/FindStrategy");
const stepUtil_1 = require("@mablhq/test-model/mobile/steps/stepUtil");
const MobileTrainingActions_1 = require("@mablhq-internal/runtime/core/messaging/actions/MobileTrainingActions");
const asyncUtil_1 = require("@mablhq-internal/runtime/util/asyncUtil");
const Globals_1 = require("@mablhq-internal/runtime/Globals");
const LocatorStrategy_1 = require("./find/strategies/LocatorStrategy");
const messaging_1 = require("@mablhq-internal/runtime/core/messaging/messaging");
const sleep_1 = require("@mablhq-internal/runtime/util/sleep");
const pageSourceUtil_1 = require("../util/pageSourceUtil");
const types_1 = require("../types");
const MAX_STATIC_PAGE_SOURCE_WAIT_ATTEMPTS = 3;
function isWebViewElementHandle(handle) {
    return (handle.context?.contextType === FindStrategy_1.MobileContextType.androidWebView ||
        handle.context?.contextType === FindStrategy_1.MobileContextType.iOSWebView);
}
class FindMobileElement {
    static async run(stepWithFindDescriptor, executionContext, findHintGenerator, disableCache) {
        return (0, asyncUtil_1.promiseWithTimeout)(FindMobileElement.runImpl(stepWithFindDescriptor, executionContext, findHintGenerator, disableCache), Globals_1.Globals.getFindOverallTimeoutMs(), execution_1.FIND_TIMEOUT_MESSAGE, execution_1.FIND_TIMEOUT_TYPE);
    }
    static async runImpl(stepWithFindDescriptor, executionContext, findHintGenerator, _disableCache) {
        const { find: findDescriptor } = stepWithFindDescriptor;
        switch (findDescriptor.findType) {
            case mablscriptFind_1.FindType.FIND_ONE:
                return FindMobileElement.normalizeDescriptorAndFindOneElement(findDescriptor, executionContext, findHintGenerator);
            case mablscriptFind_1.FindType.FIND_FIRST:
            case mablscriptFind_1.FindType.FIND_LAST:
                return FindMobileElement.runCustomFind(findDescriptor, executionContext);
            default:
                throw new Error(`Unimplemented mobile find type: [${findDescriptor.findType}]`);
        }
    }
    static emitFindBoxInfo(executionContext, foundElement, isACandidateElement) {
        try {
            MobileTrainingActions_1.MobileTrainingActionsEventEmitter.emitFoundElementInfo(foundElement, isACandidateElement);
        }
        catch (error) {
            executionContext.log.error('Unable to emit found element information back to channel', error);
        }
    }
    static async runCustomFind(findDescriptor, executionContext) {
        const driver = executionContext.mobileTestEnvironment.getDriver();
        const xpath = executionContext.variables.resolveToString(findDescriptor.findTarget.xpath);
        executionContext.log.info(`Looking for element with xpath: ${xpath}`, {
            executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
        });
        const foundElement = await driver.$(xpath).getElement();
        if (foundElement?.error) {
            executionContext.log.info(`Unable to find element with xpath: ${xpath}`, {
                executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
            });
            return {
                mobileElement: undefined,
                context: FindStrategy_1.NATIVE_CONTEXT,
            };
        }
        return {
            mobileElement: foundElement,
            context: FindStrategy_1.NATIVE_CONTEXT,
        };
    }
    static async normalizeDescriptorAndFindOneElement(findDescriptor, executionContext, findHintGenerator, maxFindExecutionTimeMs = LocatorStrategy_1.DEFAULT_MOBILE_FIND_CONFIG.FindTimeoutMs) {
        const normalizedDescriptor = getNormalizedMobileFindElementDescriptor(findDescriptor, executionContext.findContext, findHintGenerator);
        mapHintsToMobileFindDescriptor(normalizedDescriptor.findTarget);
        const resolvedDescriptor = JSON.parse(executionContext.variables.resolveToString(JSON.stringify(normalizedDescriptor)));
        const result = await FindMobileElement.findElement(resolvedDescriptor, executionContext, maxFindExecutionTimeMs);
        FindMobileElement.emitFindBoxInfo(executionContext, result, false);
        return result;
    }
    static async findElement(findDescriptor, executionContext, maxFindExecutionTimeMs = LocatorStrategy_1.DEFAULT_MOBILE_FIND_CONFIG.FindTimeoutMs) {
        let foundElement;
        let mobileFindStrategyResult;
        const currentTargetOS = executionContext.mobileTestEnvironment.getConfig().app.platformName;
        const driver = executionContext.mobileTestEnvironment.getDriver();
        const primaryFindMethod = findDescriptor.findOptions?.primaryFindMethod;
        const onlyFindByXPath = !!findDescriptor.findOptions?.onlyFindByXPath ||
            primaryFindMethod === mablscriptFind_1.PrimaryFindMethod.XPATH_ONLY;
        if (onlyFindByXPath) {
            executionContext.log.info(`Using XPath for finding the target element`, {
                executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
            });
        }
        const { selector } = findDescriptor.findTarget;
        let findExecutionComplete = false;
        const checkShortCircuit = async () => {
            while (!findExecutionComplete) {
                if (executionContext.isFindExecutionShortCircuited()) {
                    return undefined;
                }
                await new Promise((resolve) => global.setTimeout(resolve, 200));
            }
            return;
        };
        const isShortCircuited = () => executionContext.isFindExecutionShortCircuited();
        const windowSize = await executionContext.mobileTestEnvironment.getWindowSize();
        const isRecordingWhileTrainingMode = executionContext.isRecordingWhileTrainingMode();
        const isWaitUntilFind = (0, mablscriptFind_1.findIsWaitUntil)(findDescriptor.findTarget);
        let strategy;
        executionContext.log.info(`Looking for ${(0, stepUtil_1.getElementDescriptionFromMobileFindDescriptor)(findDescriptor)}${isWaitUntilFind ? ' using WaitUntil' : ''}`, {
            executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
        });
        const platform = executionContext.mobileTestEnvironment.getConfig().app.platformName;
        if (platform === mablApi_1.MobilePlatformEnum.Android) {
            const waitType = primaryFindMethod === mablscriptFind_1.PrimaryFindMethod.ELEMENT_DESCRIPTION
                ? PageSourceStabilityWaitType.STATIC
                : PageSourceStabilityWaitType.PAGE_SOURCE_CHECK;
            try {
                await waitForAndroidPageSourceStability(executionContext, MAX_STATIC_PAGE_SOURCE_WAIT_ATTEMPTS, waitType);
            }
            catch (error) {
                const timeoutError = error;
                if (timeoutError.message) {
                    executionContext.log.warning(timeoutError.message, {
                        executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
                    });
                }
            }
        }
        switch (currentTargetOS) {
            case mablApi_1.MobilePlatformEnum.Ios:
                strategy = new iOSStrategy_1.default({
                    isRecordingWhileTrainingMode,
                    isWaitUntilFind,
                    maxFindExecutionTimeMs,
                    onlyFindByXPath,
                    platform: mablApi_1.MobilePlatformEnum.Ios,
                    primaryFindMethod,
                    windowSize,
                });
                if (selector.iOS) {
                    mobileFindStrategyResult = await Promise.race([
                        strategy.findElement(driver, executionContext, findDescriptor, isShortCircuited),
                        checkShortCircuit(),
                    ]);
                    findExecutionComplete = true;
                    break;
                }
                else if (selector.android) {
                    break;
                }
                break;
            case mablApi_1.MobilePlatformEnum.Android:
                strategy = new AndroidStrategy_1.default({
                    isRecordingWhileTrainingMode,
                    isWaitUntilFind,
                    maxFindExecutionTimeMs,
                    onlyFindByXPath,
                    platform: mablApi_1.MobilePlatformEnum.Android,
                    primaryFindMethod,
                    windowSize,
                });
                if (selector.android) {
                    mobileFindStrategyResult = await Promise.race([
                        strategy.findElement(driver, executionContext, findDescriptor, isShortCircuited),
                        checkShortCircuit(),
                    ]);
                    findExecutionComplete = true;
                    break;
                }
                else if (selector.iOS) {
                    break;
                }
                break;
            default:
                executionContext.log.error(`Could not run find, insufficient find info supplied`);
                break;
        }
        if (!mobileFindStrategyResult) {
            return {
                mobileElement: undefined,
                context: FindStrategy_1.NATIVE_CONTEXT,
            };
        }
        if (mobileFindStrategyResult.elementReference) {
            foundElement = await driver
                .$(mobileFindStrategyResult.elementReference)
                .getElement();
        }
        let boundingBox = mobileFindStrategyResult.visualFindBoundingBox;
        if (!boundingBox && foundElement?.elementId) {
            const isIPad = await executionContext.mobileTestEnvironment.isIPadDevice();
            if (isIPad) {
                try {
                    boundingBox = await driver.getElementRect(foundElement.elementId);
                }
                catch (error) {
                    executionContext.log.warning(`Failed to get element rect from driver, falling back to attribute rect: ${String(error)}`, { executionPhase: messaging_1.ExecutionPhase.DURING_ACTION });
                }
            }
        }
        if (!boundingBox) {
            boundingBox = mobileFindStrategyResult.elementReference?.rect;
        }
        if (!boundingBox) {
            executionContext.log.warning('No bounding box found for element', {
                executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
            });
        }
        return {
            mobileElement: foundElement,
            boundingBox,
            context: mobileFindStrategyResult?.context,
            webViewElementBoundingClientRect: mobileFindStrategyResult?.webViewElementBoundingClientRect,
            webViewElementSelector: mobileFindStrategyResult?.webViewElementSelector,
        };
    }
    static async runMultipleResultFind(findDescriptor, executionContext) {
        if (!(0, mablscriptFind_1.isMobileFindCustomDescriptor)(findDescriptor)) {
            throw new Error(`Cannot query for multiple elements with current find descriptor`);
        }
        const driver = executionContext.mobileTestEnvironment.getDriver();
        const { xpath } = findDescriptor.findTarget;
        executionContext.log.info(`Looking for elements with xpath: ${xpath}`, {
            executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
        });
        const foundElements = await driver.$$(xpath).getElements();
        executionContext.log.info(`Found ${foundElements?.length ?? 0} matching elements`, {
            executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
        });
        return {
            elements: foundElements,
        };
    }
    static isWebdriverIOElement(value) {
        return (value &&
            typeof value === 'object' &&
            typeof value.sessionId === 'string' &&
            typeof value.elementId === 'string' &&
            typeof value[types_1.MOBILE_ELEMENT_IDENTIFIER_PROPERTY] === 'string' &&
            typeof value.$ === 'function');
    }
}
exports.FindMobileElement = FindMobileElement;
function getNormalizedMobileFindElementDescriptor(find, findContext, findHintGenerator) {
    const findSpecification = {
        ...find.findTarget,
        findOptions: find.findTarget.findOptions ?? find.findOptions,
        overrides: findContext.findOverrides.get(find.findTarget.selector.uuid)
            ?.selector_overrides || [],
        findModel: findContext.findModels.get(find.findTarget.selector.uuid),
        type: find.findType,
    };
    if (findHintGenerator) {
        let hints;
        if (findHintGenerator instanceof Function) {
            hints = findHintGenerator(findSpecification.selector.uuid);
        }
        else {
            hints = findHintGenerator;
        }
        findSpecification.findOptions = { ...findSpecification.findOptions, hints };
    }
    if (findSpecification.auxiliaryDescriptors?.length) {
        findSpecification.auxiliaryDescriptors.forEach((auxiliaryDescriptor) => {
            auxiliaryDescriptor.overrides =
                findContext.findOverrides.get(auxiliaryDescriptor.selector.uuid)
                    ?.selector_overrides || [];
            auxiliaryDescriptor.findModel = findContext.findModels.get(auxiliaryDescriptor.selector.uuid);
            auxiliaryDescriptor.type = find.findType;
        });
    }
    return { ...find, findTarget: findSpecification };
}
function mapHintsToMobileFindDescriptor(findSpecification) {
    if (findSpecification.findOptions?.hints?.selectorSubstitutions) {
        const allSelectorSubstitutions = findSpecification.findOptions.hints.selectorSubstitutions;
        const selectorSubstitutions = allSelectorSubstitutions[findSpecification.selector.uuid];
        if (selectorSubstitutions) {
            const { selector } = findSpecification;
            applySelectorSubstitutionsToSelector(selector, selectorSubstitutions);
            if (findSpecification.overrides) {
                for (const override of findSpecification.overrides) {
                    applySelectorSubstitutionsToSelector(override.selector, selectorSubstitutions);
                }
            }
        }
    }
    return findSpecification;
}
function applySelectorSubstitutionsToSelector(selector, selectorSubstitutions) {
    if (!!selector.iOS) {
        if (selector.iOS.label === selector.iOS.name &&
            (!!selectorSubstitutions.label || !!selectorSubstitutions.name)) {
            const text = selectorSubstitutions.label ?? selectorSubstitutions.name;
            selectorSubstitutions.label = text;
            selectorSubstitutions.name = text;
        }
        selector.iOS = { ...selector.iOS, ...selectorSubstitutions };
    }
    else {
        selector.android = { ...selector.android, ...selectorSubstitutions };
    }
}
var PageSourceStabilityWaitType;
(function (PageSourceStabilityWaitType) {
    PageSourceStabilityWaitType[PageSourceStabilityWaitType["STATIC"] = 0] = "STATIC";
    PageSourceStabilityWaitType[PageSourceStabilityWaitType["PAGE_SOURCE_CHECK"] = 1] = "PAGE_SOURCE_CHECK";
})(PageSourceStabilityWaitType || (PageSourceStabilityWaitType = {}));
const PAGE_SOURCE_STABILITY_TIMEOUT_MS = 60_000;
async function waitForAndroidPageSourceStability(executionContext, maxAttempts, waitType) {
    if (waitType === PageSourceStabilityWaitType.STATIC) {
        await (0, sleep_1.sleep)(1000);
        return;
    }
    return (0, asyncUtil_1.promiseWithTimeout)(waitForAndroidPageSourceToStopChanging(executionContext, maxAttempts), PAGE_SOURCE_STABILITY_TIMEOUT_MS, 'Timed out waiting for the page to stabilize.');
}
async function waitForAndroidPageSourceToStopChanging(executionContext, maxAttempts) {
    const platform = mablApi_1.MobilePlatformEnum.Android;
    const driver = executionContext.mobileTestEnvironment.getDriver();
    let checkAttempts = 1;
    let previousPageSource = '';
    let currentPageSource = await (0, pageSourceUtil_1.getLimitedPageSource)(driver, platform);
    do {
        previousPageSource = currentPageSource;
        await (0, sleep_1.sleep)(1000);
        currentPageSource = await (0, pageSourceUtil_1.getLimitedPageSource)(driver, platform);
        if (previousPageSource === currentPageSource) {
            executionContext.log.info(`Page source did not change after attempt ${checkAttempts}, assuming stable`, {
                executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
            });
            break;
        }
        checkAttempts++;
    } while (checkAttempts <= maxAttempts);
}
