"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EnterTextExecutor = void 0;
exports.performEnterTextAction = performEnterTextAction;
exports.clearTextFromActiveElement = clearTextFromActiveElement;
const sleep_1 = require("@mablhq-internal/runtime/util/sleep");
const messaging_1 = require("@mablhq-internal/runtime/core/messaging/messaging");
const mablApi_1 = require("@mablhq-internal/runtime/mablApi");
const TapStepDescriptor_1 = require("@mablhq/test-model/types/mobile/TapStepDescriptor");
const execution_1 = require("@mablhq-internal/runtime/execution");
const execution_2 = require("@mablhq-internal/runtime/execution");
const AndroidCandidate_1 = require("../actions/find/android/AndroidCandidate");
const iOSCandidate_1 = require("../actions/find/ios/iOSCandidate");
const types_1 = require("../types");
const TapExecutor_1 = require("./TapExecutor");
const androidKeyboard_1 = require("./util/androidKeyboard");
const iOSKeyboard_1 = require("./util/iOSKeyboard");
const async_retry_1 = __importDefault(require("async-retry"));
const WAIT_TIME_AFTER_CLEARING_TEXT_MS = 300;
const KEYBOARD_MAX_WAIT_TIME = 20_000;
const CLEAR_TEXT_TRY_COUNT = 3;
const CLEAR_TEXT_MIN_TIMEOUT_MS = 200;
const CLEAR_TEXT_MAX_TIMEOUT_MS = 2_000;
const CLEAR_TEXT_MAX_RETRY_TIME_MS = 5_000;
class EnterTextExecutor {
    run(step, executionContext) {
        return (0, execution_1.runActionWithElementReadyRetriesForMobile)(() => this.enterText(step, executionContext), executionContext);
    }
    async enterText(stepDescriptor, executionContext) {
        const { mobileTestEnvironment } = executionContext;
        const currentTargetOS = mobileTestEnvironment.getConfig().app.platformName;
        const driver = executionContext.mobileTestEnvironment.getDriver();
        const text = `${executionContext.variables.resolve(stepDescriptor.text)}`;
        let boundingBox;
        try {
            if (stepDescriptor.find) {
                const tapExecutor = new TapExecutor_1.TapExecutor();
                const result = await tapExecutor.run({
                    find: stepDescriptor.find,
                    type: TapStepDescriptor_1.TapType.tap,
                    actionCode: stepDescriptor.actionCode,
                }, executionContext);
                if (result.status !== execution_2.StepStatus.Passed) {
                    return result;
                }
                boundingBox = result.boundingBox;
            }
            executionContext.log.info('Waiting for the native keyboard to be displayed...', {
                executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
            });
            await waitUntilKeyboardIsDisplayed(driver, KEYBOARD_MAX_WAIT_TIME, executionContext.log);
            await executionContext.observeDeferredFunction();
            if (stepDescriptor.clearExistingText) {
                await clearTextFromActiveElement(driver, executionContext);
            }
            if (text !== '') {
                await performEnterTextAction(driver, currentTargetOS, text, stepDescriptor.sendCharByChar, stepDescriptor.useNativeKeyboard, stepDescriptor.doNotRetry, stepDescriptor.useSetValue, executionContext?.log);
            }
            else {
                executionContext.log.debug('No text to enter. Skipping...', {
                    executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
                });
            }
        }
        catch (error) {
            executionContext.log.error(error);
            return {
                status: execution_2.StepStatus.Failed,
                logInfo: 'Unable to enter text',
            };
        }
        return { status: execution_2.StepStatus.Passed, boundingBox };
    }
}
exports.EnterTextExecutor = EnterTextExecutor;
async function setActiveElementValue(driver, text, existingText) {
    const activeElementReference = await getActiveElementReference(driver);
    if (activeElementReference === undefined) {
        return false;
    }
    try {
        const activeElement = await driver.$(activeElementReference).getElement();
        await activeElement.setValue(existingText + text);
    }
    catch (error) {
        return false;
    }
    return true;
}
async function sendTextOnIOS(driver, text, existingText, useSetValue) {
    if (useSetValue) {
        if (await setActiveElementValue(driver, text, existingText)) {
            return;
        }
    }
    return driver.sendKeys([text]);
}
async function sendTextOnAndroid(driver, text, existingText, useSetValue) {
    if (useSetValue) {
        if (await setActiveElementValue(driver, text, existingText)) {
            return;
        }
    }
    return driver.execute('mobile: type', { text });
}
async function sendTextCharByChar(driver, platform, text) {
    const DELAY_BETWEEN_CHARS_MS = 100;
    let sendText;
    switch (platform) {
        case mablApi_1.MobilePlatformEnum.Ios:
            sendText = sendTextOnIOS;
            break;
        case mablApi_1.MobilePlatformEnum.Android:
            sendText = sendTextOnAndroid;
            break;
        default:
            throw new Error(`Unsupported platform: ${platform}`);
    }
    for (const char of text) {
        await sendText(driver, char, '', false);
        await (0, sleep_1.sleep)(DELAY_BETWEEN_CHARS_MS);
    }
}
async function performEnterTextAction(driver, platform, text, sendCharByChar, useNativeKeyboard, doNotRetry, useSetValue, logger) {
    let sendKeysThroughNativeKeyboard;
    let sendText;
    switch (platform) {
        case mablApi_1.MobilePlatformEnum.Ios:
            sendKeysThroughNativeKeyboard = iOSKeyboard_1.sendKeysThroughNativeiOSKeyboard;
            sendText = sendTextOnIOS;
            break;
        case mablApi_1.MobilePlatformEnum.Android:
            sendKeysThroughNativeKeyboard = androidKeyboard_1.sendKeysThroughNativeAndroidKeyboard;
            sendText = sendTextOnAndroid;
            break;
        default:
            throw new Error(`Unsupported platform: ${platform}`);
    }
    if (useNativeKeyboard) {
        return sendKeysThroughNativeKeyboard(driver, text);
    }
    if (sendCharByChar) {
        return sendTextCharByChar(driver, platform, text);
    }
    const initialActiveElementReference = await getActiveElementReference(driver);
    let existingText = getElementText(initialActiveElementReference);
    if (doNotRetry ||
        !initialActiveElementReference ||
        existingText === undefined) {
        return sendText(driver, text, '', false);
    }
    const MAX_ENTER_TEXT_ATTEMPTS = 3;
    let currentAttempt = 0;
    let isTextValid = false;
    while (!isTextValid && currentAttempt < MAX_ENTER_TEXT_ATTEMPTS) {
        await sendText(driver, text, existingText, !!useSetValue);
        const activeElementReference = await getActiveElementReference(driver);
        if (!activeElementReference) {
            logger?.warning(`Could not get the active element reference. Skipping text entry validation.`, {
                executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
            });
            return;
        }
        if (initialActiveElementReference[types_1.MOBILE_ELEMENT_IDENTIFIER_PROPERTY] !==
            activeElementReference[types_1.MOBILE_ELEMENT_IDENTIFIER_PROPERTY]) {
            logger?.warning(`The active element has changed. Skipping text entry validation.`, {
                executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
            });
            return;
        }
        if (currentAttempt === 0 &&
            hadHintAsTextValue(activeElementReference, existingText)) {
            existingText = '';
        }
        isTextValid = validateEnteredText(activeElementReference, currentAttempt === 0 && existingText ? existingText + text : text);
        if (!isTextValid) {
            await clearTextFromActiveElement(driver);
            if (currentAttempt === 0 && existingText) {
                text = existingText + text;
            }
        }
        ++currentAttempt;
    }
    if (!isTextValid && !useSetValue) {
        await sendTextCharByChar(driver, platform, text);
    }
}
async function clearTextFromActiveElement(driver, executionContext) {
    try {
        await (0, async_retry_1.default)(async () => {
            if (!(await setActiveElementValue(driver, '', ''))) {
                throw new Error();
            }
            await (0, sleep_1.sleep)(WAIT_TIME_AFTER_CLEARING_TEXT_MS);
        }, {
            retries: CLEAR_TEXT_TRY_COUNT,
            minTimeout: CLEAR_TEXT_MIN_TIMEOUT_MS,
            maxTimeout: CLEAR_TEXT_MAX_TIMEOUT_MS,
            maxRetryTime: CLEAR_TEXT_MAX_RETRY_TIME_MS,
        });
    }
    catch (error) {
        executionContext?.log.warning(`Could not clear text on active element`, {
            executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
        });
    }
}
async function getActiveElementReference(driver) {
    const activeElementReference = (await driver.getActiveElement());
    if (!activeElementReference?.[types_1.MOBILE_ELEMENT_IDENTIFIER_PROPERTY]) {
        return undefined;
    }
    return activeElementReference;
}
function validateEnteredText(activeElementReference, text) {
    if ((0, AndroidCandidate_1.isAndroidElementReference)(activeElementReference)) {
        return (activeElementReference['attribute/password'] === 'true' ||
            activeElementReference.text === text);
    }
    if ((0, iOSCandidate_1.isIOSElementReference)(activeElementReference)) {
        return (activeElementReference.type === types_1.XCUIElementType.secureTextField ||
            activeElementReference['attribute/value'] === text);
    }
    throw new Error(`Unsupported active element: ${activeElementReference}`);
}
function getElementText(elementReference) {
    if (!elementReference) {
        return undefined;
    }
    if ((0, AndroidCandidate_1.isAndroidElementReference)(elementReference)) {
        if (!elementReference['attribute/class'] ||
            !types_1.MOBILE_TEXT_FIELD_TYPES.includes(elementReference['attribute/class'])) {
            return undefined;
        }
        return elementReference.text === elementReference['attribute/hint'] ||
            elementReference['attribute/password'] === 'true'
            ? ''
            : elementReference.text ?? '';
    }
    if ((0, iOSCandidate_1.isIOSElementReference)(elementReference)) {
        if (!elementReference.type ||
            !types_1.MOBILE_TEXT_FIELD_TYPES.includes(elementReference.type)) {
            return undefined;
        }
        return elementReference.type === types_1.XCUIElementType.secureTextField
            ?
                ''
            : elementReference['attribute/value'] ?? '';
    }
    throw new Error(`Unsupported active element: ${elementReference}`);
}
function hadHintAsTextValue(elementReference, existingText) {
    if (!existingText) {
        return false;
    }
    if ((0, AndroidCandidate_1.isAndroidElementReference)(elementReference)) {
        const elementText = elementReference.text ?? '';
        return (existingText === elementReference['attribute/hint'] ||
            !elementText.startsWith(existingText));
    }
    if ((0, iOSCandidate_1.isIOSElementReference)(elementReference)) {
        const elementText = elementReference['attribute/value'] ?? '';
        return !elementText.startsWith(existingText);
    }
    throw new Error(`Unsupported element: ${elementReference}`);
}
async function waitUntilKeyboardIsDisplayed(driver, keyboardMaxWaitTime, logger) {
    const startTime = Date.now();
    let isKeyboardVisible = false;
    do {
        try {
            isKeyboardVisible = await driver.isKeyboardShown();
        }
        catch (error) {
            logger?.debug('Error checking if keyboard is visible');
        }
        if (!isKeyboardVisible) {
            await (0, sleep_1.sleep)(100);
        }
    } while (!isKeyboardVisible && Date.now() - startTime < keyboardMaxWaitTime);
}
