"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_MOBILE_FIND_CONFIG = exports.LocationStrategySelectorType = void 0;
const messaging_1 = require("@mablhq-internal/runtime/core/messaging/messaging");
const types_1 = require("../../../types");
const AndroidCandidate_1 = __importDefault(require("../android/AndroidCandidate"));
const elementUtil_1 = require("../elementUtil");
const iOSCandidate_1 = __importDefault(require("../ios/iOSCandidate"));
const FindStrategy_1 = __importStar(require("./FindStrategy"));
const async_retry_1 = __importDefault(require("async-retry"));
const PASSING_SCORE = 10;
const SELECTOR_DESCRIPTORS_PROPERTIES = {
    index: 'index',
    rect: 'rect',
    enabled: 'enabled',
    selected: 'selected',
    xpath: 'xpath',
    alternateXPaths: 'xpath',
    bounds: 'bounds',
    checkable: 'checkable',
    checked: 'checked',
    class: 'class',
    clickable: 'clickable',
    contentDesc: 'content-desc',
    displayed: 'displayed',
    focusable: 'focusable',
    focused: 'focused',
    hint: 'hint',
    longClickable: 'long-clickable',
    package: 'package',
    password: 'password',
    resourceId: 'resource-id',
    scrollable: 'scrollable',
    text: 'text',
    elementId: 'element-id',
    label: 'label',
    name: 'name',
    type: 'type',
    visible: 'visible',
    accessible: 'accessible',
    chain: 'chain',
    predicate: 'predicate',
    value: 'value',
};
var LocationStrategySelectorType;
(function (LocationStrategySelectorType) {
    LocationStrategySelectorType["Android"] = "android";
    LocationStrategySelectorType["iOS"] = "ios";
})(LocationStrategySelectorType || (exports.LocationStrategySelectorType = LocationStrategySelectorType = {}));
exports.DEFAULT_MOBILE_FIND_CONFIG = {
    PollPeriodMs: 200,
    WaitTimeoutMs: 600,
    ScrollGroupStrategyWaitTimeoutMs: 5_000,
    FindTimeoutMs: 15_000,
};
class LocatorStrategy extends FindStrategy_1.default {
    selectorType;
    locators;
    pollPeriodMs;
    waitTimeoutMs;
    matchers;
    shouldSkipRetries;
    constructor(selectorType, locators, pollPeriodMs, waitTimeoutMs, matchers, shouldSkipRetries) {
        super();
        this.selectorType = selectorType;
        this.locators = locators;
        this.pollPeriodMs = pollPeriodMs;
        this.waitTimeoutMs = waitTimeoutMs;
        this.matchers = matchers;
        this.shouldSkipRetries = shouldSkipRetries;
    }
    isApplicable(descriptor) {
        return super.isApplicable(descriptor);
    }
    async findElement(driver, mobileExecutionContext, descriptor, isShortCircuited) {
        const startTime = performance.now();
        const { configuration, selector } = descriptor.findTarget;
        const propertyPreferences = configuration?.propertyPreferences;
        if (selector.iOS && selector.android) {
            throw new Error('Expected only one platform type to be set.');
        }
        const platformSelector = (this.selectorType === LocationStrategySelectorType.Android
            ? selector.android
            : selector.iOS);
        if (!platformSelector) {
            throw new Error('Selector required but not found.');
        }
        while (true) {
            if (isShortCircuited()) {
                return undefined;
            }
            let locatedElements = new Set();
            for (const locator of this.locators) {
                let result = new Set();
                try {
                    await (0, async_retry_1.default)(async () => {
                        result = await locator.locate(driver, platformSelector, propertyPreferences);
                    }, {
                        retries: 2,
                        minTimeout: 100,
                        maxTimeout: 1_000,
                        maxRetryTime: 5_000,
                    });
                }
                catch (error) {
                    mobileExecutionContext.log.debug('Locating elements using properties ' +
                        locator.getPropertiesUsed(propertyPreferences) +
                        ' resulted in error: ' +
                        error.toString(), {
                        executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
                    });
                }
                locatedElements = (0, elementUtil_1.uniqueElementReferenceSet)([
                    ...locatedElements,
                    ...result,
                ]);
            }
            const candidateFromElementReferenceFn = this.selectorType === LocationStrategySelectorType.Android
                ? AndroidCandidate_1.default.fromElementReference
                : iOSCandidate_1.default.fromElementReference;
            const candidates = [...locatedElements].map((elementReference) => candidateFromElementReferenceFn(elementReference));
            const windowSize = await mobileExecutionContext.mobileTestEnvironment.getWindowSize();
            for (const candidate of candidates) {
                candidate.selector.screenSize = windowSize;
            }
            const visibleCandidates = candidates.filter((candidate) => {
                const isVisible = this.isCandidateFullyVisible(candidate.selector, windowSize);
                if (!isVisible) {
                    mobileExecutionContext.log.debug(`Filtering out ${this.getFoundSelectorDescription(candidate.selector)} because it is not fully visible in the viewport`, {
                        executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
                    });
                    return false;
                }
                return true;
            });
            const matchingCandidates = visibleCandidates.filter((candidate) => {
                if (this.selectorType === LocationStrategySelectorType.Android) {
                    return (candidate.selector.class ===
                        selector.android?.class);
                }
                return (candidate.selector.type === selector.iOS?.type);
            });
            let filteredCandidates = matchingCandidates;
            mobileExecutionContext.log.debug(`Filtered ${candidates.length} candidates to ${filteredCandidates.length} candidates`, {
                executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
            });
            if (!matchingCandidates.length && !!visibleCandidates.length) {
                mobileExecutionContext.log.debug(`No matching candidates. Falling back to using ${visibleCandidates.length} visible candidates`, {
                    executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
                });
                filteredCandidates = visibleCandidates;
            }
            if (filteredCandidates.length > 0) {
                const scoredCandidates = this.scoreCandidates(filteredCandidates, platformSelector, mobileExecutionContext, propertyPreferences, true);
                if (!!scoredCandidates) {
                    const correctedCandidates = await this.correctTopCandidateRectsAndRescore(scoredCandidates, driver, platformSelector, mobileExecutionContext, propertyPreferences);
                    const bestCandidate = this.getBestCandidate(correctedCandidates);
                    if (bestCandidate?.elementReference) {
                        const locatorProperties = [
                            ...new Set(this.locators
                                .flatMap((locator) => locator.getPropertiesUsed(propertyPreferences))
                                .map((property) => SELECTOR_DESCRIPTORS_PROPERTIES[property] ?? property)),
                        ];
                        mobileExecutionContext.log.info(`Found element with attributes "${this.getFoundSelectorDescription(bestCandidate?.selector)}" using the following properties: [${locatorProperties.join(', ')}]`, {
                            executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
                        });
                        return {
                            context: FindStrategy_1.NATIVE_CONTEXT,
                            elementReference: bestCandidate.elementReference,
                            scoredCandidates: correctedCandidates,
                        };
                    }
                }
            }
            const elapsed = performance.now() - startTime;
            mobileExecutionContext.log.debug(`Locator elapsed time: ${elapsed}ms`, {
                executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
            });
            if (elapsed > this.waitTimeoutMs) {
                mobileExecutionContext.log.debug(`Locator timed out`, {
                    executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
                });
                return undefined;
            }
            if (this.shouldSkipRetries) {
                mobileExecutionContext.log.debug(`Ran find only once because shouldSkipRetries is true. Element not found by this strategy.`, {
                    executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
                });
                return undefined;
            }
            await this.sleep(this.pollPeriodMs);
        }
    }
    scoreCandidates(candidates, selector, mobileExecutionContext, preferences, useOnlyImportantIfPossible = true) {
        if (useOnlyImportantIfPossible &&
            !!preferences &&
            Object.keys(preferences).length > 0) {
            useOnlyImportantIfPossible = false;
        }
        const { weightedMatchers, onlyImportant } = this.getWeightedMatchers(selector, preferences, useOnlyImportantIfPossible);
        if (onlyImportant) {
            mobileExecutionContext.log.debug(`Using only important attribute matchers for scoring.`, {
                executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
            });
        }
        mobileExecutionContext.log.debug(`Matched ${weightedMatchers.length} of ${this.matchers.length} matchers for scoring.`, {
            executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
        });
        if (weightedMatchers.length === 0) {
            return undefined;
        }
        for (const matcher of weightedMatchers) {
            matcher.adjustWeightBySpecificity(candidates, selector, preferences);
        }
        for (const candidate of candidates) {
            for (const matcher of weightedMatchers) {
                candidate.score += matcher.calculateMatchScore(selector, candidate.selector, preferences);
            }
        }
        candidates.sort((a, b) => b.score - a.score);
        if (onlyImportant && candidates.length > 1) {
            const firstCandidate = candidates[0];
            const secondCandidate = candidates[1];
            if (firstCandidate.score >= PASSING_SCORE &&
                firstCandidate.score === secondCandidate.score) {
                mobileExecutionContext.log.debug(`Top two candidates with the same score (${firstCandidate.score}). ` +
                    `Trying to score again including non-important attributes to break the tie.`, {
                    executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
                });
                return this.scoreCandidates(candidates, selector, mobileExecutionContext, preferences, false);
            }
        }
        const matchersLog = weightedMatchers
            .map((matcher) => `Attribute [${matcher.selectorKey.toLocaleString()}] - Initial weight: ${matcher.initialWeight}, Final Weight: ${matcher.getWeight()}`)
            .join('\n');
        mobileExecutionContext.log.debug(matchersLog, {
            executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
        });
        const candidatesLog = candidates
            .slice(0, 3)
            .map((candidate, index) => `Candidate ${index + 1} score: ${candidate.score}, Selector: ${JSON.stringify(candidate.selector)}`)
            .join('\n');
        mobileExecutionContext.log.debug(candidatesLog, {
            executionPhase: messaging_1.ExecutionPhase.DURING_ACTION,
        });
        return candidates;
    }
    getWeightedMatchers(selector, preferences, useOnlyImportantIfPossible) {
        const filteredMatchers = this.matchers.filter((m) => m.canMatchBy(selector, preferences));
        const importantMatchers = useOnlyImportantIfPossible
            ? filteredMatchers.filter((m) => m.important)
            : [];
        if (importantMatchers.length > 0) {
            return {
                weightedMatchers: importantMatchers,
                onlyImportant: true,
            };
        }
        return { weightedMatchers: filteredMatchers, onlyImportant: false };
    }
    getBestCandidate(scoredCandidates) {
        const bestCandidate = scoredCandidates[0];
        if (bestCandidate.score <= PASSING_SCORE) {
            return undefined;
        }
        return bestCandidate;
    }
    isCandidateFullyVisible(candidateSelector, windowSize) {
        const { rect } = candidateSelector;
        if (!rect) {
            return false;
        }
        const { x, y, height, width } = rect;
        return (x <= windowSize.width &&
            y <= windowSize.height &&
            x + width > 0 &&
            y + height > 0);
    }
    getFoundSelectorDescription(foundSelector) {
        return Object.entries(SELECTOR_DESCRIPTORS_PROPERTIES)
            .map(([key, value]) => {
            const foundSelectorRecords = foundSelector;
            if (foundSelectorRecords[key] !== undefined) {
                if (typeof foundSelectorRecords[key] === 'object') {
                    return `${value} = ${JSON.stringify(foundSelectorRecords[key])}`;
                }
                return `${value} = ${foundSelectorRecords[key]}`;
            }
            return;
        })
            .filter((value) => value !== undefined)
            .join(', ');
    }
    async correctTopCandidateRectsAndRescore(scoredCandidates, driver, selector, mobileExecutionContext, preferences) {
        if (this.selectorType !== LocationStrategySelectorType.iOS) {
            return scoredCandidates;
        }
        const isIPad = await mobileExecutionContext.mobileTestEnvironment.isIPadDevice();
        if (!isIPad) {
            return scoredCandidates;
        }
        const TOP_N = 5;
        const topCandidates = scoredCandidates.slice(0, TOP_N);
        for (let i = 0; i < topCandidates.length; i++) {
            const candidate = topCandidates[i];
            const elementId = candidate.elementReference?.[types_1.MOBILE_ELEMENT_IDENTIFIER_PROPERTY];
            if (!elementId) {
                continue;
            }
            try {
                const accurateRect = await driver.getElementRect(elementId);
                const attributeRect = candidate.selector.rect;
                const rectsMatch = attributeRect &&
                    attributeRect.x === accurateRect.x &&
                    attributeRect.y === accurateRect.y &&
                    attributeRect.width === accurateRect.width &&
                    attributeRect.height === accurateRect.height;
                if (i === 0 && rectsMatch) {
                    return scoredCandidates;
                }
                if (!rectsMatch) {
                    candidate.selector.rect = accurateRect;
                    if (candidate.elementReference) {
                        candidate.elementReference.rect = accurateRect;
                    }
                }
            }
            catch (error) {
                mobileExecutionContext.log.debug(`Failed to get accurate rect for candidate, keeping attribute rect: ${String(error)}`, { executionPhase: messaging_1.ExecutionPhase.DURING_ACTION });
                if (i === 0) {
                    return scoredCandidates;
                }
            }
        }
        for (const candidate of topCandidates) {
            candidate.score = 0;
        }
        const rescoredTop = this.scoreCandidates(topCandidates, selector, mobileExecutionContext, preferences, true);
        if (!rescoredTop) {
            return scoredCandidates;
        }
        const remaining = scoredCandidates.slice(TOP_N);
        return [...rescoredTop, ...remaining];
    }
}
exports.default = LocatorStrategy;
