"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PuppeteerPageHooker = void 0;
const debug_1 = __importDefault(require("debug"));
const utils_1 = require("./utils");
const puppeteer_selector_mapping_1 = require("./utils/puppeteer-selector-mapping");
const puppeteer_text_mapping_1 = require("./utils/puppeteer-text-mapping");
const loggerError = debug_1.default('root-cause:error');
const omittedPageMethods = [
    'then',
    'setViewport',
    'screenshot',
    'close',
    'removeListener',
    'constructor',
    'viewport',
];
const returningElementHandlesViaPromise = new Set(['$', '$$', 'waitForSelector', 'evaluateHandle']);
const flatGetters = new Set(['keyboard', 'mouse']);
const gettersViaFunction = new Set(['frames', 'asElement', 'mainFrame']);
class PuppeteerPageHooker {
    constructor(testContext, rootPage) {
        this.testContext = testContext;
        this.rootPage = rootPage;
        this.paused = false;
        this.wrappedEventsHandlersRegistry = new WeakMap();
        this.pageIdsCounter = 0;
        this.beforeAllHooks = [];
        this.afterAllHooks = [];
        this.beforeHooks = [];
        this.afterHooks = [];
        this.waitForEventMiddleman = async (target, eventName, optionsOrPredicate) => {
            if (eventName === 'popup') {
                const newPage = await target.waitForEvent(eventName, optionsOrPredicate);
                return this.innerWrapWithProxy(newPage, [], ++this.pageIdsCounter);
            }
            return target.waitForEvent(eventName, optionsOrPredicate);
        };
        this.onMethodMiddleman = (target, eventName, userEventHandler) => {
            if (eventName !== 'popup') {
                return target.on(eventName, userEventHandler);
            }
            const ourPopupHandler = (newPage) => {
                const wrappedPage = this.innerWrapWithProxy(newPage, [], ++this.pageIdsCounter);
                userEventHandler(wrappedPage);
            };
            this.addAssociatedEventHandler(target, 'popup', userEventHandler, ourPopupHandler);
            return target.on('popup', ourPopupHandler);
        };
        this.offMethodMiddleman = (target, eventName, userEventHandler) => {
            const actualOurHandler = this.getAssociatedEventHandler(target, eventName, userEventHandler);
            if (actualOurHandler) {
                return target.off(eventName, actualOurHandler);
            }
            return target.off(eventName, userEventHandler);
        };
        this.onceMethodMiddleman = (target, eventName, userEventHandler) => {
            if (eventName !== 'popup') {
                return target.once(eventName, handler);
            }
            const ourOncePopupHandler = (newPage) => {
                const wrappedPage = this.innerWrapWithProxy(newPage, [], ++this.pageIdsCounter);
                userEventHandler(wrappedPage);
            };
            this.addAssociatedEventHandler(target, 'popup', userEventHandler, ourOncePopupHandler);
            return target.once(eventName, ourOncePopupHandler);
        };
    }
    pause() {
        this.paused = true;
    }
    resume() {
        this.paused = false;
    }
    registerBeforeAllHook(hook) {
        this.beforeAllHooks.push(hook);
    }
    registerAfterAllHook(hook) {
        this.afterAllHooks.push(hook);
    }
    registerBeforeHook(hook) {
        this.beforeHooks.push(hook);
    }
    registerAfterHook(hook) {
        this.afterHooks.push(hook);
    }
    wrapWithProxy(proxiedObject) {
        return this.innerWrapWithProxy(proxiedObject, [], 0);
    }
    innerWrapWithProxy(proxiedObject, methodCallData, pageId) {
        const pauseStateHolder = this;
        const pageHookerThis = this;
        const handler = {
            get(target, prop, receiver) {
                if (utils_1.isRootCausePage(target)) {
                    if (prop === 'on' || prop === 'addListener') {
                        return pageHookerThis.onMethodMiddleman.bind(pageHookerThis, target);
                    }
                    if (prop === 'off' || prop === 'removeListener') {
                        return pageHookerThis.offMethodMiddleman.bind(pageHookerThis, target);
                    }
                    if (prop === 'once') {
                        return pageHookerThis.onceMethodMiddleman.bind(pageHookerThis, target);
                    }
                    if (prop === 'waitForEvent') {
                        if (!('waitForEvent' in target)) {
                            return undefined;
                        }
                        return pageHookerThis.waitForEventMiddleman.bind(pageHookerThis, target);
                    }
                }
                const reflectedProperty = Reflect.get(target, prop, receiver);
                const accessedPropAsString = prop.toString();
                if (accessedPropAsString.startsWith('_') ||
                    omittedPageMethods.includes(accessedPropAsString) ||
                    pauseStateHolder.paused) {
                    return reflectedProperty;
                }
                if (flatGetters.has(accessedPropAsString)) {
                    return pageHookerThis.wrapWithProxy(reflectedProperty);
                }
                if (gettersViaFunction.has(accessedPropAsString)) {
                    const method = reflectedProperty;
                    return function gettersViaFunctionWrapped(...args) {
                        const result = method.apply(target, args);
                        const newMethodCallData = {
                            selector: puppeteer_selector_mapping_1.extractPuppeteerSelector(proxiedObject, accessedPropAsString, args),
                            creationFunction: accessedPropAsString,
                            text: puppeteer_text_mapping_1.extractPuppeteerText(proxiedObject, accessedPropAsString, args, result),
                            pageId,
                        };
                        return pageHookerThis.wrapReturnValueInProxy(result, methodCallData, newMethodCallData, pageId);
                    };
                }
                if (returningElementHandlesViaPromise.has(accessedPropAsString)) {
                    return utils_1.appendToFunctionName(async function returningElementHandlesViaPromiseWrappedFunction(...args) {
                        const result = await pageHookerThis.makeStep(proxiedObject, target, reflectedProperty, accessedPropAsString, args, methodCallData, pageId);
                        if (!result) {
                            return result;
                        }
                        const newMethodCallData = {
                            selector: puppeteer_selector_mapping_1.extractPuppeteerSelector(proxiedObject, accessedPropAsString, args),
                            creationFunction: accessedPropAsString,
                            text: puppeteer_text_mapping_1.extractPuppeteerText(proxiedObject, accessedPropAsString, args, result),
                            pageId,
                        };
                        return pageHookerThis.wrapReturnValueInProxy(result, methodCallData, newMethodCallData, pageId);
                    }, `_${proxiedObject.constructor.name}_${accessedPropAsString}`);
                }
                if (typeof reflectedProperty !== 'function') {
                    return reflectedProperty;
                }
                return utils_1.appendToFunctionName(async function rootCauseWrappedFunction(...args) {
                    return await pageHookerThis.makeStep(proxiedObject, target, reflectedProperty, accessedPropAsString, args, methodCallData, pageId);
                }, `_${proxiedObject.constructor.name}_${accessedPropAsString}`);
            },
        };
        return new Proxy(proxiedObject, handler);
    }
    async start() {
        for (const beforeAllHook of this.beforeAllHooks) {
            try {
                await beforeAllHook({
                    testContext: this.testContext,
                    rootPage: this.rootPage,
                    proxyContext: this.rootPage,
                });
            }
            catch (e) {
                loggerError(e);
            }
        }
    }
    async end(endStatus) {
        for (const afterAllHook of this.afterAllHooks) {
            try {
                await afterAllHook({ testContext: this.testContext, endStatus });
            }
            catch (e) {
                loggerError(e);
            }
        }
        await this.testContext.testEnded();
    }
    wrapReturnValueInProxy(result, previousMethodCallData, methodCallData, pageId) {
        if (Array.isArray(result)) {
            return result.map((element, index) => {
                return this.innerWrapWithProxy(element, previousMethodCallData.concat({ ...methodCallData, index }), pageId);
            });
        }
        return this.innerWrapWithProxy(result, previousMethodCallData.concat(methodCallData), pageId);
    }
    async makeStep(proxyContext, target, method, fnName, args, methodCallData, pageId) {
        const { beforeHooks, afterHooks, testContext, rootPage } = this;
        const runningStep = testContext.stepStarted();
        runningStep.pageId = pageId;
        const newMethodCallData = {
            selector: puppeteer_selector_mapping_1.extractPuppeteerSelector(proxyContext, fnName, args),
            creationFunction: fnName,
            pageId,
        };
        methodCallData = methodCallData.concat(newMethodCallData);
        const commonHookArgs = {
            testContext,
            fnName,
            proxyContext,
            rootPage,
            args,
            methodCallData,
            stepResult: runningStep,
        };
        for (const beforeHook of beforeHooks) {
            try {
                await beforeHook({ ...commonHookArgs });
            }
            catch (err) {
                loggerError(err);
            }
        }
        try {
            const result = await method.apply(target, args);
            newMethodCallData.text = puppeteer_text_mapping_1.extractPuppeteerText(proxyContext, fnName, args, result);
            for (const afterHook of afterHooks) {
                try {
                    await afterHook({
                        ...commonHookArgs,
                        instrumentedFunctionResult: {
                            success: true,
                            data: result,
                        },
                    });
                }
                catch (err) {
                    loggerError(err);
                }
            }
            return result;
        }
        catch (err) {
            for (const afterHook of afterHooks) {
                try {
                    await afterHook({
                        ...commonHookArgs,
                        instrumentedFunctionResult: {
                            success: false,
                            error: err,
                        },
                    });
                }
                catch (err) {
                    loggerError(err);
                }
            }
            throw err;
        }
        finally {
            await testContext.stepEnded(runningStep);
        }
    }
    getAssociatedEventHandler(target, eventName, userHandler) {
        var _a, _b;
        return (_b = (_a = this.wrappedEventsHandlersRegistry.get(target)) === null || _a === void 0 ? void 0 : _a.get(eventName)) === null || _b === void 0 ? void 0 : _b.get(userHandler);
    }
    addAssociatedEventHandler(target, eventName, userHandler, ourHandler) {
        let mapForTarget = this.wrappedEventsHandlersRegistry.get(target);
        if (!mapForTarget) {
            mapForTarget = new Map();
            this.wrappedEventsHandlersRegistry.set(target, mapForTarget);
        }
        let mapOfEvent = mapForTarget.get(eventName);
        if (!mapOfEvent) {
            mapOfEvent = new WeakMap();
            mapForTarget.set(eventName, mapOfEvent);
        }
        mapOfEvent.set(userHandler, ourHandler);
    }
}
exports.PuppeteerPageHooker = PuppeteerPageHooker;
