import node_assert from "node:assert";
import { z } from "@midscene/core";
import { createDefaultMobileActions, defineAction, resolveTextInputOptions, sendTextSequentially, shouldInputSequentially } from "@midscene/core/device";
import { sleep } from "@midscene/core/utils";
import { DEFAULT_WDA_PORT, PLAYGROUND_SERVER_PORT } from "@midscene/shared/constants";
import { createImgBase64ByFormat } from "@midscene/shared/img";
import { getDebug } from "@midscene/shared/logger";
import { mergeAndNormalizeAppNameMapping, normalizeForComparison } from "@midscene/shared/utils";
import { WDAManager, WebDriverClient } from "@midscene/webdriver";
import { Agent } from "@midscene/core/agent";
import { MIDSCENE_IOS_DEVICE_CLASS_OVERRIDE, overrideAIConfig } from "@midscene/shared/env";
import { agentBehaviorInitArgShape, getAgentInitArgsSignature, shouldRebuildAgentForInitArgs } from "@midscene/shared/agent-tools/agent-behavior-init-args";
import { BaseMidsceneTools } from "@midscene/shared/agent-tools/base-tools";
import { exec } from "node:child_process";
import { platform } from "node:os";
import { promisify } from "node:util";
import node_path from "node:path";
import { createMjpegPreviewDescriptor, definePlaygroundPlatform } from "@midscene/playground";
import { findAvailablePort } from "@midscene/shared/node";
const keyboardAccessoryToolbarGeometry = {
    minHeight: 24,
    maxHeight: 80,
    minHorizontalOverlap: 0.8,
    minRightButtonCenterRatio: 0.65,
    maxGap: 48
};
const keyboardNamedButtonMaxDistance = 100;
const hasPositiveSize = (rect)=>rect.width > 0 && rect.height > 0;
const centerOf = (rect)=>({
        x: rect.x + rect.width / 2,
        y: rect.y + rect.height / 2
    });
function isNamedDismissButtonNearKeyboard(buttonRect, keyboardRect) {
    if (!hasPositiveSize(buttonRect) || !hasPositiveSize(keyboardRect)) return false;
    const buttonCenter = centerOf(buttonRect);
    return buttonCenter.x >= keyboardRect.x && buttonCenter.x <= keyboardRect.x + keyboardRect.width && buttonCenter.y >= keyboardRect.y - keyboardNamedButtonMaxDistance && buttonCenter.y <= keyboardRect.y + keyboardRect.height;
}
function keyboardAccessoryToolbarGap(toolbarRect, keyboardRect) {
    return keyboardRect.y - (toolbarRect.y + toolbarRect.height);
}
function isKeyboardAccessoryToolbar(toolbarRect, keyboardRect) {
    if (!hasPositiveSize(toolbarRect) || !hasPositiveSize(keyboardRect)) return false;
    const overlapWidth = Math.max(0, Math.min(toolbarRect.x + toolbarRect.width, keyboardRect.x + keyboardRect.width) - Math.max(toolbarRect.x, keyboardRect.x));
    const horizontalOverlap = overlapWidth / keyboardRect.width;
    const gap = keyboardAccessoryToolbarGap(toolbarRect, keyboardRect);
    const maxGap = Math.max(keyboardAccessoryToolbarGeometry.maxGap, toolbarRect.height);
    return toolbarRect.height >= keyboardAccessoryToolbarGeometry.minHeight && toolbarRect.height <= keyboardAccessoryToolbarGeometry.maxHeight && horizontalOverlap >= keyboardAccessoryToolbarGeometry.minHorizontalOverlap && toolbarRect.y <= keyboardRect.y && gap >= -toolbarRect.height && gap <= maxGap;
}
function selectKeyboardAccessoryDismissButton(toolbarRect, buttons) {
    if (!hasPositiveSize(toolbarRect)) return null;
    const rightRegionStart = toolbarRect.x + toolbarRect.width * keyboardAccessoryToolbarGeometry.minRightButtonCenterRatio;
    const buttonsInsideToolbar = buttons.filter(({ rect })=>{
        if (!hasPositiveSize(rect)) return false;
        const center = centerOf(rect);
        return center.x >= toolbarRect.x && center.x <= toolbarRect.x + toolbarRect.width && center.y >= toolbarRect.y && center.y <= toolbarRect.y + toolbarRect.height;
    });
    const leftSideButtons = buttonsInsideToolbar.filter(({ rect })=>centerOf(rect).x < rightRegionStart);
    const rightSideButtons = buttonsInsideToolbar.filter(({ rect })=>centerOf(rect).x >= rightRegionStart);
    if (0 === leftSideButtons.length || 1 !== rightSideButtons.length) return null;
    const dismissButton = rightSideButtons[0];
    const rightEdgeGap = toolbarRect.x + toolbarRect.width - (dismissButton.rect.x + dismissButton.rect.width);
    const maxRightEdgeGap = Math.max(toolbarRect.height, dismissButton.rect.width);
    if (rightEdgeGap < 0 || rightEdgeGap > maxRightEdgeGap) return null;
    return dismissButton;
}
const debugIOS = getDebug('webdriver:ios');
const WDA_MJPEG_SCREENSHOT_QUALITY = 50;
const WDA_MJPEG_FRAMERATE = 30;
const WDA_MJPEG_SCALING_FACTOR = 50;
const w3cElementId = 'element-6066-11e4-a52e-4f735466cecf';
const keyboardDismissDeadlineMs = 5000;
const keyboardDismissPollIntervalMs = 100;
class IOSWebDriverClient extends WebDriverClient {
    async launchApp(bundleId) {
        this.ensureSession();
        try {
            await this.makeRequest('POST', `/session/${this.sessionId}/wda/apps/launch`, {
                bundleId
            });
            debugIOS(`Launched app: ${bundleId}`);
        } catch (error) {
            debugIOS(`Failed to launch app ${bundleId}: ${error}`);
            throw error;
        }
    }
    async activateApp(bundleId) {
        this.ensureSession();
        await this.makeRequest('POST', `/session/${this.sessionId}/wda/apps/activate`, {
            bundleId
        });
    }
    async terminateApp(bundleId) {
        this.ensureSession();
        await this.makeRequest('POST', `/session/${this.sessionId}/wda/apps/terminate`, {
            bundleId
        });
    }
    async openUrl(url) {
        this.ensureSession();
        try {
            await this.makeRequest('POST', `/session/${this.sessionId}/url`, {
                url
            });
        } catch (error) {
            debugIOS(`Direct URL opening failed, trying Safari fallback: ${error}`);
            await this.launchApp('com.apple.mobilesafari');
            await new Promise((resolve)=>setTimeout(resolve, 2000));
            await this.makeRequest('POST', `/session/${this.sessionId}/url`, {
                url
            });
        }
    }
    async pressHomeButton() {
        this.ensureSession();
        try {
            await this.makeRequest('POST', `/session/${this.sessionId}/wda/pressButton`, {
                name: 'home'
            });
            debugIOS('Home button pressed using hardware key');
        } catch (error) {
            debugIOS(`Failed to press home button: ${error}`);
            throw new Error(`Failed to press home button: ${error}`);
        }
    }
    async appSwitcher() {
        this.ensureSession();
        try {
            const windowSize = await this.getWindowSize();
            const centerX = Math.round(windowSize.width / 2);
            const startY = Math.max(0, windowSize.height - 1);
            const endY = Math.round(0.5 * windowSize.height);
            debugIOS('Triggering app switcher with native WDA drag gesture');
            await this.makeRequest('POST', `/session/${this.sessionId}/wda/dragfromtoforduration`, {
                fromX: centerX,
                fromY: startY,
                toX: centerX,
                toY: endY,
                duration: 1
            });
            await new Promise((resolve)=>setTimeout(resolve, 800));
        } catch (error) {
            debugIOS(`App switcher failed: ${error}`);
            throw new Error(`Failed to trigger app switcher: ${error}`);
        }
    }
    async pressKey(key) {
        this.ensureSession();
        debugIOS(`Attempting to press key: ${key}`);
        if ('+' !== key.trim() && key.includes('+')) throw new Error(`iOS keyboardPress does not support key combinations: ${JSON.stringify(key)}`);
        if ('Enter' === key || 'Return' === key || 'return' === key) {
            debugIOS('Handling Enter/Return key for iOS');
            await this.makeRequest('POST', `/session/${this.sessionId}/wda/keys`, {
                value: [
                    '\n'
                ]
            });
            debugIOS('Sent newline character for Enter key');
            await new Promise((resolve)=>setTimeout(resolve, 100));
            return;
        }
        if ('Backspace' === key || 'Delete' === key) try {
            await this.makeRequest('POST', `/session/${this.sessionId}/wda/keys`, {
                value: [
                    '\b'
                ]
            });
            debugIOS('Sent backspace character');
            return;
        } catch (error) {
            debugIOS(`Backspace failed: ${error}`);
        }
        if ('Space' === key) try {
            await this.makeRequest('POST', `/session/${this.sessionId}/wda/keys`, {
                value: [
                    ' '
                ]
            });
            debugIOS('Sent space character');
            return;
        } catch (error) {
            debugIOS(`Space key failed: ${error}`);
        }
        const normalizedKey = this.normalizeKeyName(key);
        const iosKeyMap = {
            Tab: '\t',
            ArrowUp: '\uE013',
            ArrowDown: '\uE015',
            ArrowLeft: '\uE012',
            ArrowRight: '\uE014',
            Home: '\uE011',
            End: '\uE010'
        };
        if (iosKeyMap[normalizedKey]) try {
            await this.makeRequest('POST', `/session/${this.sessionId}/wda/keys`, {
                value: [
                    iosKeyMap[normalizedKey]
                ]
            });
            debugIOS(`Sent WebDriver key code for: ${key}`);
            return;
        } catch (error) {
            debugIOS(`WebDriver key failed for "${key}": ${error}`);
        }
        if (1 === key.length) try {
            await this.makeRequest('POST', `/session/${this.sessionId}/wda/keys`, {
                value: [
                    key
                ]
            });
            debugIOS(`Sent single character: "${key}"`);
            return;
        } catch (error) {
            debugIOS(`Failed to send character "${key}": ${error}`);
        }
        debugIOS(`Warning: Key "${key}" is not supported on iOS platform`);
        throw new Error(`Key "${key}" is not supported on iOS platform`);
    }
    async getActiveElement() {
        this.ensureSession();
        debugIOS('Getting active element');
        try {
            const response = await this.makeRequest('GET', `/session/${this.sessionId}/element/active`);
            const elementId = response.value?.ELEMENT || response.value?.['element-6066-11e4-a52e-4f735466cecf'] || response.ELEMENT || response['element-6066-11e4-a52e-4f735466cecf'];
            if (elementId) {
                debugIOS(`Got active element ID: ${elementId}`);
                return elementId;
            }
            debugIOS('No active element found');
            return null;
        } catch (error) {
            debugIOS(`Failed to get active element: ${error}`);
            return null;
        }
    }
    async clearElement(elementId) {
        this.ensureSession();
        debugIOS(`Clearing element: ${elementId}`);
        try {
            await this.makeRequest('POST', `/session/${this.sessionId}/element/${elementId}/clear`);
            debugIOS('Element cleared successfully');
        } catch (error) {
            debugIOS(`Failed to clear element: ${error}`);
            throw new Error(`Failed to clear element: ${error}`);
        }
    }
    async clearActiveElement() {
        try {
            const elementId = await this.getActiveElement();
            if (!elementId) {
                debugIOS('No active element to clear');
                return false;
            }
            await this.clearElement(elementId);
            return true;
        } catch (error) {
            debugIOS(`Failed to clear active element: ${error}`);
            return false;
        }
    }
    normalizeKeyName(key) {
        return key.charAt(0).toUpperCase() + key.slice(1).toLowerCase();
    }
    getResponseValue(response) {
        if ('object' != typeof response || null === response) return response;
        const responseRecord = response;
        return 'value' in responseRecord ? responseRecord.value : response;
    }
    getElementId(element) {
        if ('object' != typeof element || null === element) return null;
        const elementRecord = element;
        const elementId = elementRecord[w3cElementId] ?? elementRecord.ELEMENT;
        return 'string' == typeof elementId ? elementId : null;
    }
    getElementIds(response) {
        const elements = this.getResponseValue(response);
        if (!Array.isArray(elements)) throw new Error(`Unexpected WDA elements response: ${JSON.stringify(response)}`);
        return elements.map((element, index)=>{
            const elementId = this.getElementId(element);
            if (!elementId) throw new Error(`WDA element at index ${index} has no element ID: ${JSON.stringify(element)}`);
            return elementId;
        });
    }
    remainingKeyboardDismissTimeout(deadline) {
        const remainingTimeout = deadline - Date.now();
        if (remainingTimeout <= 0) throw new Error(`iOS keyboard dismissal exceeded its ${keyboardDismissDeadlineMs}ms deadline`);
        return Math.max(1, Math.ceil(remainingTimeout));
    }
    async makeKeyboardDismissRequest(method, endpoint, data, deadline) {
        if (void 0 === deadline) return await this.makeRequest(method, endpoint, data);
        return await this.makeRequest(method, endpoint, data, {
            timeout: this.remainingKeyboardDismissTimeout(deadline)
        });
    }
    async findElementIds(using, value, options = {}) {
        const endpoint = options.rootElementId ? `/session/${this.sessionId}/element/${options.rootElementId}/elements` : `/session/${this.sessionId}/elements`;
        const response = await this.makeKeyboardDismissRequest('POST', endpoint, {
            using,
            value
        }, options.deadline);
        return this.getElementIds(response);
    }
    async getElementRect(elementId, deadline) {
        const response = await this.makeKeyboardDismissRequest('GET', `/session/${this.sessionId}/element/${elementId}/rect`, void 0, deadline);
        const rect = this.getResponseValue(response);
        if ('object' != typeof rect || null === rect || ![
            'x',
            'y',
            'width',
            'height'
        ].every((key)=>Number.isFinite(rect[key]))) throw new Error(`Unexpected WDA element rect response: ${JSON.stringify(response)}`);
        const rectRecord = rect;
        return {
            x: rectRecord.x,
            y: rectRecord.y,
            width: rectRecord.width,
            height: rectRecord.height
        };
    }
    async clickElement(elementId, deadline) {
        await this.makeKeyboardDismissRequest('POST', `/session/${this.sessionId}/element/${elementId}/click`, void 0, deadline);
    }
    async findVisibleKeyboardIds(deadline) {
        return await this.findElementIds('predicate string', 'type == "XCUIElementTypeKeyboard" AND visible == true', {
            deadline
        });
    }
    async getVisibleKeyboardRect(deadline) {
        const keyboardIds = await this.findVisibleKeyboardIds(deadline);
        if (0 === keyboardIds.length) return null;
        const keyboardRects = [];
        for (const keyboardId of keyboardIds){
            const rect = await this.getElementRect(keyboardId, deadline);
            if (rect.width > 0 && rect.height > 0) keyboardRects.push(rect);
        }
        const keyboardRect = keyboardRects.sort((left, right)=>right.width * right.height - left.width * left.height)[0];
        if (!keyboardRect) throw new Error('WDA reported a visible keyboard without a valid rect');
        return keyboardRect;
    }
    async dismissKeyboardByName(keyNames, keyboardRect, deadline) {
        const predicateNames = keyNames.map((name)=>`"${name.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`).join(', ');
        const buttonIds = await this.findElementIds('predicate string', `type IN {"XCUIElementTypeButton", "XCUIElementTypeKey"} AND enabled == true AND visible == true AND (name IN {${predicateNames}} OR label IN {${predicateNames}})`, {
            deadline
        });
        const nearbyButtons = [];
        for (const buttonId of buttonIds){
            const rect = await this.getElementRect(buttonId, deadline);
            if (isNamedDismissButtonNearKeyboard(rect, keyboardRect)) nearbyButtons.push({
                id: buttonId,
                distanceFromKeyboard: Math.abs(rect.y + rect.height / 2 - keyboardRect.y)
            });
        }
        nearbyButtons.sort((left, right)=>left.distanceFromKeyboard - right.distanceFromKeyboard);
        const dismissButton = nearbyButtons[0];
        if (!dismissButton) return false;
        await this.clickElement(dismissButton.id, deadline);
        debugIOS(`Dismissed keyboard using configured button: ${keyNames.join(', ')}`);
        return true;
    }
    async dismissKeyboardUsingAccessoryToolbar(keyboardRect, deadline) {
        const toolbarIds = await this.findElementIds('class name', 'XCUIElementTypeToolbar', {
            deadline
        });
        const toolbarCandidates = [];
        for (const toolbarId of toolbarIds){
            const rect = await this.getElementRect(toolbarId, deadline);
            if (isKeyboardAccessoryToolbar(rect, keyboardRect)) {
                const gap = keyboardAccessoryToolbarGap(rect, keyboardRect);
                toolbarCandidates.push({
                    id: toolbarId,
                    rect,
                    gap
                });
            }
        }
        debugIOS(`Found ${toolbarCandidates.length} standard keyboard accessory toolbar candidate(s) from ${toolbarIds.length} toolbar(s)`);
        toolbarCandidates.sort((left, right)=>Math.abs(left.gap) - Math.abs(right.gap));
        for (const toolbar of toolbarCandidates){
            const buttonIds = await this.findElementIds('predicate string', 'type == "XCUIElementTypeButton" AND enabled == true AND visible == true', {
                rootElementId: toolbar.id,
                deadline
            });
            const buttons = [];
            for (const buttonId of buttonIds)buttons.push({
                id: buttonId,
                rect: await this.getElementRect(buttonId, deadline)
            });
            const dismissButton = selectKeyboardAccessoryDismissButton(toolbar.rect, buttons);
            if (!dismissButton) {
                debugIOS(`Rejected keyboard accessory toolbar ${toolbar.id}: expected left navigation controls and exactly one right-edge dismiss control`);
                continue;
            }
            await this.clickElement(dismissButton.id, deadline);
            debugIOS('Dismissed keyboard using the accessory toolbar button');
            return true;
        }
        return false;
    }
    async waitForKeyboardToHide(deadline) {
        while(Date.now() < deadline){
            if (0 === (await this.findVisibleKeyboardIds(deadline)).length) return true;
            const remainingTimeout = deadline - Date.now();
            if (remainingTimeout <= 0) break;
            await new Promise((resolve)=>setTimeout(resolve, Math.min(keyboardDismissPollIntervalMs, remainingTimeout)));
        }
        return false;
    }
    async dismissKeyboard(keyNames) {
        this.ensureSession();
        const startedAt = Date.now();
        const deadline = startedAt + keyboardDismissDeadlineMs;
        const keyboardRect = await this.getVisibleKeyboardRect(deadline);
        if (!keyboardRect) return true;
        const dismissalStarted = keyNames && keyNames.length > 0 ? await this.dismissKeyboardByName(keyNames, keyboardRect, deadline) : await this.dismissKeyboardUsingAccessoryToolbar(keyboardRect, deadline);
        if (!dismissalStarted) return false;
        const hidden = await this.waitForKeyboardToHide(deadline);
        debugIOS(`Keyboard dismissal ${hidden ? 'completed' : 'did not complete'} in ${Date.now() - startedAt}ms`);
        return hidden;
    }
    async isKeyboardVisible() {
        this.ensureSession();
        return (await this.findVisibleKeyboardIds()).length > 0;
    }
    async typeRawKeys(chars) {
        this.ensureSession();
        try {
            await this.makeRequest('POST', `/session/${this.sessionId}/wda/keys`, {
                value: chars
            });
            debugIOS(`Sent raw keys: ${JSON.stringify(chars)}`);
        } catch (error) {
            debugIOS(`Failed to send raw keys ${JSON.stringify(chars)}: ${error}`);
            throw new Error(`Failed to send keys: ${error}`);
        }
    }
    async typeText(text) {
        this.ensureSession();
        try {
            const cleanText = text.trim();
            await this.makeRequest('POST', `/session/${this.sessionId}/wda/keys`, {
                value: cleanText.split('')
            });
            debugIOS(`Typed text: "${text}"`);
        } catch (error) {
            debugIOS(`Failed to type text "${text}": ${error}`);
            throw new Error(`Failed to type text: ${error}`);
        }
    }
    async tap(x, y) {
        this.ensureSession();
        try {
            await this.makeRequest('POST', `/session/${this.sessionId}/wda/tap`, {
                x,
                y
            });
            debugIOS(`Tapped at coordinates (${x}, ${y})`);
        } catch (error) {
            debugIOS(`New tap endpoint failed, trying legacy endpoint: ${error}`);
            try {
                await this.makeRequest('POST', `/session/${this.sessionId}/wda/tap/0`, {
                    x,
                    y
                });
                debugIOS(`Tapped at coordinates (${x}, ${y}) using legacy endpoint`);
            } catch (fallbackError) {
                debugIOS(`Failed to tap at (${x}, ${y}): ${fallbackError}`);
                throw new Error(`Failed to tap at coordinates: ${fallbackError}`);
            }
        }
    }
    async swipe(fromX, fromY, toX, toY, duration = 500) {
        this.ensureSession();
        const actions = {
            actions: [
                {
                    type: 'pointer',
                    id: 'finger1',
                    parameters: {
                        pointerType: 'touch'
                    },
                    actions: [
                        {
                            type: 'pointerMove',
                            duration: 0,
                            x: fromX,
                            y: fromY
                        },
                        {
                            type: 'pointerDown',
                            button: 0
                        },
                        {
                            type: 'pause',
                            duration: 100
                        },
                        {
                            type: 'pointerMove',
                            duration,
                            x: toX,
                            y: toY
                        },
                        {
                            type: 'pointerUp',
                            button: 0
                        }
                    ]
                }
            ]
        };
        await this.makeRequest('POST', `/session/${this.sessionId}/actions`, actions);
        debugIOS(`Swiped using W3C Actions from (${fromX}, ${fromY}) to (${toX}, ${toY}) in ${duration}ms`);
    }
    async pinch(centerX, centerY, startDistance, endDistance, duration = 500) {
        this.ensureSession();
        const halfStart = startDistance / 2;
        const halfEnd = endDistance / 2;
        const actions = {
            actions: [
                {
                    type: 'pointer',
                    id: 'finger1',
                    parameters: {
                        pointerType: 'touch'
                    },
                    actions: [
                        {
                            type: 'pointerMove',
                            duration: 0,
                            x: centerX,
                            y: Math.round(centerY - halfStart)
                        },
                        {
                            type: 'pointerDown',
                            button: 0
                        },
                        {
                            type: 'pause',
                            duration: 100
                        },
                        {
                            type: 'pointerMove',
                            duration,
                            x: centerX,
                            y: Math.round(centerY - halfEnd)
                        },
                        {
                            type: 'pointerUp',
                            button: 0
                        }
                    ]
                },
                {
                    type: 'pointer',
                    id: 'finger2',
                    parameters: {
                        pointerType: 'touch'
                    },
                    actions: [
                        {
                            type: 'pointerMove',
                            duration: 0,
                            x: centerX,
                            y: Math.round(centerY + halfStart)
                        },
                        {
                            type: 'pointerDown',
                            button: 0
                        },
                        {
                            type: 'pause',
                            duration: 100
                        },
                        {
                            type: 'pointerMove',
                            duration,
                            x: centerX,
                            y: Math.round(centerY + halfEnd)
                        },
                        {
                            type: 'pointerUp',
                            button: 0
                        }
                    ]
                }
            ]
        };
        await this.makeRequest('POST', `/session/${this.sessionId}/actions`, actions);
        debugIOS(`Pinched at (${centerX}, ${centerY}) from distance ${startDistance} to ${endDistance} in ${duration}ms`);
    }
    async longPress(x, y, duration = 1000) {
        this.ensureSession();
        await this.makeRequest('POST', `/session/${this.sessionId}/wda/touchAndHold`, {
            x,
            y,
            duration: duration / 1000
        });
        debugIOS(`Long pressed at coordinates (${x}, ${y}) for ${duration}ms`);
    }
    async doubleTap(x, y) {
        this.ensureSession();
        await this.makeRequest('POST', `/session/${this.sessionId}/wda/doubleTap`, {
            x,
            y
        });
        debugIOS(`Double tapped at coordinates (${x}, ${y})`);
    }
    async tripleTap(x, y) {
        this.ensureSession();
        await this.makeRequest('POST', `/session/${this.sessionId}/wda/tapWithNumberOfTaps`, {
            x,
            y,
            numberOfTaps: 3,
            numberOfTouches: 1
        });
        debugIOS(`Triple tapped at coordinates (${x}, ${y})`);
    }
    async getScreenScale() {
        this.ensureSession();
        try {
            const screenResponse = await this.makeRequest('GET', `/session/${this.sessionId}/wda/screen`);
            if (screenResponse?.value?.scale) {
                debugIOS(`Got screen scale from WDA screen endpoint: ${screenResponse.value.scale}`);
                return screenResponse.value.scale;
            }
        } catch (error) {
            debugIOS(`Failed to get screen scale from /wda/screen: ${error}`);
        }
        try {
            debugIOS('Calculating screen scale from screenshot and window size');
            const [screenshotBase64, windowSize] = await Promise.all([
                this.takeScreenshot(),
                this.getWindowSize()
            ]);
            const { imageInfoOfBase64 } = await import("@midscene/shared/img");
            const { width: screenshotWidth, height: screenshotHeight } = await imageInfoOfBase64(screenshotBase64);
            const scale = Math.max(screenshotWidth, screenshotHeight) / Math.max(windowSize.width, windowSize.height);
            const roundedScale = Math.round(scale);
            debugIOS(`Calculated screen scale: ${roundedScale} (screenshot: ${screenshotWidth}x${screenshotHeight}, window: ${windowSize.width}x${windowSize.height})`);
            return roundedScale;
        } catch (error) {
            debugIOS(`Failed to calculate screen scale: ${error}`);
        }
        debugIOS('No screen scale found');
        return null;
    }
    async createSession(capabilities) {
        const defaultCapabilities = {
            platformName: 'iOS',
            automationName: 'XCUITest',
            shouldUseSingletonTestManager: false,
            shouldUseTestManagerForVisibilityDetection: false,
            ...capabilities
        };
        const session = await super.createSession(defaultCapabilities);
        await this.setupIOSSession();
        return session;
    }
    async setupIOSSession() {
        if (!this.sessionId) return;
        try {
            await this.makeRequest('POST', `/session/${this.sessionId}/appium/settings`, {
                snapshotMaxDepth: 50,
                elementResponseAttributes: 'type,label,name,value,rect,enabled,visible',
                mjpegServerScreenshotQuality: WDA_MJPEG_SCREENSHOT_QUALITY,
                mjpegServerFramerate: WDA_MJPEG_FRAMERATE,
                mjpegScalingFactor: WDA_MJPEG_SCALING_FACTOR
            });
            debugIOS('iOS session configuration applied (including MJPEG settings)');
        } catch (error) {
            debugIOS(`Failed to apply iOS session configuration: ${error}`);
        }
    }
    async setupExistingSession() {
        this.ensureSession();
        await this.setupIOSSession();
    }
    async executeRequest(method, endpoint, data) {
        return this.makeRequest(method, this.buildSessionEndpoint(endpoint), data);
    }
}
function _define_property(obj, key, value) {
    if (key in obj) Object.defineProperty(obj, key, {
        value: value,
        enumerable: true,
        configurable: true,
        writable: true
    });
    else obj[key] = value;
    return obj;
}
const debug = getDebug('ios:mjpeg');
const SOI_0 = 0xff;
const SOI_1 = 0xd8;
const EOI_1 = 0xd9;
function extractJpegFrames(buffer) {
    const frames = [];
    let searchStart = 0;
    while(true){
        let soi = -1;
        for(let i = searchStart; i + 1 < buffer.length; i++)if (buffer[i] === SOI_0 && buffer[i + 1] === SOI_1) {
            soi = i;
            break;
        }
        if (-1 === soi) {
            const last = buffer.length - 1;
            const rest = last >= searchStart && buffer[last] === SOI_0 ? buffer.subarray(last) : Buffer.alloc(0);
            return {
                frames,
                rest
            };
        }
        let eoi = -1;
        for(let i = soi + 2; i + 1 < buffer.length; i++)if (buffer[i] === SOI_0 && buffer[i + 1] === EOI_1) {
            eoi = i;
            break;
        }
        if (-1 === eoi) return {
            frames,
            rest: buffer.subarray(soi)
        };
        frames.push(buffer.subarray(soi, eoi + 2));
        searchStart = eoi + 2;
    }
}
class MjpegFrameSource {
    async ensureStarted(timeoutMs = 3000) {
        if (this.stopped) throw new Error('MjpegFrameSource has been stopped');
        if (!this.runPromise) {
            this.abortController = new AbortController();
            this.runPromise = this.run(this.abortController.signal);
        }
        if (this.latest) return;
        const start = this.nowFn();
        while(this.nowFn() - start < timeoutMs){
            if (this.latest) return;
            if (this.stopped) throw new Error('MjpegFrameSource has been stopped');
            await new Promise((resolve)=>setTimeout(resolve, 30));
        }
        throw new Error(`MjpegFrameSource: no frame received from ${this.url} within ${timeoutMs}ms`);
    }
    getLatest() {
        if (!this.latest) return null;
        return {
            base64: this.latest.base64,
            capturedAt: this.latest.capturedAt
        };
    }
    stop() {
        this.stopped = true;
        this.abortController?.abort();
        this.abortController = null;
        this.runPromise = null;
        this.latest = null;
    }
    async run(signal) {
        while(!this.stopped && !signal.aborted){
            try {
                const response = await fetch(this.url, {
                    signal
                });
                if (!response.ok || !response.body) throw new Error(`MJPEG stream responded with ${response.status}`);
                let pending = Buffer.alloc(0);
                for await (const chunk of response.body){
                    if (this.stopped || signal.aborted) break;
                    pending = Buffer.concat([
                        pending,
                        Buffer.from(chunk)
                    ]);
                    const { frames, rest } = extractJpegFrames(pending);
                    if (frames.length > 0) this.latest = {
                        base64: `data:image/jpeg;base64,${frames[frames.length - 1].toString('base64')}`,
                        capturedAt: this.nowFn()
                    };
                    pending = rest;
                    if (pending.length > 8388608) pending = pending.subarray(pending.length - 1024);
                }
            } catch (error) {
                if (this.stopped || signal.aborted) return;
                debug('MJPEG stream error, will retry: %s', error);
            }
            if (!this.stopped && !signal.aborted) await new Promise((resolve)=>setTimeout(resolve, 500));
        }
    }
    constructor(url, nowFn = Date.now){
        _define_property(this, "url", void 0);
        _define_property(this, "latest", void 0);
        _define_property(this, "abortController", void 0);
        _define_property(this, "runPromise", void 0);
        _define_property(this, "stopped", void 0);
        _define_property(this, "nowFn", void 0);
        this.url = url;
        this.latest = null;
        this.abortController = null;
        this.runPromise = null;
        this.stopped = false;
        this.nowFn = nowFn;
    }
}
function device_define_property(obj, key, value) {
    if (key in obj) Object.defineProperty(obj, key, {
        value: value,
        enumerable: true,
        configurable: true,
        writable: true
    });
    else obj[key] = value;
    return obj;
}
const debugDevice = getDebug('ios:device');
const debugDeviceWarning = getDebug('ios:device', {
    console: true
});
const WDA_HTTP_METHODS = [
    'GET',
    'POST',
    'DELETE',
    'PUT'
];
const DEFAULT_WDA_MJPEG_PORT = 9100;
const keyboardFollowUpMaxAgeMs = 30000;
const keyboardFollowUpKeys = new Set([
    'enter',
    'return',
    'tab'
]);
class IOSDevice {
    invalidatePendingKeyboardFollowUp(reason) {
        if (!this.pendingKeyboardFollowUp) return;
        debugDevice(`Discarding pending keyboard follow-up: ${reason}`);
        this.pendingKeyboardFollowUp = void 0;
    }
    registerPendingKeyboardFollowUp(target) {
        if (!target) return;
        this.pendingKeyboardFollowUp = {
            target,
            createdAt: Date.now()
        };
        debugDevice(`Registered one keyboard follow-up for (${target.x}, ${target.y})`);
    }
    consumePendingKeyboardFollowUp(key) {
        const pendingFollowUp = this.pendingKeyboardFollowUp;
        this.pendingKeyboardFollowUp = void 0;
        if (!pendingFollowUp) return;
        const normalizedKey = key.trim().toLowerCase();
        if (!keyboardFollowUpKeys.has(normalizedKey)) return void debugDevice(`Discarding pending keyboard follow-up: ${JSON.stringify(key)} is not a submit/navigation key`);
        const ageMs = Date.now() - pendingFollowUp.createdAt;
        if (ageMs > keyboardFollowUpMaxAgeMs) return void debugDevice(`Discarding pending keyboard follow-up: expired after ${ageMs}ms`);
        return pendingFollowUp.target;
    }
    async tryAutoDismissKeyboard() {
        let dismissed;
        try {
            dismissed = await this.hideKeyboard();
        } catch (error) {
            debugDeviceWarning('Text input request completed, but auto-dismissing the iOS keyboard failed', error);
            return false;
        }
        if (!dismissed) debugDeviceWarning('Text input request completed, but the iOS keyboard could not be auto-dismissed because no supported dismissal control was found or the keyboard remained visible');
        return dismissed;
    }
    async tapPoint(point) {
        this.invalidatePendingKeyboardFollowUp('tap');
        debugDevice(`tap at coordinates (${point.x}, ${point.y})`);
        await this.wdaBackend.tap(Math.round(point.x), Math.round(point.y));
    }
    async doubleTapPoint(point) {
        this.invalidatePendingKeyboardFollowUp('double tap');
        await this.wdaBackend.doubleTap(Math.round(point.x), Math.round(point.y));
    }
    async longPressPoint(point, duration = 1000) {
        this.invalidatePendingKeyboardFollowUp('long press');
        await this.wdaBackend.longPress(Math.round(point.x), Math.round(point.y), duration);
    }
    async swipePoint(start, end, duration = 500) {
        this.invalidatePendingKeyboardFollowUp('swipe');
        await this.wdaBackend.swipe(Math.round(start.x), Math.round(start.y), Math.round(end.x), Math.round(end.y), duration);
    }
    async clearInputAt(point) {
        this.invalidatePendingKeyboardFollowUp('clear input');
        if (point) {
            await this.tapPoint(point);
            await sleep(100);
        }
        debugDevice('Attempting to clear input with WebDriver Clear API');
        const cleared = await this.wdaBackend.clearActiveElement();
        cleared ? debugDevice('Successfully cleared input with WebDriver Clear API') : debugDevice('WebDriver Clear API returned false (no active element or clear failed)');
    }
    actionSpace() {
        const mobileActionContext = {
            input: this.inputPrimitives,
            size: ()=>this.size(),
            sleep: async (timeMs)=>{
                this.invalidatePendingKeyboardFollowUp('sleep action');
                await sleep(timeMs);
            },
            getDefaultAutoDismissKeyboard: ()=>this.options?.autoDismissKeyboard
        };
        const defaultActions = [
            ...createDefaultMobileActions(mobileActionContext)
        ];
        const platformSpecificActions = Object.values(createPlatformActions(this));
        const customActions = (this.customActions || []).map((action)=>({
                ...action,
                call: async (param, context)=>{
                    this.invalidatePendingKeyboardFollowUp(`custom action ${action.name}`);
                    return await action.call(param, context);
                }
            }));
        return [
            ...defaultActions,
            ...platformSpecificActions,
            ...customActions
        ];
    }
    async performActionScroll(param) {
        const element = param.locate;
        const startingPoint = element ? {
            left: element.center[0],
            top: element.center[1]
        } : void 0;
        const scrollToEventName = param?.scrollType;
        if ('scrollToTop' === scrollToEventName) await this.scrollUntilTop(startingPoint);
        else if ('scrollToBottom' === scrollToEventName) await this.scrollUntilBottom(startingPoint);
        else if ('scrollToRight' === scrollToEventName) await this.scrollUntilRight(startingPoint);
        else if ('scrollToLeft' === scrollToEventName) await this.scrollUntilLeft(startingPoint);
        else if ('singleAction' !== scrollToEventName && scrollToEventName) throw new Error(`Unknown scroll event type: ${scrollToEventName}, param: ${JSON.stringify(param)}`);
        else {
            if (param?.direction !== 'down' && param && param.direction) if ('up' === param.direction) await this.scrollUp(param.distance || void 0, startingPoint);
            else if ('left' === param.direction) await this.scrollLeft(param.distance || void 0, startingPoint);
            else if ('right' === param.direction) await this.scrollRight(param.distance || void 0, startingPoint);
            else throw new Error(`Unknown scroll direction: ${param.direction}`);
            else await this.scrollDown(param?.distance || void 0, startingPoint);
            await sleep(500);
        }
    }
    describe() {
        return this.description || `Device ID: ${this.deviceId}`;
    }
    async getConnectedDeviceInfo() {
        return await this.wdaBackend.getDeviceInfo();
    }
    async connect() {
        node_assert(!this.destroyed, `IOSDevice ${this.deviceId} has been destroyed and cannot execute commands`);
        this.invalidatePendingKeyboardFollowUp('connect');
        debugDevice(`Connecting to iOS device: ${this.deviceId}`);
        try {
            await this.wdaManager.start();
            if (this.options?.sessionId) {
                debugDevice(`Using existing WDA session: ${this.options.sessionId}`);
                await this.wdaBackend.setupExistingSession();
            } else await this.wdaBackend.createSession();
            const deviceInfo = await this.wdaBackend.getDeviceInfo();
            if (deviceInfo?.udid) {
                this.deviceId = deviceInfo.udid;
                debugDevice(`Updated device ID to real UDID: ${this.deviceId}`);
            }
            const size = await this.getScreenSize();
            this.description = `
UDID: ${this.deviceId}${deviceInfo ? `
Name: ${deviceInfo.name}
Model: ${deviceInfo.model}` : ''}
Type: WebDriverAgent
ScreenSize: ${size.width}x${size.height} (DPR: ${size.scale})
`;
            debugDevice('iOS device connected successfully', this.description);
        } catch (e) {
            debugDevice(`Failed to connect to iOS device: ${e}`);
            throw new Error(`Unable to connect to iOS device ${this.deviceId}: ${e}`);
        }
    }
    setAppNameMapping(mapping) {
        this.appNameMapping = mapping;
    }
    resolveBundleId(appName) {
        const normalizedAppName = normalizeForComparison(appName);
        return this.appNameMapping[normalizedAppName];
    }
    async launch(uri) {
        this.invalidatePendingKeyboardFollowUp('launch');
        this.uri = uri;
        try {
            debugDevice(`Launching app: ${uri}`);
            if (uri.startsWith('http://') || uri.startsWith('https://') || uri.includes('://')) await this.openUrl(uri);
            else {
                const resolvedUri = this.resolveBundleId(uri) ?? uri;
                await this.wdaBackend.launchApp(resolvedUri);
            }
            debugDevice(`Successfully launched: ${uri}`);
        } catch (error) {
            debugDevice(`Error launching ${uri}: ${error}`);
            throw new Error(`Failed to launch ${uri}: ${error.message}`);
        }
        return this;
    }
    async terminate(bundleId) {
        this.invalidatePendingKeyboardFollowUp('terminate');
        const resolved = this.resolveBundleId(bundleId) ?? bundleId;
        try {
            debugDevice(`Terminating app: ${resolved}`);
            await this.wdaBackend.terminateApp(resolved);
            debugDevice(`Successfully terminated: ${resolved}`);
        } catch (error) {
            debugDevice(`Error terminating ${resolved}: ${error}`);
            throw new Error(`Failed to terminate ${resolved}: ${error.message}`);
        }
    }
    async getElementsInfo() {
        return [];
    }
    async getElementsNodeTree() {
        return {
            node: null,
            children: []
        };
    }
    async initializeDevicePixelRatio() {
        if (this.devicePixelRatioInitialized) return;
        const apiScale = await this.wdaBackend.getScreenScale();
        node_assert(apiScale && apiScale > 0, 'Failed to get device pixel ratio from WebDriverAgent API');
        debugDevice(`Got screen scale from WebDriverAgent API: ${apiScale}`);
        this.devicePixelRatio = apiScale;
        this.devicePixelRatioInitialized = true;
    }
    async getScreenSize() {
        await this.initializeDevicePixelRatio();
        const windowSize = await this.wdaBackend.getWindowSize();
        return {
            width: windowSize.width,
            height: windowSize.height,
            scale: this.devicePixelRatio
        };
    }
    async size() {
        const screenSize = await this.getScreenSize();
        return {
            width: screenSize.width,
            height: screenSize.height
        };
    }
    async screenshotBase64() {
        debugDevice('Taking screenshot via WDA');
        try {
            const base64Data = await this.wdaBackend.takeScreenshot();
            const result = createImgBase64ByFormat('png', base64Data);
            debugDevice('Screenshot taken successfully');
            return result;
        } catch (error) {
            debugDevice(`Screenshot failed: ${error}`);
            throw new Error(`Failed to take screenshot: ${error}`);
        }
    }
    async openMjpegFrameSource() {
        const source = await this.ensureMjpegFrameSource();
        return {
            latest: ()=>{
                const frame = source.getLatest();
                return frame ? {
                    ref: frame.base64,
                    capturedAt: frame.capturedAt
                } : null;
            },
            decode: async (refs)=>refs.map((frameRef)=>frameRef.ref),
            stop: ()=>{
                source.stop();
                if (this.mjpegFrameSource === source) this.mjpegFrameSource = null;
            }
        };
    }
    async ensureMjpegFrameSource() {
        node_assert(!this.destroyed, `IOSDevice ${this.deviceId} has been destroyed and cannot stream frames`);
        if (!this.mjpegFrameSource) this.mjpegFrameSource = new MjpegFrameSource(this.mjpegStreamUrl);
        try {
            await this.mjpegFrameSource.ensureStarted();
        } catch (error) {
            this.mjpegFrameSource.stop();
            this.mjpegFrameSource = null;
            throw error;
        }
        return this.mjpegFrameSource;
    }
    async clearInput(element) {
        await this.clearInputAt(element ? {
            x: element.center[0],
            y: element.center[1]
        } : void 0);
    }
    async url() {
        return '';
    }
    async tap(x, y) {
        await this.tapPoint({
            x,
            y
        });
    }
    async swipe(fromX, fromY, toX, toY, duration = 500) {
        await this.swipeCoordinates(fromX, fromY, toX, toY, duration);
    }
    async swipeCoordinates(fromX, fromY, toX, toY, duration = 500) {
        await this.swipePoint({
            x: fromX,
            y: fromY
        }, {
            x: toX,
            y: toY
        }, duration);
    }
    async typeText(text, options, focusRestorePoint, resolvedInputOptions = resolveTextInputOptions(options, this.options)) {
        this.invalidatePendingKeyboardFollowUp('new text input');
        if (!text) return;
        const shouldAutoDismissKeyboard = options?.autoDismissKeyboard ?? this.options?.autoDismissKeyboard ?? true;
        const typeDelay = resolvedInputOptions.keyboardTypeDelay;
        const inputSequentially = shouldInputSequentially(resolvedInputOptions);
        debugDevice(`Typing text: "${text}"`);
        try {
            await sleep(200);
            if (inputSequentially) await sendTextSequentially(text, {
                sendCharacter: (character)=>this.wdaBackend.typeRawKeys([
                        character
                    ]),
                wait: sleep
            }, {
                delayMs: typeDelay,
                delayAfterLast: 'legacy' === resolvedInputOptions.inputStrategy
            });
            else await this.wdaBackend.typeText(text);
            await sleep(300);
        } catch (error) {
            debugDevice(`Failed to type text with WDA: ${error}`);
            throw error;
        }
        if (shouldAutoDismissKeyboard) {
            const dismissed = await this.tryAutoDismissKeyboard();
            if (dismissed) this.registerPendingKeyboardFollowUp(focusRestorePoint);
        }
    }
    async pressKey(key, target) {
        const explicitTargetPoint = target ? {
            x: target.center[0],
            y: target.center[1]
        } : void 0;
        let focusRestorePoint;
        if (explicitTargetPoint) {
            this.invalidatePendingKeyboardFollowUp('keyboard press has an explicit target');
            focusRestorePoint = explicitTargetPoint;
        } else focusRestorePoint = this.consumePendingKeyboardFollowUp(key);
        if (focusRestorePoint) {
            debugDevice(`Restoring text input focus at (${focusRestorePoint.x}, ${focusRestorePoint.y}) before pressing ${key}`);
            await this.tapPoint(focusRestorePoint);
        }
        await this.wdaBackend.pressKey(key);
    }
    async scrollUp(distance, startPoint) {
        const { width, height } = await this.size();
        const start = startPoint ? {
            x: Math.round(startPoint.left),
            y: Math.round(startPoint.top)
        } : {
            x: Math.round(width / 2),
            y: Math.round(height / 2)
        };
        const scrollDistance = Math.round(distance || height / 3);
        await this.swipeCoordinates(start.x, start.y, start.x, start.y + scrollDistance);
    }
    async scrollDown(distance, startPoint) {
        const { width, height } = await this.size();
        const start = startPoint ? {
            x: Math.round(startPoint.left),
            y: Math.round(startPoint.top)
        } : {
            x: Math.round(width / 2),
            y: Math.round(height / 2)
        };
        const scrollDistance = Math.round(distance || height / 3);
        await this.swipeCoordinates(start.x, start.y, start.x, start.y - scrollDistance);
    }
    async scrollLeft(distance, startPoint) {
        const { width, height } = await this.size();
        const start = startPoint ? {
            x: Math.round(startPoint.left),
            y: Math.round(startPoint.top)
        } : {
            x: Math.round(width / 2),
            y: Math.round(height / 2)
        };
        const scrollDistance = Math.round(distance || 0.7 * width);
        await this.swipeCoordinates(start.x, start.y, start.x + scrollDistance, start.y);
    }
    async scrollRight(distance, startPoint) {
        const { width, height } = await this.size();
        const start = startPoint ? {
            x: Math.round(startPoint.left),
            y: Math.round(startPoint.top)
        } : {
            x: Math.round(width / 2),
            y: Math.round(height / 2)
        };
        const scrollDistance = Math.round(distance || 0.7 * width);
        await this.swipeCoordinates(start.x, start.y, start.x - scrollDistance, start.y);
    }
    async scrollUntilTop(startPoint) {
        debugDevice('Using screenshot-based scroll detection for better reliability');
        await this.scrollUntilBoundary('up', startPoint, 1);
    }
    async scrollUntilBottom(startPoint) {
        debugDevice('Using screenshot-based scroll detection for better reliability');
        await this.scrollUntilBoundary('down', startPoint, 1);
    }
    compareScreenshots(screenshot1, screenshot2, tolerancePercent = 2) {
        if (screenshot1 === screenshot2) {
            debugDevice('Screenshots are identical');
            return true;
        }
        const len1 = screenshot1.length;
        const len2 = screenshot2.length;
        debugDevice(`Screenshots differ: length1=${len1}, length2=${len2}`);
        if (Math.abs(len1 - len2) > 0.1 * Math.min(len1, len2)) {
            debugDevice('Screenshots have significant length difference');
            return false;
        }
        if (len1 > 0 && len2 > 0) {
            const minLength = Math.min(len1, len2);
            const sampleSize = Math.min(2000, minLength);
            let diffCount = 0;
            for(let i = 0; i < sampleSize; i++)if (screenshot1[i] !== screenshot2[i]) diffCount++;
            const diffPercent = diffCount / sampleSize * 100;
            debugDevice(`Character differences: ${diffCount}/${sampleSize} (${diffPercent.toFixed(2)}%)`);
            const isSimilar = diffPercent <= tolerancePercent;
            if (isSimilar) debugDevice(`Screenshots are similar enough (${diffPercent.toFixed(2)}% <= ${tolerancePercent}%)`);
            return isSimilar;
        }
        return false;
    }
    async scrollUntilBoundary(direction, startPoint, maxUnchangedCount = 1) {
        const maxAttempts = 20;
        const { width, height } = await this.size();
        let start;
        if (startPoint) start = {
            x: Math.round(startPoint.left),
            y: Math.round(startPoint.top)
        };
        else switch(direction){
            case 'up':
                start = {
                    x: Math.round(width / 2),
                    y: Math.round(0.2 * height)
                };
                break;
            case 'down':
                start = {
                    x: Math.round(width / 2),
                    y: Math.round(0.8 * height)
                };
                break;
            case 'left':
                start = {
                    x: Math.round(0.8 * width),
                    y: Math.round(height / 2)
                };
                break;
            case 'right':
                start = {
                    x: Math.round(0.2 * width),
                    y: Math.round(height / 2)
                };
                break;
        }
        let lastScreenshot = null;
        let unchangedCount = 0;
        debugDevice(`Starting scroll to ${direction} with content detection`);
        for(let i = 0; i < maxAttempts; i++)try {
            debugDevice(`Scroll attempt ${i + 1}/${maxAttempts}`);
            await sleep(500);
            const currentScreenshot = await this.screenshotBase64();
            if (lastScreenshot && this.compareScreenshots(lastScreenshot, currentScreenshot, 10)) {
                unchangedCount++;
                debugDevice(`Screen content unchanged (${unchangedCount}/${maxUnchangedCount})`);
                if (unchangedCount >= maxUnchangedCount) {
                    debugDevice(`Reached ${direction}: screen content no longer changes`);
                    break;
                }
            } else {
                if (lastScreenshot) debugDevice(`Content changed, resetting counter (was ${unchangedCount})`);
                unchangedCount = 0;
            }
            if (i >= 15 && 0 === unchangedCount) {
                debugDevice(`Too many attempts with dynamic content, stopping scroll to ${direction}`);
                break;
            }
            lastScreenshot = currentScreenshot;
            const scrollDistance = Math.round('left' === direction || 'right' === direction ? 0.6 * width : 0.6 * height);
            debugDevice(`Performing scroll: ${direction}, distance: ${scrollDistance}`);
            switch(direction){
                case 'up':
                    await this.swipeCoordinates(start.x, start.y, start.x, start.y + scrollDistance, 300);
                    break;
                case 'down':
                    await this.swipeCoordinates(start.x, start.y, start.x, start.y - scrollDistance, 300);
                    break;
                case 'left':
                    await this.swipeCoordinates(start.x, start.y, start.x + scrollDistance, start.y, 300);
                    break;
                case 'right':
                    await this.swipeCoordinates(start.x, start.y, start.x - scrollDistance, start.y, 300);
                    break;
            }
            debugDevice('Waiting for scroll and inertia to complete...');
            await sleep(2000);
        } catch (error) {
            debugDevice(`Error during scroll attempt ${i + 1}: ${error}`);
            await sleep(300);
        }
        debugDevice(`Scroll to ${direction} completed after ${maxAttempts} attempts`);
    }
    async scrollUntilLeft(startPoint) {
        await this.scrollUntilBoundary('left', startPoint, 1);
    }
    async scrollUntilRight(startPoint) {
        await this.scrollUntilBoundary('right', startPoint, 3);
    }
    async home() {
        this.invalidatePendingKeyboardFollowUp('home');
        await this.wdaBackend.pressHomeButton();
    }
    async appSwitcher() {
        this.invalidatePendingKeyboardFollowUp('app switcher');
        await this.wdaBackend.appSwitcher();
    }
    async hideKeyboard(keyNames) {
        this.invalidatePendingKeyboardFollowUp('hide keyboard');
        try {
            debugDevice(keyNames && keyNames.length > 0 ? `Attempting to dismiss keyboard using configured buttons: ${keyNames.join(', ')}` : 'Attempting to dismiss keyboard using its accessory toolbar');
            const dismissed = await this.wdaBackend.dismissKeyboard(keyNames);
            debugDevice(dismissed ? 'Successfully dismissed keyboard' : 'No supported keyboard dismiss control was found');
            return dismissed;
        } catch (error) {
            debugDevice(`Failed to hide keyboard: ${error}`);
            throw new Error(`Failed to hide the iOS keyboard through WDA: ${error}`, {
                cause: error
            });
        }
    }
    async openUrl(url, options) {
        this.invalidatePendingKeyboardFollowUp('open URL');
        const opts = {
            useSafariAsBackup: true,
            waitTime: 2000,
            ...options
        };
        try {
            debugDevice(`Opening URL: ${url}`);
            await this.wdaBackend.openUrl(url);
            await sleep(opts.waitTime);
            debugDevice(`Successfully opened URL: ${url}`);
        } catch (error) {
            debugDevice(`Direct URL opening failed: ${error}`);
            if (opts.useSafariAsBackup) {
                debugDevice(`Attempting to open URL via Safari: ${url}`);
                await this.openUrlViaSafari(url);
            } else throw new Error(`Failed to open URL: ${error}`);
        }
    }
    async openUrlViaSafari(url) {
        this.invalidatePendingKeyboardFollowUp('open URL via Safari');
        try {
            debugDevice(`Opening URL via Safari: ${url}`);
            await this.wdaBackend.terminateApp('com.apple.mobilesafari');
            await this.wdaBackend.launchApp('com.apple.mobilesafari');
            await sleep(2000);
            await this.typeText(url, {
                autoDismissKeyboard: false
            });
            await sleep(500);
            await this.pressKey('Return');
            await sleep(1000);
            try {
                await sleep(2000);
                debugDevice(`URL opened via Safari: ${url}`);
            } catch (dialogError) {
                debugDevice(`No confirmation dialog or dialog handling failed: ${dialogError}`);
            }
        } catch (error) {
            debugDevice(`Failed to open URL via Safari: ${error}`);
            throw new Error(`Failed to open URL via Safari: ${error}`);
        }
    }
    async runWdaRequest(method, endpoint, data) {
        this.invalidatePendingKeyboardFollowUp('raw WDA request');
        return await this.wdaBackend.executeRequest(method, endpoint, data);
    }
    async destroy() {
        if (this.destroyed) return;
        this.invalidatePendingKeyboardFollowUp('destroy');
        try {
            this.mjpegFrameSource?.stop();
            this.mjpegFrameSource = null;
            await this.wdaBackend.deleteSession();
            await this.wdaManager.stop();
        } catch (error) {
            debugDevice(`Error during cleanup: ${error}`);
        }
        this.destroyed = true;
        debugDevice(`iOS device ${this.deviceId} destroyed`);
    }
    constructor(options){
        device_define_property(this, "deviceId", void 0);
        device_define_property(this, "devicePixelRatio", 1);
        device_define_property(this, "devicePixelRatioInitialized", false);
        device_define_property(this, "destroyed", false);
        device_define_property(this, "description", void 0);
        device_define_property(this, "customActions", void 0);
        device_define_property(this, "wdaBackend", void 0);
        device_define_property(this, "wdaManager", void 0);
        device_define_property(this, "mjpegStreamUrl", void 0);
        device_define_property(this, "mjpegFrameSource", null);
        device_define_property(this, "openFrameSource", void 0);
        device_define_property(this, "appNameMapping", {});
        device_define_property(this, "pendingKeyboardFollowUp", void 0);
        device_define_property(this, "interfaceType", 'ios');
        device_define_property(this, "uri", void 0);
        device_define_property(this, "options", void 0);
        device_define_property(this, "inputPrimitives", {
            pointer: {
                tap: (point)=>this.tapPoint(point),
                doubleClick: (point)=>this.doubleTapPoint(point),
                longPress: (point, opts)=>this.longPressPoint(point, opts?.duration),
                dragAndDrop: (from, to)=>this.swipePoint(from, to, 1000)
            },
            keyboard: {
                keyboardPress: (keyName, opts)=>this.pressKey(keyName, opts?.target),
                typeText: async (value, opts)=>{
                    const resolvedInputOptions = resolveTextInputOptions(opts, this.options);
                    const target = opts?.target;
                    const focusRestorePoint = target ? {
                        x: target.center[0],
                        y: target.center[1]
                    } : void 0;
                    if (target && opts?.replace !== false) await this.clearInput(target);
                    else if (target) await this.tapPoint({
                        x: target.center[0],
                        y: target.center[1]
                    });
                    if (opts?.focusOnly) return;
                    await this.typeText(value, opts, focusRestorePoint, resolvedInputOptions);
                },
                clearInput: (target)=>this.clearInput(target),
                cursorMove: async (direction, times = 1)=>{
                    const arrowKey = 'left' === direction ? 'ArrowLeft' : 'ArrowRight';
                    for(let i = 0; i < times; i++)await this.pressKey(arrowKey);
                }
            },
            touch: {
                swipe: async (start, end, opts)=>{
                    const duration = opts?.duration ?? 300;
                    const repeat = opts?.repeat ?? 1;
                    for(let i = 0; i < repeat; i++)await this.swipePoint(start, end, duration);
                },
                pinch: async (center, opts)=>{
                    this.invalidatePendingKeyboardFollowUp('pinch');
                    await this.wdaBackend.pinch(Math.round(center.x), Math.round(center.y), opts.startDistance, opts.endDistance, opts.duration);
                }
            },
            scroll: {
                scroll: (param)=>this.performActionScroll(param)
            }
        });
        this.deviceId = 'pending-connection';
        this.options = options;
        this.customActions = options?.customActions;
        const wdaPort = options?.wdaPort || DEFAULT_WDA_PORT;
        const wdaHost = options?.wdaHost || 'localhost';
        const mjpegPort = options?.wdaMjpegPort ?? DEFAULT_WDA_MJPEG_PORT;
        this.wdaBackend = new IOSWebDriverClient({
            port: wdaPort,
            host: wdaHost,
            ...options?.sessionId ? {
                sessionId: options.sessionId
            } : {}
        });
        this.wdaManager = WDAManager.getInstance(wdaPort, wdaHost);
        this.mjpegStreamUrl = `http://${wdaHost}:${mjpegPort}`;
        if (options?.wdaMjpegFrameSource?.enabled) this.openFrameSource = ()=>this.openMjpegFrameSource();
    }
}
const runWdaRequestParamSchema = z.object({
    method: z["enum"](WDA_HTTP_METHODS).describe('HTTP method (GET, POST, DELETE, PUT)'),
    endpoint: z.string().describe('WebDriver API endpoint'),
    data: z.object({}).passthrough().optional().describe('Optional request body data as JSON object')
});
const launchParamSchema = z.object({
    uri: z.string().describe('App name, bundle ID, or URL to launch. Prioritize using the exact bundle ID or URL the user has provided. If none provided, use the accurate app name.')
});
const terminateParamSchema = z.object({
    uri: z.string().describe('Bundle ID of the app to terminate (close). Use the exact bundle ID, e.g. com.apple.Preferences.')
});
const createPlatformActions = (device)=>({
        RunWdaRequest: defineAction({
            name: 'RunWdaRequest',
            description: 'Execute WebDriverAgent API request directly on iOS device',
            interfaceAlias: 'runWdaRequest',
            paramSchema: runWdaRequestParamSchema,
            sample: {
                method: 'GET',
                endpoint: '/status'
            },
            call: async (param)=>await device.runWdaRequest(param.method, param.endpoint, param.data)
        }),
        Launch: defineAction({
            name: 'Launch',
            description: 'Launch an iOS app or URL',
            interfaceAlias: 'launch',
            paramSchema: launchParamSchema,
            sample: {
                uri: 'com.apple.Preferences'
            },
            call: async (param)=>{
                if (!param.uri || '' === param.uri.trim()) throw new Error('Launch requires a non-empty uri parameter');
                await device.launch(param.uri);
            }
        }),
        Terminate: defineAction({
            name: 'Terminate',
            description: 'Terminate (close) an iOS app by its bundle ID',
            interfaceAlias: 'terminate',
            paramSchema: terminateParamSchema,
            sample: {
                uri: 'com.apple.Preferences'
            },
            call: async (param)=>{
                if (!param.uri || '' === param.uri.trim()) throw new Error('Terminate requires a non-empty uri parameter');
                await device.terminate(param.uri);
            }
        }),
        IOSHomeButton: defineAction({
            name: 'IOSHomeButton',
            description: 'Trigger the system "home" operation on iOS devices',
            call: async ()=>{
                await device.home();
            }
        }),
        IOSAppSwitcher: defineAction({
            name: 'IOSAppSwitcher',
            description: 'Trigger the system "app switcher" operation on iOS devices',
            call: async ()=>{
                await device.appSwitcher();
            }
        })
    });
const defaultAppNameMapping = {
    微信: 'com.tencent.xin',
    企业微信: 'com.tencent.ww',
    微信读书: 'com.tencent.weread',
    微信听书: 'com.tencent.wehear',
    QQ: 'com.tencent.mqq',
    QQ音乐: 'com.tencent.QQMusic',
    QQ阅读: 'com.tencent.qqreaderiphone',
    QQ邮箱: 'com.tencent.qqmail',
    QQ浏览器: 'com.tencent.mttlite',
    TIM: 'com.tencent.tim',
    微视: 'com.tencent.microvision',
    腾讯新闻: 'com.tencent.info',
    腾讯视频: 'com.tencent.live4iphone',
    腾讯动漫: 'com.tencent.ied.app.comic',
    腾讯微云: 'com.tencent.weiyun',
    腾讯体育: 'com.tencent.sportskbs',
    腾讯文档: 'com.tencent.txdocs',
    腾讯翻译君: 'com.tencent.qqtranslator',
    腾讯课堂: 'com.tencent.edu',
    腾讯地图: 'com.tencent.sosomap',
    小鹅拼拼: 'com.tencent.dwdcoco',
    全民k歌: 'com.tencent.QQKSong',
    支付宝: 'com.alipay.iphoneclient',
    钉钉: 'com.laiwang.DingTalk',
    闲鱼: 'com.taobao.fleamarket',
    淘宝: 'com.taobao.taobao4iphone',
    斗鱼: 'tv.douyu.live',
    天猫: 'com.taobao.tmall',
    口碑: 'com.taobao.kbmeishi',
    饿了么: 'me.ele.ios.eleme',
    高德地图: 'com.autonavi.amap',
    UC浏览器: 'com.ucweb.iphone.lowversion',
    一淘: 'com.taobao.etaocoupon',
    飞猪: 'com.taobao.travel',
    虾米音乐: 'com.xiami.spark',
    淘票票: 'com.taobao.movie.MoviePhoneClient',
    优酷: 'com.youku.YouKu',
    菜鸟裹裹: 'com.cainiao.cnwireless',
    土豆视频: 'com.tudou.tudouiphone',
    抖音: 'com.ss.iphone.ugc.Aweme',
    抖音极速版: 'com.ss.iphone.ugc.aweme.lite',
    抖音火山版: 'com.ss.iphone.ugc.Live',
    懂车帝: 'com.ss.ios.auto',
    Tiktok: 'com.zhiliaoapp.musically',
    飞书: 'com.bytedance.ee.lark',
    今日头条: 'com.ss.iphone.article.News',
    西瓜视频: 'com.ss.iphone.article.Video',
    皮皮虾: 'com.bd.iphone.super',
    美团: 'com.meituan.imeituan',
    美团外卖: 'com.meituan.itakeaway',
    大众点评: 'com.dianping.dpscope',
    美团优选: 'com.meituan.iyouxuan',
    美团优选团长: 'com.meituan.igrocery.gh',
    美团骑手: 'com.meituan.banma.homebrew',
    美团开店宝: 'com.meituan.imerchantbiz',
    美团拍店: 'com.meituan.pai',
    美团众包: 'com.meituan.banma.crowdsource',
    美团买菜: 'com.baobaoaichi.imaicai',
    京东: 'com.360buy.jdmobile',
    京东读书: 'com.jd.reader',
    网易新闻: 'com.netease.news',
    网易云音乐: 'com.netease.cloudmusic',
    网易邮箱大师: 'com.netease.macmail',
    网易严选: 'com.netease.yanxuan',
    网易公开课: 'com.netease.videoHD',
    网易有道词典: 'youdaoPro',
    有道云笔记: 'com.youdao.note.YoudaoNoteMac',
    百度: 'com.baidu.BaiduMobile',
    百度网盘: 'com.baidu.netdisk',
    百度贴吧: 'com.baidu.tieba',
    百度地图: 'com.baidu.map',
    百度阅读: 'com.baidu.yuedu',
    百度翻译: 'com.baidu.translate',
    百度文库: 'com.baidu.Wenku',
    百度视频: 'com.baidu.videoiphone',
    百度输入法: 'com.baidu.inputMethod',
    快手: 'com.jiangjia.gif',
    快手极速版: 'com.kuaishou.nebula',
    哔哩哔哩: 'tv.danmaku.bilianime',
    芒果TV: 'com.hunantv.imgotv',
    苏宁易购: 'SuningEMall',
    微博: 'com.sina.weibo',
    微博极速版: 'com.sina.weibolite',
    微博国际: 'com.weibo.international',
    墨客: 'com.moke.moke.iphone',
    豆瓣: 'com.douban.frodo',
    知乎: 'com.zhihu.ios',
    小红书: 'com.xingin.discover',
    喜马拉雅: 'com.gemd.iting',
    得到: 'com.luojilab.LuoJiFM-IOS',
    得物: 'com.siwuai.duapp',
    起点读书: 'm.qidian.QDReaderAppStore',
    番茄小说: 'com.dragon.read',
    书旗小说: 'com.shuqicenter.reader',
    拼多多: 'com.xunmeng.pinduoduo',
    多点: 'com.dmall.dmall',
    便利蜂: 'com.bianlifeng.customer.ios',
    亿通行: 'com.ruubypay.yitongxing',
    云闪付: 'com.unionpay.chsp',
    大都会Metro: 'com.DDH.SHSubway',
    爱奇艺视频: 'com.qiyi.iphone',
    搜狐视频: 'com.sohu.iPhoneVideo',
    搜狐新闻: 'com.sohu.newspaper',
    搜狗浏览器: 'com.sogou.SogouExplorerMobile',
    虎牙: 'com.yy.kiwi',
    比心: 'com.yitan.bixin',
    转转: 'com.wuba.zhuanzhuan',
    YY: 'yyvoice',
    绿洲: 'com.sina.oasis',
    陌陌: 'com.wemomo.momoappdemo1',
    什么值得买: 'com.smzdm.client.ios',
    美团秀秀: 'com.meitu.mtxx',
    唯品会: 'com.vipshop.iphone',
    唱吧: 'com.changba.ktv',
    酷狗音乐: 'com.kugou.kugou1002',
    CSDN: 'net.csdn.CsdnPlus',
    多抓鱼: 'com.duozhuyu.dejavu',
    自如: 'com.ziroom.ZiroomProject',
    携程: 'ctrip.com',
    去哪儿旅行: 'com.qunar.iphoneclient8',
    Xmind: 'net.xmind.brownieapp',
    印象笔记: 'com.yinxiang.iPhone',
    欧陆词典: 'eusoft.eudic.pro',
    115: 'com.115.personal',
    名片全能王: 'com.intsig.camcard.lite',
    中国银行: 'com.boc.BOCMBCI',
    '58同城': 'com.taofang.iphone',
    'Google Chrome': 'com.google.chrome.ios',
    Gmail: 'com.google.Gmail',
    Facebook: 'com.facebook.Facebook',
    Firefox: 'org.mozilla.ios.Firefox',
    Messenger: 'com.facebook.Messenger',
    Instagram: 'com.burbn.instagram',
    Starbucks: 'com.starbucks.mystarbucks',
    'Luckin Coffee': 'com.bjlc.luckycoffee',
    Line: 'jp.naver.line',
    Linkedin: 'com.linkedin.LinkedIn',
    Dcard: 'com.dcard.app.Dcard',
    Youtube: 'com.google.ios.youtube',
    Spotify: 'com.spotify.client',
    Netflix: 'com.netflix.Netflix',
    Twitter: 'com.atebits.Tweetie2',
    WhatsApp: 'net.whatsapp.WhatsApp',
    Safari: 'com.apple.mobilesafari',
    'App Store': 'com.apple.AppStore',
    设置: 'com.apple.Preferences',
    相机: 'com.apple.camera',
    照片: 'com.apple.mobileslideshow',
    时钟: 'com.apple.mobiletimer',
    闹钟: 'com.apple.mobiletimer',
    备忘录: 'com.apple.mobilenotes',
    提醒事项: 'com.apple.reminders',
    快捷指令: 'com.apple.shortcuts',
    天气: 'com.apple.weather',
    日历: 'com.apple.mobilecal',
    地图: 'com.apple.Maps',
    电话: 'com.apple.mobilephone',
    通讯录: 'com.apple.MobileAddressBook',
    信息: 'com.apple.MobileSMS',
    FaceTime: 'com.apple.facetime',
    计算器: 'com.apple.calculator',
    家庭: 'com.apple.Home',
    健康: 'com.apple.Health',
    钱包: 'com.apple.Passbook',
    股市: 'com.apple.stocks',
    图书: 'com.apple.iBooks',
    新闻: 'com.apple.news',
    视频: 'com.apple.tv',
    文件: 'com.apple.DocumentsApp',
    邮件: 'com.apple.mobilemail',
    查找: 'com.apple.findmy',
    翻译: 'com.apple.Translate',
    音乐: 'com.apple.Music',
    播客: 'com.apple.podcasts',
    库乐队: 'com.apple.mobilegarageband',
    语音备忘录: 'com.apple.VoiceMemos',
    iMovie: 'com.apple.iMovie',
    Watch: 'com.apple.Bridge',
    'Apple Store': 'com.apple.store.Jolly',
    TestFlight: 'com.apple.TestFlight',
    Keynote: 'com.apple.Keynote',
    'Keynote 讲演': 'com.apple.Keynote'
};
function agent_define_property(obj, key, value) {
    if (key in obj) Object.defineProperty(obj, key, {
        value: value,
        enumerable: true,
        configurable: true,
        writable: true
    });
    else obj[key] = value;
    return obj;
}
const debugAgent = getDebug('ios:agent');
class IOSAgent extends Agent {
    async launch(uri) {
        const action = this.wrapActionInActionSpace('Launch');
        return action({
            uri
        });
    }
    async terminate(uri) {
        const action = this.wrapActionInActionSpace('Terminate');
        return action({
            uri
        });
    }
    createActionWrapper(name) {
        const action = this.wrapActionInActionSpace(name);
        return (...args)=>action(args[0]);
    }
    constructor(device, opts){
        super(device, opts), agent_define_property(this, "runWdaRequest", void 0), agent_define_property(this, "home", void 0), agent_define_property(this, "appSwitcher", void 0), agent_define_property(this, "appNameMapping", void 0);
        this.appNameMapping = mergeAndNormalizeAppNameMapping(defaultAppNameMapping, opts?.appNameMapping);
        device.setAppNameMapping(this.appNameMapping);
        this.runWdaRequest = this.createActionWrapper('RunWdaRequest');
        this.home = this.createActionWrapper('IOSHomeButton');
        this.appSwitcher = this.createActionWrapper('IOSAppSwitcher');
    }
}
async function agentFromWebDriverAgent(opts) {
    debugAgent('Creating iOS agent with WebDriverAgent');
    const overrideModule = opts?.iOSDeviceClassOverride?.trim() || process.env[MIDSCENE_IOS_DEVICE_CLASS_OVERRIDE]?.trim();
    let DeviceClass = IOSDevice;
    if (overrideModule) try {
        const overrideExports = await import(overrideModule);
        const overrideDeviceClass = Object.prototype.hasOwnProperty.call(overrideExports, 'IOSDevice') ? overrideExports.IOSDevice : overrideExports.default;
        if ('function' != typeof overrideDeviceClass) throw new Error(`Module "${overrideModule}" does not export a valid iOS device class (expected "IOSDevice" or default export).`);
        DeviceClass = overrideDeviceClass;
    } catch (error) {
        throw new Error(`Failed to load iOS device class override from "${overrideModule}". Please make sure the package is installed and exports IOSDevice (or default) with Midscene-compatible methods. Original error: ${error instanceof Error ? error.message : String(error)}`);
    }
    const device = new DeviceClass(opts || {});
    await device.connect();
    return new IOSAgent(device, opts);
}
function agent_tools_define_property(obj, key, value) {
    if (key in obj) Object.defineProperty(obj, key, {
        value: value,
        enumerable: true,
        configurable: true,
        writable: true
    });
    else obj[key] = value;
    return obj;
}
const agent_tools_debug = getDebug('agent-tools:ios');
const iosInitArgShape = {
    wdaHost: z.string().optional().describe('WebDriverAgent host, defaults to localhost'),
    wdaPort: z.number().optional().describe('WebDriverAgent port'),
    sessionId: z.string().optional().describe('Existing WebDriverAgent session ID to reuse'),
    wdaMjpegPort: z.number().optional().describe('WebDriverAgent MJPEG streaming port'),
    wdaMjpegFrameSource: z.object({
        enabled: z.boolean().optional()
    }).optional().describe('Opt-in: use the WDA MJPEG stream as a continuous frame source for UI observation (default off)'),
    ...agentBehaviorInitArgShape
};
function getTargetIdentity(initArgs) {
    if (initArgs?.wdaHost || initArgs?.wdaPort || initArgs?.sessionId) {
        const wdaHost = initArgs.wdaHost ?? 'localhost';
        const wdaPort = initArgs.wdaPort ?? 'default';
        const sessionSegment = initArgs.sessionId ? `-session-${initArgs.sessionId}` : '';
        return `wda-${wdaHost}-${wdaPort}${sessionSegment}`;
    }
    return 'auto';
}
class IOSMidsceneTools extends BaseMidsceneTools {
    getCliReportSessionName() {
        return 'midscene-ios';
    }
    createTemporaryDevice() {
        return new IOSDevice({});
    }
    async ensureAgent(opts) {
        const nextSignature = getAgentInitArgsSignature(opts);
        if (this.agent && shouldRebuildAgentForInitArgs(this.lastOptsSignature, nextSignature)) {
            try {
                await this.agent.destroy?.();
            } catch (error) {
                agent_tools_debug('Failed to destroy agent during cleanup:', error);
            }
            this.agent = void 0;
        }
        if (this.agent) return this.agent;
        agent_tools_debug('Creating iOS agent with WebDriverAgent options:', opts || {});
        const reportOptions = this.readCliReportAgentOptions();
        this.agent = await agentFromWebDriverAgent({
            autoDismissKeyboard: false,
            ...reportOptions ?? {},
            ...opts ?? {}
        });
        this.lastOptsSignature = nextSignature;
        return this.agent;
    }
    preparePlatformTools() {
        return [
            {
                name: 'ios_connect',
                description: 'Connect to iOS device or simulator via WebDriverAgent',
                schema: this.getAgentInitArgSchema(),
                cli: this.getAgentInitArgCliMetadata(),
                handler: async (args)=>{
                    const initArgs = this.extractAgentInitParam(args);
                    const identity = getTargetIdentity(initArgs);
                    const reportSession = this.createNewCliReportSession(identity);
                    this.commitCliReportSession(reportSession);
                    if (this.agent) {
                        try {
                            await this.agent.destroy?.();
                        } catch (error) {
                            agent_tools_debug('Failed to destroy agent during connect:', error);
                        }
                        this.agent = void 0;
                        this.lastOptsSignature = void 0;
                    }
                    const agent = await this.ensureAgent(initArgs);
                    const screenshot = await agent.page.screenshotBase64();
                    return {
                        content: [
                            {
                                type: 'text',
                                text: 'Connected to iOS device'
                            },
                            ...this.buildScreenshotContent(screenshot)
                        ],
                        isError: false
                    };
                }
            },
            {
                name: 'ios_disconnect',
                description: 'Disconnect from current iOS device and release WebDriverAgent resources',
                schema: {},
                handler: this.createDisconnectHandler('iOS device')
            }
        ];
    }
    constructor(...args){
        super(...args), agent_tools_define_property(this, "initArgSpec", {
            namespace: 'ios',
            shape: iosInitArgShape,
            cli: {
                preferBareKeys: true
            },
            adapt: (extracted)=>extracted
        }), agent_tools_define_property(this, "lastOptsSignature", void 0);
    }
}
const execAsync = promisify(exec);
const debugUtils = getDebug('ios:utils');
function checkMacOSPlatform() {
    const currentPlatform = platform();
    return {
        isMacOS: 'darwin' === currentPlatform,
        platform: currentPlatform
    };
}
async function checkIOSEnvironment() {
    try {
        const platformCheck = checkMacOSPlatform();
        if (!platformCheck.isMacOS) return {
            available: false,
            error: `iOS development is only supported on macOS. Current platform: ${platformCheck.platform}`
        };
        const { stdout: xcrunPath } = await execAsync('which xcrun');
        if (!xcrunPath.trim()) return {
            available: false,
            error: 'xcrun not found. Please install Xcode Command Line Tools: xcode-select --install'
        };
        try {
            await execAsync('xcodebuild -version');
        } catch (error) {
            return {
                available: false,
                error: 'xcodebuild not found. Please install Xcode from the App Store'
            };
        }
        debugUtils('iOS environment is available for WebDriverAgent');
        return {
            available: true
        };
    } catch (error) {
        const errorMsg = error instanceof Error ? error.message : String(error);
        debugUtils(`iOS environment not available: ${errorMsg}`);
        if (errorMsg.includes('xcrun')) return {
            available: false,
            error: 'Xcode Command Line Tools not properly configured. Please run: sudo xcode-select --reset'
        };
        return {
            available: false,
            error: `iOS development environment not available: ${errorMsg}`
        };
    }
}
async function probeWdaReady(host, port, timeoutMs = 800) {
    const controller = new AbortController();
    const timer = setTimeout(()=>controller.abort(), timeoutMs);
    try {
        const res = await fetch(`http://${host}:${port}/status`, {
            signal: controller.signal
        });
        if (!res.ok) return false;
        const body = await res.json().catch(()=>null);
        return body?.value?.ready === true;
    } catch  {
        return false;
    } finally{
        clearTimeout(timer);
    }
}
const iosPlaygroundPlatform = definePlaygroundPlatform({
    id: 'ios',
    title: 'Midscene iOS Playground',
    description: "iOS playground platform descriptor",
    async prepare (options) {
        const staticDir = options?.staticDir || node_path.join(__dirname, '../../static');
        const availablePlaygroundPort = await findAvailablePort(PLAYGROUND_SERVER_PORT);
        if (availablePlaygroundPort !== PLAYGROUND_SERVER_PORT) console.log(`⚠️  Port ${PLAYGROUND_SERVER_PORT} is busy, using port ${availablePlaygroundPort} instead`);
        const sessionManager = {
            async getSetupSchema () {
                const wdaReady = await probeWdaReady('localhost', DEFAULT_WDA_PORT);
                return {
                    title: 'Connect WebDriverAgent',
                    description: 'Provide the WebDriverAgent host and port that are already running for your selected iPhone or simulator.',
                    primaryActionLabel: 'Create Agent',
                    autoSubmitWhenReady: wdaReady,
                    fields: [
                        {
                            key: 'host',
                            label: 'WebDriverAgent host',
                            type: 'text',
                            required: true,
                            defaultValue: 'localhost',
                            placeholder: 'localhost'
                        },
                        {
                            key: 'port',
                            label: 'WebDriverAgent port',
                            type: 'number',
                            required: true,
                            defaultValue: DEFAULT_WDA_PORT,
                            placeholder: DEFAULT_WDA_PORT.toString()
                        },
                        {
                            key: 'sessionId',
                            label: 'WebDriverAgent session ID',
                            type: 'text',
                            required: false,
                            placeholder: 'Existing session ID'
                        }
                    ]
                };
            },
            async createSession (input) {
                const host = 'string' == typeof input?.host && input.host.trim() ? input.host.trim().replace(/^https?:\/\//, '') : 'localhost';
                const port = 'number' == typeof input?.port ? input.port : Number.parseInt(String(input?.port ?? DEFAULT_WDA_PORT), 10);
                if (Number.isNaN(port) || port < 1 || port > 65535) throw new Error(`Invalid WebDriverAgent port: ${String(input?.port)}`);
                const sessionId = 'string' == typeof input?.sessionId && input.sessionId.trim() ? input.sessionId.trim() : void 0;
                const connectAgent = async ()=>agentFromWebDriverAgent({
                        ...options?.getAgentOptions?.(),
                        wdaHost: host,
                        wdaPort: port,
                        ...sessionId ? {
                            sessionId
                        } : {}
                    });
                const agent = await connectAgent();
                const deviceInfo = await agent.interface.getConnectedDeviceInfo?.();
                const displayName = deviceInfo ? `${deviceInfo.name} (${deviceInfo.model})` : `${host}:${port}`;
                return {
                    agent,
                    agentFactory: connectAgent,
                    preview: createMjpegPreviewDescriptor({
                        title: 'iOS device preview'
                    }),
                    displayName,
                    metadata: {
                        wdaHost: host,
                        wdaPort: port,
                        ...sessionId ? {
                            sessionId
                        } : {},
                        ...deviceInfo ? {
                            deviceInfo
                        } : {}
                    }
                };
            }
        };
        return {
            platformId: 'ios',
            title: 'Midscene iOS Playground',
            sessionManager,
            launchOptions: {
                port: availablePlaygroundPort,
                openBrowser: false,
                verbose: false,
                staticPath: staticDir
            },
            preview: createMjpegPreviewDescriptor({
                title: 'iOS device preview'
            }),
            metadata: {
                sessionConnected: false,
                setupState: 'required'
            }
        };
    }
});
export { IOSAgent, IOSDevice, IOSMidsceneTools, IOSWebDriverClient, agentFromWebDriverAgent, checkIOSEnvironment, iosPlaygroundPlatform, overrideAIConfig };
