/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { zod } from '../third_party/index.js';
import { ToolCategory } from './categories.js';
import { defineTool, pageIdSchema } from './ToolDefinition.js';
export const evaluateScript = defineTool(cliArgs => {
    return {
        name: 'evaluate_script',
        description: `Evaluate a JavaScript function inside the currently selected page${cliArgs?.categoryExtensions ? ' or service worker' : ''}. Returns the response as JSON,
so returned values have to be JSON-serializable.`,
        annotations: {
            category: ToolCategory.DEBUGGING,
            readOnlyHint: false,
        },
        schema: {
            ...(cliArgs?.experimentalPageIdRouting ? pageIdSchema : {}),
            function: zod.string().describe(`A JavaScript function declaration to be executed by the tool in the currently selected page.
Example without arguments: \`() => {
  return document.title
}\` or \`async () => {
  return await fetch("example.com")
}\`.
Example with arguments: \`(el) => {
  return el.innerText;
}\`
`),
            args: zod
                .array(zod
                .string()
                .describe('The uid of an element on the page from the page content snapshot'))
                .optional()
                .describe(`An optional list of arguments to pass to the function.`),
            filePath: zod
                .string()
                .optional()
                .describe('The absolute or relative path to a file to save the script output to. If omitted, the output is returned inline.'),
            dialogAction: zod
                .string()
                .optional()
                .describe('Handle dialogs while execution. "accept", "dismiss", or string for response of window.prompt. Defaults to accept.'),
            ...(cliArgs?.categoryExtensions
                ? {
                    serviceWorkerId: zod
                        .string()
                        .optional()
                        .describe(`The optional service worker id to evaluate the script in. If provided, 'pageId' should be omitted. Note: 'args' (element UIDs) cannot be used when evaluating in a service worker.`),
                }
                : {}),
        },
        blockedByDialog: true,
        verifyFilesSchema: ['filePath'],
        handler: async (request, response, context) => {
            const { serviceWorkerId, args: uidArgs, function: fnString, pageId, dialogAction, filePath, } = request.params;
            if (cliArgs?.categoryExtensions && serviceWorkerId) {
                if (uidArgs && uidArgs.length > 0) {
                    throw new Error('args (element uids) cannot be used when evaluating in a service worker.');
                }
                if (pageId) {
                    throw new Error('specify either a pageId or a serviceWorkerId.');
                }
                const worker = await getWebWorker(context, serviceWorkerId);
                const result = await context
                    .getSelectedMcpPage()
                    .waitForEventsAfterAction(async () => {
                    await performEvaluation(worker, fnString, [], response, {
                        filePath,
                        context,
                    });
                }, { handleDialog: dialogAction ?? 'accept' });
                response.attachWaitForResult(result);
                return;
            }
            const mcpPage = cliArgs?.experimentalPageIdRouting
                ? context.getPageById(request.params.pageId)
                : context.getSelectedMcpPage();
            const page = mcpPage.pptrPage;
            const args = [];
            try {
                const frames = new Set();
                for (const uid of uidArgs ?? []) {
                    const handle = await mcpPage.getElementByUid(uid);
                    frames.add(handle.frame);
                    args.push(handle);
                }
                const evaluatable = await getPageOrFrame(page, frames);
                const result = await mcpPage.waitForEventsAfterAction(async () => {
                    await performEvaluation(evaluatable, fnString, args, response, {
                        filePath,
                        context,
                    });
                }, { handleDialog: dialogAction ?? 'accept' });
                response.attachWaitForResult(result);
            }
            finally {
                void Promise.allSettled(args.map(arg => arg.dispose()));
            }
        },
    };
});
const performEvaluation = async (evaluatable, fnString, args, response, options) => {
    const fn = await evaluatable.evaluateHandle(`(${fnString})`);
    try {
        const result = await evaluatable.evaluate(async (fn, ...args) => {
            // @ts-expect-error no types for function fn
            return JSON.stringify(await fn(...args));
        }, fn, ...args);
        if (options?.filePath) {
            const data = new TextEncoder().encode(result ?? 'undefined');
            const { filename } = await options.context.saveFile(data, options.filePath, '.json');
            response.appendResponseLine(`Script ran on page. Output saved to ${filename}.`);
        }
        else {
            response.appendResponseLine('Script ran on page and returned:');
            response.appendResponseLine('```json');
            response.appendResponseLine(`${result}`);
            response.appendResponseLine('```');
        }
    }
    finally {
        void fn.dispose();
    }
};
const getPageOrFrame = async (page, frames) => {
    let pageOrFrame;
    // We can't evaluate the element handle across frames
    if (frames.size > 1) {
        throw new Error("Elements from different frames can't be evaluated together.");
    }
    else {
        pageOrFrame = [...frames.values()][0] ?? page;
    }
    return pageOrFrame;
};
const getWebWorker = async (context, serviceWorkerId) => {
    const serviceWorkers = context.getExtensionServiceWorkers();
    const serviceWorker = serviceWorkers.find((sw) => context.getExtensionServiceWorkerId(sw) === serviceWorkerId);
    if (serviceWorker && serviceWorker.target) {
        const worker = await serviceWorker.target.worker();
        if (!worker) {
            throw new Error('Service worker target not found.');
        }
        return worker;
    }
    else {
        throw new Error('Service worker not found.');
    }
};
//# sourceMappingURL=script.js.map