"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAndroidHomeDirectoryPath = getAndroidHomeDirectoryPath;
exports.getListOfLocalAndroidEmulators = getListOfLocalAndroidEmulators;
exports.getEmulatorVersion = getEmulatorVersion;
exports.startAndroidEmulator = startAndroidEmulator;
exports.waitForEmulatorRunning = waitForEmulatorRunning;
exports.waitForBootToComplete = waitForBootToComplete;
exports.getRunningAndroidDevices = getRunningAndroidDevices;
exports.waitForAnyOnlineAndroidDevice = waitForAnyOnlineAndroidDevice;
exports.parseEmulatedDevice = parseEmulatedDevice;
exports.parseUsbDevice = parseUsbDevice;
exports.shutdownAndroidEmulator = shutdownAndroidEmulator;
exports.getDeviceSerialId = getDeviceSerialId;
exports.getFirstDeviceSerialId = getFirstDeviceSerialId;
exports.getJavaVersion = getJavaVersion;
exports.resetUiautomatorServer2ForDevice = resetUiautomatorServer2ForDevice;
exports.setAndroidOrientation = setAndroidOrientation;
const child_process_1 = require("child_process");
const fs = __importStar(require("fs"));
const net = __importStar(require("node:net"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const SetOrientationStepDescriptor_1 = require("@mablhq/test-model/types/mobile/SetOrientationStepDescriptor");
const shellUtil_1 = require("./shellUtil");
function sleep(milliseconds) {
    return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
var EmulatorState;
(function (EmulatorState) {
    EmulatorState["running"] = "running";
    EmulatorState["shutdown"] = "shutdown";
})(EmulatorState || (EmulatorState = {}));
function maybeAddExe() {
    if (process.platform === 'win32') {
        return '.exe';
    }
    return '';
}
function getAndroidHomeDirectoryPath() {
    const envAndroidHome = process.env.ANDROID_HOME;
    if (envAndroidHome) {
        return envAndroidHome;
    }
    switch (process.platform) {
        case 'darwin':
            return path.resolve(os.homedir(), path.normalize('Library/Android/sdk'));
        case 'linux':
            const candidatePaths = ['Android/sdk', 'Android/Sdk'].map((relativePath) => path.resolve(os.homedir(), path.normalize(relativePath)));
            const firstExists = candidatePaths.find(fs.existsSync);
            return firstExists ?? candidatePaths[0];
        case 'win32':
        default:
            return path.resolve(os.homedir(), path.normalize('AppData/Local/Android/Sdk'));
    }
}
function getEmulatorCommandPath() {
    const androidHome = getAndroidHomeDirectoryPath();
    return path.normalize(`${androidHome}/emulator/emulator${maybeAddExe()}`);
}
function getADBCommandPath() {
    const androidHome = getAndroidHomeDirectoryPath();
    return path.normalize(`${androidHome}/platform-tools/adb${maybeAddExe()}`);
}
async function executeGeneralAdbCommand(args, timeoutMillis) {
    const adbCommandPath = getADBCommandPath();
    console.debug(`Executing ADB command: ${JSON.stringify(args)}`);
    try {
        const result = await (0, shellUtil_1.asyncSpawn)(adbCommandPath, args, timeoutMillis);
        console.debug(`ADB results: ${JSON.stringify(result)}`);
        return result;
    }
    catch (error) {
        console.error(`ADB Error: `, error);
        throw error;
    }
}
async function executeDeviceAdbCommand(deviceId, args) {
    const deviceArgs = ['-s', deviceId, ...args];
    return executeGeneralAdbCommand(deviceArgs);
}
function createFriendlyEmulatorName(avdName) {
    let name = avdName.replace(/_/gi, ' ');
    if (name.includes('API')) {
        name = name.replace('API', '(API') + ')';
    }
    return name;
}
async function getListOfLocalAndroidEmulators(includePhysicalDevices) {
    const commandPath = getEmulatorCommandPath();
    if (!fs.existsSync(commandPath)) {
        console.warn('No SDK found. Cannot enumerate devices.');
        return {};
    }
    const virtualDeviceListResponse = (0, child_process_1.execFileSync)(commandPath, ['-list-avds'], {
        encoding: 'utf8',
    });
    const avdNames = virtualDeviceListResponse.split(shellUtil_1.NEW_LINE_REGEX);
    if (avdNames.length && avdNames[0].includes('Storing crashdata in')) {
        avdNames.shift();
    }
    const formatted = {};
    avdNames.forEach((avdName) => {
        if (avdName.length) {
            formatted[avdName] = {
                avdName,
                name: createFriendlyEmulatorName(avdName),
                state: EmulatorState.shutdown,
            };
        }
    });
    const runningDevices = await getRunningAndroidDevices();
    for (const device of runningDevices) {
        if (device.avdName) {
            formatted[device.avdName] = {
                avdName: device.avdName,
                name: createFriendlyEmulatorName(device.avdName),
                state: EmulatorState.running,
                runningInfo: device,
            };
        }
        else {
            if (includePhysicalDevices) {
                const hackAvdDeviceName = 'device-' + device.model;
                const hackDeviceInfo = {
                    avdName: hackAvdDeviceName,
                    name: createFriendlyEmulatorName(device.model),
                    runningInfo: { ...device, avdName: hackAvdDeviceName },
                    state: EmulatorState.running,
                    isRealDevice: true,
                };
                formatted[hackAvdDeviceName] = hackDeviceInfo;
            }
        }
    }
    return formatted;
}
function getEmulatorVersion() {
    const commandPath = getEmulatorCommandPath();
    const versionInfoResponse = (0, child_process_1.execFileSync)(commandPath, ['-version'], {
        encoding: 'utf8',
    });
    const versionInfo = versionInfoResponse.split(shellUtil_1.NEW_LINE_REGEX);
    const extracted = /Android emulator version (.*) \(build_id (.*)\) /gm.exec(versionInfo[0]);
    if (extracted) {
        return { version: extracted[1], buildId: extracted[2] };
    }
    return { version: versionInfo[0] };
}
let androidEmulatorProcess;
async function startAndroidEmulator(avdName, headless, gpuMode = 'swiftshader_indirect', blockOnProcess, additionalEmulatorArgs) {
    const commandPath = getEmulatorCommandPath();
    if (androidEmulatorProcess?.connected) {
        androidEmulatorProcess.kill();
    }
    const commandArgs = [
        '-avd',
        avdName,
        '-gpu',
        gpuMode,
        '-engine',
        'qemu2',
        '-noaudio',
        '-camera-back',
        'emulated',
        '-camera-front',
        'emulated',
    ];
    if (headless) {
        commandArgs.push('-no-window');
    }
    console.log(`Starting emulator with command: [${commandPath} ${commandArgs.join(' ')}]`);
    additionalEmulatorArgs?.forEach((arg) => commandArgs.push(arg));
    androidEmulatorProcess = (0, child_process_1.spawn)(commandPath, commandArgs, {
        detached: !blockOnProcess,
    });
    androidEmulatorProcess.stdout?.on('data', (data) => {
        console.log(`${avdName}::stdout: ${data}`);
    });
    let error = '';
    androidEmulatorProcess.stderr?.on('data', (data) => {
        console.error(`${avdName}::stderr: ${data}`);
        error += data;
    });
    console.log('Waiting for emulator to be running...');
    const deviceSerialId = await waitForEmulatorRunning(avdName);
    if (!deviceSerialId) {
        throw new Error('Could not find device serial ID after launching device.');
    }
    console.log('Emulator is running');
    console.log('Waiting for boot to complete...');
    await waitForBootToComplete(deviceSerialId);
    console.log('Emulator boot is complete');
    if (blockOnProcess) {
        const exitCode = await new Promise((resolve) => {
            androidEmulatorProcess.on('close', resolve);
        });
        if (exitCode) {
            throw new Error(`Emulator exited with code: [${exitCode}] \n\n[${error}`);
        }
        return deviceSerialId;
    }
    androidEmulatorProcess.unref();
    return deviceSerialId;
}
async function waitForEmulatorRunning(avdName) {
    const commandTimeoutMs = 4_000;
    const waitPerLoopMs = 1_000;
    const maxLoops = 60;
    for (let i = 0; i < maxLoops; i++) {
        try {
            await executeGeneralAdbCommand(['wait-for-device'], commandTimeoutMs);
            const devices = await getRunningAndroidDevices();
            const desiredDevice = devices.find((device) => device.avdName === avdName);
            if (desiredDevice) {
                return desiredDevice.id;
            }
        }
        catch (error) {
            console.debug(`Error while waiting for emulator to be running: ${error}`);
        }
        await sleep(waitPerLoopMs);
    }
    throw new Error('Timed out while waiting for the device to be running.');
}
async function waitForBootToComplete(deviceSerialId) {
    const waitPerLoopMs = 1000;
    const maxLoops = 300;
    for (let i = 0; i < maxLoops; i++) {
        const result = await executeDeviceAdbCommand(deviceSerialId, [
            'shell',
            'getprop sys.boot_completed',
        ]);
        const cleanResult = result.filter((v) => !!v);
        const booted = cleanResult.length === 1 && cleanResult[0] === '1';
        if (booted) {
            return;
        }
        await sleep(waitPerLoopMs);
    }
    throw new Error('Timed out while waiting for the device to boot complete.');
}
function removeLineBreaksFromString(value) {
    return value.replace(/(\r\n|\n|\r)/gm, '');
}
async function getRunningAndroidDevices() {
    const avdList = await executeGeneralAdbCommand(['devices', '-l']);
    avdList.shift();
    const runningEmulators = [];
    let index = 0;
    for (const deviceLine of avdList) {
        const deviceInfo = await tryParseDeviceLine(deviceLine, index);
        if (deviceInfo) {
            runningEmulators.push(deviceInfo);
        }
        index += 1;
    }
    return runningEmulators;
}
async function waitForAnyOnlineAndroidDevice() {
    const waitPerLoopMs = 1_000;
    const maxLoops = 60;
    for (let i = 0; i < maxLoops; i++) {
        try {
            const devices = await getRunningAndroidDevices();
            if (devices.length > 0) {
                return devices[0];
            }
        }
        catch (error) {
            console.debug(`Error while waiting for emulator to be running: ${error}`);
        }
        await sleep(waitPerLoopMs);
    }
    throw new Error('Timed out while waiting for the device to be running.');
}
async function tryParseDeviceLine(deviceLine, index) {
    return ((await parseEmulatedDevice(deviceLine, index)) ||
        parseUsbDevice(deviceLine, index));
}
async function parseEmulatedDevice(deviceLine, index) {
    const extracted = /(.*) device product:(.*) model:(.*) device:(.*) transport_id:(.*)/gm.exec(deviceLine);
    if (extracted) {
        const serialId = extracted[1]?.replace(/\s+/g, '') ?? `unknown-id-${index}`;
        const product = extracted[2] ?? `unknown-device-product-${index}`;
        const model = extracted[3] ?? `unknown-model-${index}`;
        const device = extracted[4] ?? `unknown-device-${index}`;
        const transportId = extracted[5] ?? `unknown-transport-id-${index}`;
        const avdNameCommand = await getAvdName(serialId);
        return {
            id: serialId,
            avdName: removeLineBreaksFromString(avdNameCommand || ''),
            product,
            model,
            device,
            transportId,
        };
    }
    return;
}
function parseUsbDevice(deviceLine, index) {
    const usbExtracted = /(.*) device (.*) product:(.*) model:(.*) device:(.*) transport_id:(.*)/gm.exec(deviceLine);
    if (usbExtracted) {
        const serialId = usbExtracted[1]?.replace(/\s+/g, '') ?? `unknown-id-${index}`;
        const product = usbExtracted[3] ?? `unknown-device-product-${index}`;
        const model = usbExtracted[4] ?? `unknown-model-${index}`;
        const device = usbExtracted[5] ?? `unknown-device-${index}`;
        const transportId = usbExtracted[6] ?? `unknown-transport-id-${index}`;
        return {
            id: serialId,
            avdName: '',
            product,
            model,
            device,
            transportId,
        };
    }
    return undefined;
}
async function getAvdName(deviceSerialId) {
    const avdNameResults = await executeDeviceAdbCommand(deviceSerialId, [
        'emu',
        'avd',
        'name',
    ]);
    if (avdNameResults.length === 0) {
        console.log('Failed to get avd name using adb emu. Falling back to tcp.');
        return getAvdNameViaTcp(deviceSerialId);
    }
    return avdNameResults[0];
}
async function getAvdNameViaTcp(deviceSerialId) {
    const [typeName, id] = deviceSerialId.split('-');
    if (typeName !== 'emulator') {
        console.log(`The device did not appear to be an emulator so we are skipping it. Device serial ID: ${deviceSerialId}`);
        return undefined;
    }
    const port = parseInt(id);
    if (port < 5000 || port > 6000) {
        throw new Error(`The device ID seems invalid. Expected a value between 5000 and 6000. ${deviceSerialId} ${port}`);
    }
    console.log(`Connecting to Android Console on port ${port}`);
    const socket = net.connect(port, undefined);
    try {
        const lines = await readWithTimeout(socket);
        if (lines === undefined) {
            throw new Error(`Unexpectedly received no response from Android console. Possible timeout.`);
        }
        if (lines[lines.length - 1] !== 'OK') {
            throw new Error(`Unexpected response from Android console. Expected OK but received: ${lines}`);
        }
        socket.write('avd name\n', (err) => {
            if (err) {
                console.log(`An error occurred while sending data to the Android console: ${err}`);
            }
        });
        const result = await readWithTimeout(socket);
        if (result === undefined) {
            throw new Error(`Unexpectedly received no response from Android console while waiting for result. Possible timeout.`);
        }
        if (result.length !== 2) {
            throw new Error(`Unexpected response from Android console. Expected 2 lines but received: ${result}`);
        }
        if (result[1] !== 'OK') {
            throw new Error(`Unexpected response from Android Console. Expected OK but received: ${result[1]}`);
        }
        const avdName = result[0];
        console.log(`AVD Name determined via Android Console: ${avdName} for device: ${deviceSerialId}`);
        return avdName;
    }
    finally {
        if (socket) {
            socket.destroy();
        }
    }
}
async function readWithTimeout(socket) {
    for (let i = 0; i < 100; i++) {
        const result = socket.read();
        if (result) {
            return result
                .toString('utf-8')
                .split(/\r|\n/)
                .filter((value) => !!value);
        }
        await sleep(50);
    }
    return;
}
async function shutdownAndroidEmulator(avdName) {
    const deviceSerialId = await getDeviceSerialId(avdName);
    if (!deviceSerialId) {
        throw new Error(`Could not shut down emulator. Could not find emulator with AVD name: ${avdName}`);
    }
    await executeDeviceAdbCommand(deviceSerialId, ['emu', 'kill']);
}
async function getDeviceSerialId(avdName) {
    const runningDevices = await getRunningAndroidDevices();
    const matchedEmulator = runningDevices.find((emulator) => emulator.avdName === avdName);
    return matchedEmulator?.id;
}
async function getFirstDeviceSerialId() {
    const runningDevices = await getRunningAndroidDevices();
    return runningDevices.length > 0 ? runningDevices[0].id : undefined;
}
function getJavaVersion(javaBinaryPath) {
    try {
        const javaVersionResult = (0, shellUtil_1.syncSpawn)(javaBinaryPath, ['-version']);
        const javaVersionLine = javaVersionResult.find((line) => line?.includes('version'));
        if (!javaVersionLine) {
            return undefined;
        }
        const javaVersionString = javaVersionLine.split(' ')[2].replace('"', '');
        return parseInt(javaVersionString.startsWith('1.')
            ?
                javaVersionString.split('.')[1]
            :
                javaVersionString.split('.')[0], 10);
    }
    catch (error) {
        return undefined;
    }
}
async function resetUiautomatorServer2ForDevice(deviceSerialId) {
    await executeDeviceAdbCommand(deviceSerialId, [
        'uninstall',
        'io.appium.uiautomator2.server',
    ]);
    await executeDeviceAdbCommand(deviceSerialId, [
        'uninstall',
        'io.appium.uiautomator2.server.test',
    ]);
}
async function setAndroidOrientation(deviceSerialId, orientation) {
    const PORTRAIT = '0';
    const LANDSCAPE_NAV_RIGHT = '1';
    const adbOrientation = orientation === SetOrientationStepDescriptor_1.Orientation.Portrait ? PORTRAIT : LANDSCAPE_NAV_RIGHT;
    await executeDeviceAdbCommand(deviceSerialId, [
        'shell',
        'settings',
        'put',
        'system',
        'user_rotation',
        adbOrientation,
    ]);
}
