/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import fs from 'fs';
import os from 'os';
import path from 'path';
import debug from 'debug';
import { escapeWithQuotes } from '@isomorphic/stringUtils';
import { disposeAll } from '@isomorphic/disposable';
import { eventsHelper } from '@utils/eventsHelper';
import { isPathInside, isSystemDirectory, isWritable } from '@utils/fileUtils';
import { playwright } from '../../inprocess';
import { Tab } from './tab';
const testDebug = debug('pw:mcp:test');
export class Context {
    config;
    sessionLog;
    options;
    _rawBrowserContext;
    _browserContextPromise;
    _tabs = [];
    _currentTab;
    _routes = [];
    _video;
    _disposables = [];
    _runningToolName;
    _pendingUnhandledRejections = [];
    _unhandledRejectionListeners = new Set();
    _onUnhandledRejection = (reason) => {
        this._pendingUnhandledRejections.push(reason);
        for (const listener of this._unhandledRejectionListeners)
            listener(reason);
    };
    constructor(browserContext, options) {
        this.config = options.config;
        this.sessionLog = options.sessionLog;
        this.options = options;
        this._rawBrowserContext = browserContext;
        testDebug('create context');
        process.on('unhandledRejection', this._onUnhandledRejection);
    }
    async dispose() {
        process.off('unhandledRejection', this._onUnhandledRejection);
        await disposeAll(this._disposables);
        for (const tab of this._tabs)
            await tab.dispose();
        this._tabs.length = 0;
        this._currentTab = undefined;
        await this.stopVideoRecording();
    }
    drainPendingUnhandledRejections() {
        const reasons = this._pendingUnhandledRejections.slice();
        this._pendingUnhandledRejections.length = 0;
        return reasons;
    }
    onUnhandledRejection(listener) {
        this._unhandledRejectionListeners.add(listener);
        return () => this._unhandledRejectionListeners.delete(listener);
    }
    debugger() {
        return this._rawBrowserContext.debugger;
    }
    tabs() {
        return this._tabs;
    }
    currentTab() {
        return this._currentTab;
    }
    currentTabOrDie() {
        if (!this._currentTab)
            throw new Error('No open pages available.');
        return this._currentTab;
    }
    async newTab() {
        const browserContext = await this.ensureBrowserContext();
        const page = await browserContext.newPage();
        this._currentTab = this._tabs.find(t => t.page === page);
        return this._currentTab;
    }
    async selectTab(index) {
        const tab = this._tabs[index];
        if (!tab)
            throw new Error(`Tab ${index} not found`);
        await tab.page.bringToFront();
        this._currentTab = tab;
        return tab;
    }
    async ensureTab() {
        const browserContext = await this.ensureBrowserContext();
        if (!this._currentTab)
            await browserContext.newPage();
        return this._currentTab;
    }
    async closeTab(index) {
        const tab = index === undefined ? this._currentTab : this._tabs[index];
        if (!tab)
            throw new Error(`Tab ${index} not found`);
        const url = tab.page.url();
        await tab.page.close();
        return url;
    }
    async workspaceFile(fileName, perCallWorkspaceDir) {
        return await workspaceFile(this.options, fileName, perCallWorkspaceDir);
    }
    async outputFile(template, options) {
        const baseName = template.suggestedFilename || `${template.prefix}-${(template.date ?? new Date()).toISOString().replace(/[:.]/g, '-')}${template.ext ? '.' + template.ext : ''}`;
        return await outputFile(this.options, baseName, options);
    }
    async startVideoRecording(fileName, params) {
        if (this._video)
            throw new Error('Video recording has already been started.');
        this._video = { params, fileName, fileNames: [] };
        const browserContext = await this.ensureBrowserContext();
        for (const page of browserContext.pages())
            await this._startPageVideo(page);
    }
    async stopVideoRecording() {
        if (!this._video)
            return [];
        const video = this._video;
        for (const page of this._rawBrowserContext.pages())
            await page.screencast.stop();
        this._video = undefined;
        return [...video.fileNames];
    }
    async _startPageVideo(page) {
        if (!this._video)
            return;
        const suffix = this._video.fileNames.length ? `-${this._video.fileNames.length}` : '';
        let fileName = this._video.fileName;
        if (fileName && suffix) {
            const ext = path.extname(fileName);
            fileName = path.basename(fileName, ext) + suffix + ext;
        }
        this._video.fileNames.push(fileName);
        await page.screencast.start({ path: fileName, ...this._video.params });
    }
    _onPageCreated(page) {
        const tab = new Tab(this, page, tab => this._onPageClosed(tab));
        this._tabs.push(tab);
        if (!this._currentTab)
            this._currentTab = tab;
        this._startPageVideo(page).catch(() => { });
    }
    _onPageClosed(tab) {
        const index = this._tabs.indexOf(tab);
        if (index === -1)
            return;
        this._tabs.splice(index, 1);
        if (this._currentTab === tab)
            this._currentTab = this._tabs[Math.min(index, this._tabs.length - 1)];
    }
    routes() {
        return this._routes;
    }
    async addRoute(entry) {
        const browserContext = await this.ensureBrowserContext();
        await browserContext.route(entry.pattern, entry.handler);
        this._routes.push(entry);
    }
    async removeRoute(pattern) {
        let removed = 0;
        const browserContext = await this.ensureBrowserContext();
        if (pattern) {
            const toRemove = this._routes.filter(r => r.pattern === pattern);
            for (const route of toRemove)
                await browserContext.unroute(route.pattern, route.handler);
            this._routes = this._routes.filter(r => r.pattern !== pattern);
            removed = toRemove.length;
        }
        else {
            for (const route of this._routes)
                await browserContext.unroute(route.pattern, route.handler);
            removed = this._routes.length;
            this._routes = [];
        }
        return removed;
    }
    isRunningTool() {
        return this._runningToolName !== undefined;
    }
    setRunningTool(name) {
        this._runningToolName = name;
    }
    async _setupRequestInterception(context) {
        if (this.config.network?.allowedOrigins?.length) {
            this._disposables.push(await context.route('**', route => route.abort('blockedbyclient')));
            for (const origin of this.config.network.allowedOrigins) {
                const glob = originOrHostGlob(origin);
                this._disposables.push(await context.route(glob, route => route.continue()));
            }
        }
        if (this.config.network?.blockedOrigins?.length) {
            for (const origin of this.config.network.blockedOrigins)
                this._disposables.push(await context.route(originOrHostGlob(origin), route => route.abort('blockedbyclient')));
        }
    }
    async ensureBrowserContext() {
        if (this._browserContextPromise)
            return this._browserContextPromise;
        this._browserContextPromise = this._initializeBrowserContext();
        return this._browserContextPromise;
    }
    async _initializeBrowserContext() {
        if (this.config.testIdAttribute)
            playwright.selectors.setTestIdAttribute(this.config.testIdAttribute);
        const browserContext = this._rawBrowserContext;
        await this._setupRequestInterception(browserContext);
        if (this.config.saveTrace) {
            await browserContext.tracing.start({
                name: 'trace-' + Date.now(),
                screenshots: true,
                snapshots: true,
                live: true,
            });
            this._disposables.push({
                dispose: async () => {
                    await browserContext.tracing.stop();
                },
            });
        }
        for (const initScript of this.config.browser?.initScript || [])
            this._disposables.push(await browserContext.addInitScript({ path: path.resolve(this.options.cwd, initScript) }));
        for (const page of browserContext.pages())
            this._onPageCreated(page);
        this._disposables.push(eventsHelper.addEventListener(browserContext, 'page', page => this._onPageCreated(page)));
        return browserContext;
    }
    checkUrlAllowed(url) {
        if (this.config.allowUnrestrictedFileAccess)
            return;
        if (!URL.canParse(url))
            return;
        if (new URL(url).protocol === 'file:')
            throw new Error(`Access to "file:" protocol is blocked. Attempted URL: "${url}"`);
    }
    lookupSecret(secretName) {
        if (!this.config.secrets?.[secretName])
            return { value: secretName, code: escapeWithQuotes(secretName, '\'') };
        return {
            value: this.config.secrets[secretName],
            code: `process.env['${secretName}']`,
        };
    }
}
function originOrHostGlob(originOrHost) {
    // Support wildcard port patterns like "http://localhost:*" or "https://example.com:*"
    const wildcardPortMatch = originOrHost.match(/^(https?:\/\/[^/:]+):\*$/);
    if (wildcardPortMatch)
        return `${wildcardPortMatch[1]}:*/**`;
    try {
        const url = new URL(originOrHost);
        // localhost:1234 will parse as protocol 'localhost:' and 'null' origin.
        if (url.origin !== 'null')
            return `${url.origin}/**`;
    }
    catch {
    }
    // Support for legacy host-only mode.
    return `*://${originOrHost}/**`;
}
export async function workspaceFile(options, fileName, perCallWorkspaceDir) {
    const workspace = perCallWorkspaceDir ?? options.cwd;
    const resolvedName = path.resolve(workspace, fileName);
    await checkFile(options, resolvedName, { origin: 'llm' });
    return resolvedName;
}
export function outputDir(options) {
    if (options.config.outputDir)
        return path.resolve(options.config.outputDir);
    const baseName = options.config.skillMode ? '.playwright-cli' : '.playwright-mcp';
    if (isSystemDirectory(options.cwd) || !isWritable(options.cwd))
        return path.join(os.tmpdir(), baseName);
    return path.join(options.cwd, baseName);
}
export async function outputFile(options, fileName, flags) {
    const resolvedFile = path.resolve(outputDir(options), fileName);
    await checkFile(options, resolvedFile, flags);
    await fs.promises.mkdir(path.dirname(resolvedFile), { recursive: true });
    debug('pw:mcp:file')(resolvedFile);
    return resolvedFile;
}
async function checkFile(options, resolvedFilename, flags) {
    // Trust code and unrestricted file access.
    if (flags.origin === 'code' || options.config.allowUnrestrictedFileAccess || options.config.skillMode)
        return;
    // Trust llm to use valid characters in file names.
    const output = outputDir(options);
    const workspace = options.cwd;
    if (!isPathInside(output, resolvedFilename) && !isPathInside(workspace, resolvedFilename))
        throw new Error(`File access denied: ${resolvedFilename} is outside allowed roots. Allowed roots: ${output}, ${workspace}`);
}
//# sourceMappingURL=context.js.map