🏡 index : quine-global/webdriverio.git

import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'

import { describe, expect, it, vi, beforeEach } from 'vitest'
// @ts-expect-error mock feature
import { Local, mockStart } from 'browserstack-local'
import logger from '@wdio/logger'
import { SevereServiceError } from 'webdriverio'
import type { Capabilities, Options } from '@wdio/types'

import BrowserstackLauncher from '../src/launcher.js'
import type { BrowserstackConfig } from '../src/types.js'
import * as utils from '../src/util.js'
import * as bstackLogger from '../src/bstackLogger.js'
import * as FunnelInstrumentation from '../src/instrumentation/funnelInstrumentation.js'
import { RERUN_TESTS_ENV, BROWSERSTACK_TESTHUB_UUID, RERUN_ENV } from '../src/constants.js'
import * as thUtils from '../src/testHub/utils.js'
import TestOpsConfig from '../src/testOps/testOpsConfig.js'

vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger')))
vi.mock('browserstack-local')
vi.mock('node:fs', () => ({
    default: {
        createReadStream: vi.fn().mockReturnValue({ pipe: vi.fn() }),
        createWriteStream: vi.fn().mockReturnValue({ pipe: vi.fn() }),
        stat: vi.fn().mockReturnValue(Promise.resolve({ size: 123 })),
        readFileSync: vi.fn().mockReturnValue('1234\nsomepath'),
        existsSync: vi.fn(),
        truncateSync: vi.fn(),
        mkdirSync: vi.fn(),
        writeFileSync: vi.fn()
    }
}))

vi.spyOn(utils, 'uploadLogs').mockImplementation((_user, _key, _uuid) => new Promise((resolve) => {
    resolve('success')
}))

const bstackLoggerSpy = vi.spyOn(bstackLogger.BStackLogger, 'logToFile')
bstackLoggerSpy.mockImplementation(() => {})
vi.spyOn(FunnelInstrumentation, 'sendFinish').mockImplementation(async () => {})
vi.spyOn(FunnelInstrumentation, 'sendStart').mockImplementation(async () => {})

vi.spyOn(bstackLogger.BStackLogger, 'clearLogFile').mockImplementation(() => {})

const instance = TestOpsConfig.getInstance(true, false) // Passing arguments to ensure instance is created
const buildHashedId = 'mocktesthubbuilduuid'
instance.buildHashedId = buildHashedId
vi.spyOn(TestOpsConfig, 'getInstance').mockImplementation(() => instance)

const productMap = {
    'observability': true,
    'accessibility': false,
    'percy': false,
    'automate': false,
    'app_automate': false
}
vi.spyOn(thUtils, 'getProductMap').mockImplementation(() => {
    return productMap
})

const pkg = await vi.importActual('../package.json') as any
const log = logger('test')
const error = new Error('I\'m an error!')
const sleep = (ms: number = 100) => new Promise((resolve) => setTimeout(resolve, ms))

beforeEach(() => {
    vi.clearAllMocks()
})

describe('onPrepare', () => {
    const options: BrowserstackConfig = { browserstackLocal: true }
    const caps: any = [{}]
    const config = {
        user: 'foobaruser',
        key: '12345678901234567890',
        capabilities: []
    }
    vi.spyOn(utils, 'launchTestSession').mockImplementation(() => {})
    vi.spyOn(utils, 'isBStackSession').mockImplementation(() => {return true})

    it('should not try to upload app is app is undefined', async () => {
        const service = new BrowserstackLauncher({ testObservability: false } as any, caps, config)
        await service.onPrepare(config, caps)

        expect(log.debug).toHaveBeenCalledWith('app is not defined in browserstack-service config, skipping ...')
    })

    it('should not call local if browserstackLocal is undefined', async () => {
        const service = new BrowserstackLauncher({ testObservability: false, percy: false } as any, caps, {
            user: 'foobaruser',
            key: '12345',
            capabilities: []
        })
        await service.onPrepare(config, caps)

        expect(log.info).toHaveBeenNthCalledWith(1, 'browserstackLocal is not enabled - skipping...')
        expect(service.browserstackLocal).toBeUndefined()
    })

    it('should not call local if browserstackLocal is false', async () => {
        const service = new BrowserstackLauncher({
            browserstackLocal: false,
            testObservability: false,
            percy: false
        } as any, caps, {
            user: 'foobaruser',
            key: '12345',
            capabilities: []
        })
        await service.onPrepare(config, caps)

        expect(log.info).toHaveBeenNthCalledWith(1, 'browserstackLocal is not enabled - skipping...')
        expect(service.browserstackLocal).toBeUndefined()
    })

    it('should add the "app" property to a multiremote capability if no "bstack:options"', async () => {
        const options: BrowserstackConfig = { app: 'bs://<app-id>' }
        const service = new BrowserstackLauncher(options as any, caps, config)
        const capabilities = { samsungGalaxy: { capabilities: {} } }

        await service.onPrepare(config, capabilities)
        expect(capabilities.samsungGalaxy.capabilities).toEqual({ 'appium:app': 'bs://<app-id>', 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId } })
    })

    it('should add the "appium:app" property to a multiremote capability if "bstack:options" present', async () => {
        const options: BrowserstackConfig = { app: 'bs://<app-id>' }
        const service = new BrowserstackLauncher(options as any, caps, config)
        const capabilities = { samsungGalaxy: { capabilities: { 'bstack:options': {} } } }

        await service.onPrepare(config, capabilities)
        expect(capabilities.samsungGalaxy.capabilities).toEqual({ 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://<app-id>' })
    })

    it('should add the "appium:app" property to a multiremote capability if any extension cap present', async () => {
        const options: BrowserstackConfig = { app: 'bs://<app-id>' }
        const service = new BrowserstackLauncher(options as any, caps, config)
        const capabilities = { samsungGalaxy: { capabilities: { 'appium:chromeOptions': {} } } }

        await service.onPrepare(config, capabilities as any)
        expect(capabilities.samsungGalaxy.capabilities).toEqual({ 'appium:app': 'bs://<app-id>', 'appium:chromeOptions': {}, 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId } })
    })

    it('should add the "app" property to an array of capabilities if no "bstack:options"', async () => {
        const options: BrowserstackConfig = { app: 'bs://<app-id>' }
        const service = new BrowserstackLauncher(options as any, caps, config)
        const capabilities = [{}, {}, {}]

        await service.onPrepare(config, capabilities)
        expect(capabilities).toEqual([
            { 'app': 'bs://<app-id>', 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap },
            { 'app': 'bs://<app-id>', 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap },
            { 'app': 'bs://<app-id>', 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap }
        ])
    })

    it('should add the "appium:app" property to an array of capabilities if "bstack:options" present', async () => {
        const options: BrowserstackConfig = { app: 'bs://<app-id>' }
        const service = new BrowserstackLauncher(options as any, caps, config)
        const capabilities = [{ 'bstack:options': {} }, { 'bstack:options': {} }, { 'bstack:options': {} }]

        await service.onPrepare(config, capabilities)
        expect(capabilities).toEqual([
            { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId },  'appium:app': 'bs://<app-id>' },
            { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId },  'appium:app': 'bs://<app-id>' },
            { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId },  'appium:app': 'bs://<app-id>' }
        ])
    })

    it('should add the "appium:app" property to an array of capabilities if any extension cap present', async () => {
        const options: BrowserstackConfig = { app: 'bs://<app-id>' }
        const service = new BrowserstackLauncher(options as any, caps, config)
        const capabilities = [{ 'appium:chromeOptions': {} }, { 'appium:chromeOptions': {} }]

        await service.onPrepare(config, capabilities as any)
        expect(capabilities).toEqual([
            { 'appium:app': 'bs://<app-id>', 'appium:chromeOptions': {}, 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId } },
            { 'appium:app': 'bs://<app-id>', 'appium:chromeOptions': {}, 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId } }
        ])
    })

    it('should add the "appium:app" as custom_id of app to capability object', async () => {
        const options: BrowserstackConfig = { app: 'custom_id' }
        const service = new BrowserstackLauncher(options as any, caps, config)
        const capabilities = [{ 'appium:chromeOptions': {} }, { 'appium:chromeOptions': {} }]

        await service.onPrepare(config, capabilities as any)
        expect(capabilities).toEqual([
            { 'appium:app': 'custom_id', 'appium:chromeOptions': {}, 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId } },
            { 'appium:app': 'custom_id', 'appium:chromeOptions': {}, 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId } }
        ])
    })

    it('should add the "appium:app" as shareable_id of app to capability object', async () => {
        const options: BrowserstackConfig = { app: 'user/custom_id' }
        const service = new BrowserstackLauncher(options as any, caps, config)
        const capabilities = [{ 'appium:chromeOptions': {} }, { 'appium:chromeOptions': {} }]

        await service.onPrepare(config, capabilities as any)
        expect(capabilities).toEqual([
            { 'appium:app': 'user/custom_id', 'appium:chromeOptions': {}, 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId } },
            { 'appium:app': 'user/custom_id', 'appium:chromeOptions': {}, 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId } }
        ])
    })

    it('should add "appium:app" property with value returned from app upload to capabilities', async () => {
        const options: BrowserstackConfig = { app: '/some/dummy/file.apk' }
        const service = new BrowserstackLauncher(options as any, caps, config)
        const capabilities = [{ 'bstack:options': {} }, { 'bstack:options': {} }, { 'bstack:options': {} }]

        vi.spyOn(fs, 'existsSync').mockReturnValueOnce(true)
        vi.spyOn(service, '_uploadApp').mockImplementation(() => Promise.resolve({ app_url: 'bs://<app-id>' }))

        await service.onPrepare(config, capabilities)
        expect(capabilities).toEqual([
            { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://<app-id>' },
            { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://<app-id>' },
            { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://<app-id>' }
        ])
    })

    it('should upload app if path property present in appConfig', async() => {
        const options: BrowserstackConfig = { app: { path: '/path/to/app.apk' } }
        const service = new BrowserstackLauncher(options as any, caps, config)
        const capabilities = [{ 'bstack:options': {} }, { 'bstack:options': {} }, { 'bstack:options': {} }]

        vi.spyOn(fs, 'existsSync').mockReturnValueOnce(true)
        vi.spyOn(service, '_uploadApp').mockImplementation(() => Promise.resolve({ app_url: 'bs://<app-id>' }))

        await service.onPrepare(config, capabilities)
        expect(capabilities).toEqual([
            { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://<app-id>' },
            { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://<app-id>' },
            { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://<app-id>' }
        ])
    })

    it('should upload app along with custom_id if path and custom_id property present in appConfig', async() => {
        const options: BrowserstackConfig = { app: { path: '/path/to/app.apk', custom_id: 'custom_id' } }
        const service = new BrowserstackLauncher(options as any, caps, config)
        const capabilities = [{ 'bstack:options': {} }, { 'bstack:options': {} }, { 'bstack:options': {} }]

        vi.spyOn(fs, 'existsSync').mockReturnValueOnce(true)
        vi.spyOn(service, '_uploadApp').mockImplementation(() => Promise.resolve({ app_url: 'bs://<app-id>', custom_id: 'custom_id', shareable_id: 'foobaruser/custom_id' }))

        await service.onPrepare(config, capabilities)
        expect(capabilities).toEqual([
            { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://<app-id>' },
            { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://<app-id>' },
            { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://<app-id>' }
        ])
    })

    it('should throw SevereServiceError if _validateApp fails', async () => {
        const options = { app: 'bs://<app-id>' } as BrowserstackConfig & Options.Testrunner
        const service = new BrowserstackLauncher(options, caps, config)
        const capabilities = { samsungGalaxy: { capabilities: {} } }

        vi.spyOn(service, '_validateApp').mockImplementationOnce(() => { throw new Error() } )

        try {
            await service.onPrepare(config, capabilities)
        } catch (e: any) {
            expect(e.name).toEqual('SevereServiceError')
        }
    })

    it('should throw SevereServiceError if fs.existsSync fails', async () => {
        const options = { app: { path: '/path/to/app.apk', custom_id: 'custom_id' } } as BrowserstackConfig & Options.Testrunner
        const service = new BrowserstackLauncher(options, caps, config)
        const capabilities = { samsungGalaxy: { capabilities: {} } }

        vi.spyOn(service, '_validateApp').mockImplementation(() => Promise.resolve({ app: 'bs://<app-id>', customId: 'custom_id' }))
        vi.spyOn(fs, 'existsSync').mockReturnValue(false)

        try {
            await service.onPrepare(config, capabilities)
        } catch (e: any) {
            expect(e.name).toEqual('SevereServiceError')
            expect(e.message).toEqual('[Invalid app path] app path ${app.app} is not correct, Provide correct path to app under test')
        }
    })

    it('should initialize the opts object, and spawn a new Local instance', async () => {
        const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)
        await service.onPrepare(config, caps)
        expect(service.browserstackLocal).toBeDefined()
    })

    it('should add the "browserstack.local" property to a multiremote capability if no "bstack:options"', async () => {
        const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)
        const capabilities = { chromeBrowser: { capabilities: {} } }

        await service.onPrepare(config, capabilities)
        expect(capabilities.chromeBrowser.capabilities).toEqual({ 'browserstack.local': true, 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap })
    })

    it('should add the "browserstack.localIdentifier" property to a multiremote capability if no "bstack:options"', async () => {
        const service = new BrowserstackLauncher({
            browserstackLocal: true,
            opts: { localIdentifier: 'wdio1' }
        } as BrowserstackConfig & Options.Testrunner, caps, {
            user: 'foobaruser',
            key: '12345',
            capabilities: []
        })
        const capabilities = { chromeBrowser: { capabilities: {} } }

        await service.onPrepare(config, capabilities)
        expect(capabilities.chromeBrowser.capabilities).toEqual({ 'browserstack.local': true, 'browserstack.localIdentifier': 'wdio1', 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap })
    })

    it('should add the "local" property to a multiremote capability inside "bstack:options" if "bstack:options" present', async () => {
        const service = new BrowserstackLauncher(options as any, caps, config)
        const capabilities = { chromeBrowser: { capabilities: { 'bstack:options': {} } } }

        await service.onPrepare(config, capabilities)
        expect(capabilities.chromeBrowser.capabilities).toEqual({ 'bstack:options': { local: true, testhubBuildUuid: buildHashedId, buildProductMap: productMap } })
    })

    it('should add the "localIdentifier" property to a multiremote capability inside "bstack:options" if "bstack:options" present', async () => {
        const service = new BrowserstackLauncher({
            browserstackLocal: true,
            opts: { localIdentifier: 'wdio1' }
        } as BrowserstackConfig & Options.Testrunner, caps, {
            user: 'foobaruser',
            key: '12345',
            capabilities: []
        })
        const capabilities = { chromeBrowser: { capabilities: { 'bstack:options': {} } } }

        await service.onPrepare(config, capabilities)
        expect(capabilities.chromeBrowser.capabilities).toEqual({ 'bstack:options': { local: true, localIdentifier: 'wdio1', testhubBuildUuid: buildHashedId, buildProductMap: productMap } })
    })

    it('should add the "local" property to a multiremote capability inside "bstack:options" if any extension cap present', async () => {
        const service = new BrowserstackLauncher(options as any, caps, config)
        const capabilities = { chromeBrowser: { capabilities: { 'goog:chromeOptions': {} } } }

        await service.onPrepare(config, capabilities)
        expect(capabilities.chromeBrowser.capabilities).toEqual({ 'bstack:options': { local: true, testhubBuildUuid: buildHashedId, buildProductMap: productMap }, 'goog:chromeOptions': {} })
    })

    it('should add the "localIdentifier" property to a multiremote capability inside "bstack:options" if any extension cap present', async () => {
        const service = new BrowserstackLauncher({
            browserstackLocal: true,
            opts: { localIdentifier: 'wdio1' }
        } as BrowserstackConfig & Options.Testrunner, caps, config)
        const capabilities = { chromeBrowser: { capabilities: { 'goog:chromeOptions': {} } } }

        await service.onPrepare(config, capabilities)
        expect(capabilities.chromeBrowser.capabilities).toEqual({ 'bstack:options': { local: true, localIdentifier: 'wdio1', testhubBuildUuid: buildHashedId, buildProductMap: productMap }, 'goog:chromeOptions': {} })
    })

    it('should add the "localIdentifier" property to an array of capabilities inside "bstack:options" if "bstack:options" present', async () => {
        const service = new BrowserstackLauncher({
            browserstackLocal: true,
            opts: { localIdentifier: 'wdio1' }
        } as BrowserstackConfig & Options.Testrunner, caps, config)
        const capabilities = [{ 'bstack:options': {} }, { 'bstack:options': {} }, { 'bstack:options': {} }]

        await service.onPrepare(config, capabilities)
        expect(capabilities).toEqual([
            { 'bstack:options': { local: true, localIdentifier: 'wdio1', testhubBuildUuid: buildHashedId, buildProductMap: productMap } },
            { 'bstack:options': { local: true, localIdentifier: 'wdio1', testhubBuildUuid: buildHashedId, buildProductMap: productMap } },
            { 'bstack:options': { local: true, localIdentifier: 'wdio1', testhubBuildUuid: buildHashedId, buildProductMap: productMap } }
        ])
    })

    it('should add the "browserstack.local" property to an array of capabilities if no "bstack:options"', async () => {
        const service = new BrowserstackLauncher(options as any, caps, config)
        const capabilities = [{}, {}, {}]

        await service.onPrepare(config, capabilities)
        expect(capabilities).toEqual([
            { 'browserstack.local': true, 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap },
            { 'browserstack.local': true, 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap },
            { 'browserstack.local': true, 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap }
        ])
    })

    it('should add the "browserstack.localIdentifier" property to an array of capabilities if no "bstack:options"', async () => {
        const service = new BrowserstackLauncher({
            browserstackLocal: true,
            opts: { localIdentifier: 'wdio1' }
        } as BrowserstackConfig & Options.Testrunner, caps, {
            user: 'foobaruser',
            key: '12345',
            capabilities: []
        })
        const capabilities = [{}, {}, {}]

        await service.onPrepare(config, capabilities)
        expect(capabilities).toEqual([
            { 'browserstack.local': true, 'browserstack.localIdentifier': 'wdio1', 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap },
            { 'browserstack.local': true, 'browserstack.localIdentifier': 'wdio1', 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap },
            { 'browserstack.local': true, 'browserstack.localIdentifier': 'wdio1', 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap }
        ])
    })

    it('should add the "local" property to an array of capabilities inside "bstack:options" if "bstack:options" present', async () => {
        const service = new BrowserstackLauncher(options as any, caps, config)
        const capabilities = [{ 'bstack:options': {} }, { 'bstack:options': {} }, { 'bstack:options': {} }]

        await service.onPrepare(config, capabilities)
        expect(capabilities).toEqual([
            { 'bstack:options': { local: true, testhubBuildUuid: buildHashedId, buildProductMap: productMap } },
            { 'bstack:options': { local: true, testhubBuildUuid: buildHashedId, buildProductMap: productMap } },
            { 'bstack:options': { local: true, testhubBuildUuid: buildHashedId, buildProductMap: productMap } }
        ])
    })

    it('should add the "local" property to an array of capabilities inside "bstack:options" if any extension cap present', async () => {
        const service = new BrowserstackLauncher(options as any, caps, config)
        const capabilities = [{ 'ms:edgeOptions': {} }, { 'goog:chromeOptions': {} }, { 'moz:firefoxOptions': {} }]

        await service.onPrepare(config, capabilities)
        expect(capabilities).toEqual([
            { 'bstack:options': { local: true, testhubBuildUuid: buildHashedId, buildProductMap: productMap }, 'ms:edgeOptions': {} },
            { 'bstack:options': { local: true, testhubBuildUuid: buildHashedId, buildProductMap: productMap }, 'goog:chromeOptions': {} },
            { 'bstack:options': { local: true, testhubBuildUuid: buildHashedId, buildProductMap: productMap }, 'moz:firefoxOptions': {} }
        ])
    })

    it('should add the "buildIdentifier" property to a multiremote capability inside "bstack:options" if "bstack:options" present', async () => {
        const caps: any = { chromeBrowser: { capabilities: { 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#${BUILD_NUMBER}' } } } }
        const service = new BrowserstackLauncher({} as any, caps, config)
        const capabilities = { chromeBrowser: { capabilities: { 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#${BUILD_NUMBER}' } } } }

        vi.spyOn(utils, 'getCiInfo').mockReturnValueOnce(null)
        vi.spyOn(service, '_getLocalBuildNumber').mockImplementation(() => { return '1' })

        await service.onPrepare(config, capabilities)
        expect(capabilities.chromeBrowser.capabilities).toEqual({ 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#1', testhubBuildUuid: buildHashedId, buildProductMap: productMap } })
    })

    it('should add the "buildIdentifier" property to an array of capabilities inside "bstack:options" if "bstack:options" present', async () => {
        const caps: any = [{ 'bstack:options': {
            buildName: 'browserstack wdio build',
            buildIdentifier: '#${BUILD_NUMBER}'
        } },
        { 'bstack:options': {
            buildName: 'browserstack wdio build',
            buildIdentifier: '#${BUILD_NUMBER}'
        } }]
        const service = new BrowserstackLauncher(options as any, caps, config)
        const capabilities = [{ 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#${BUILD_NUMBER}' } }, { 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#${BUILD_NUMBER}' } }]

        vi.spyOn(utils, 'getCiInfo').mockReturnValueOnce(null)
        vi.spyOn(service, '_getLocalBuildNumber').mockImplementation(() => { return '1' })

        await service.onPrepare(config, capabilities)
        expect(capabilities).toEqual([
            { 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#1', local: true, testhubBuildUuid: buildHashedId, buildProductMap: productMap } },
            { 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#1', local: true, testhubBuildUuid: buildHashedId, buildProductMap: productMap } },
        ])
    })

    it('should delete the "buildIdentifier" property from Capabilities object', async () => {
        const caps: any = [{ 'bstack:options': {
            buildIdentifier: '#${BUILD_NUMBER}'
        } }]
        const service = new BrowserstackLauncher({} as any, caps, config)
        const capabilities = [{ 'bstack:options': { buildIdentifier: '#${BUILD_NUMBER}' } }]

        vi.spyOn(utils, 'getCiInfo').mockReturnValueOnce(null)
        vi.spyOn(service, '_getLocalBuildNumber').mockImplementation(() => { return '1' })

        await service.onPrepare(config, capabilities as any)
        expect(capabilities[0]).toEqual({ 'bstack:options': { testhubBuildUuid: buildHashedId, buildProductMap: productMap } })
    })

    it('should evaluate and set buildIdentifier from service options', async () => {
        const caps: any = { chromeBrowser: { capabilities: { 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: 'test ${BUILD_NUMBER}' } } } }
        const service = new BrowserstackLauncher({ buildIdentifier: '#${BUILD_NUMBER}' } as BrowserstackConfig & Options.Testrunner, caps, config)
        const capabilities = { chromeBrowser: { capabilities: { 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: 'test ${BUILD_NUMBER}' } } } }

        vi.spyOn(utils, 'getCiInfo').mockReturnValueOnce(null)
        vi.spyOn(service, '_getLocalBuildNumber').mockImplementation(() => { return '1' })

        await service.onPrepare(config, capabilities)
        expect(capabilities.chromeBrowser.capabilities).toEqual({ 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#1', testhubBuildUuid: buildHashedId, buildProductMap: productMap } })
    })

    it('should add "browserstack.buildIdentifier" property in capabilities if no "bstack:options" and buildIdentifier present in capabilities', async () => {
        const capabilities = [{ build: 'browserstack wdio build', 'browserstack.buildIdentifier': '#${BUILD_NUMBER}' }]
        const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, capabilities as Capabilities.RemoteCapability, {
            user: 'foobaruser',
            key: '12345678901234567890',
            capabilities: []
        })

        vi.spyOn(utils, 'getCiInfo').mockReturnValueOnce(null)
        vi.spyOn(service, '_getLocalBuildNumber').mockImplementation(() => { return '1' })

        await service.onPrepare(config, capabilities)
        expect(capabilities).toEqual([{
            build: 'browserstack wdio build',
            'browserstack.buildIdentifier': '#1',
            'browserstack.local': true,
            'browserstack.wdioService': pkg.version,
            'browserstack.testhubBuildUuid': buildHashedId,
            'browserstack.buildProductMap': productMap
        }])
    })

    it('should add the "browserstack.buildIdentifier" property in capabilities if no "bstack:options" and passing buildIdentifier in service options', async () => {
        const capabilities = [{ build: 'browserstack wdio build' }]
        const service = new BrowserstackLauncher({
            buildIdentifier: '#${BUILD_NUMBER}',
        } as BrowserstackConfig & Options.Testrunner, capabilities as Capabilities.RemoteCapability, {
            user: 'foobaruser',
            key: '12345678901234567890',
            capabilities: []
        })

        vi.spyOn(utils, 'getCiInfo').mockReturnValueOnce(null)
        vi.spyOn(service, '_getLocalBuildNumber').mockImplementation(() => { return '1' })

        await service.onPrepare(config, capabilities)
        expect(capabilities).toEqual([{
            build: 'browserstack wdio build',
            'browserstack.buildIdentifier': '#1',
            'browserstack.wdioService': pkg.version,
            'browserstack.testhubBuildUuid': buildHashedId,
            'browserstack.buildProductMap': productMap
        }])
    })

    it('should not add "browserstack.buildIdentifier" property in capabilities if no "bstack:options" and "build" not present', async () => {
        const capabilities = [{}]
        const service = new BrowserstackLauncher({
            buildIdentifier: '#${BUILD_NUMBER}',
        } as BrowserstackConfig & Options.Testrunner, capabilities as Capabilities.RemoteCapability, {
            user: 'foobaruser',
            key: '12345678901234567890',
            capabilities: []
        })
        vi.spyOn(service, '_getLocalBuildNumber').mockImplementation(() => { return '1' })

        await service.onPrepare(config, capabilities)
        expect(capabilities).toEqual([{ 'browserstack.wdioService': pkg.version, 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap }])
    })

    it('should delete "browserstack.buildIdentifier" property from capabilities if no "bstack:options" and "build" not present', async () => {
        const capabilities = [{ 'browserstack.buildIdentifier': '#${BUILD_NUMBER}' }]
        const service = new BrowserstackLauncher({
            buildIdentifier: '#${BUILD_NUMBER}',
        } as BrowserstackConfig & Options.Testrunner, capabilities as Capabilities.RemoteCapability, {
            user: 'foobaruser',
            key: '12345678901234567890',
            capabilities: []
        })
        vi.spyOn(service, '_getLocalBuildNumber').mockImplementation(() => { return '1' })

        await service.onPrepare(config, capabilities)
        expect(capabilities).toEqual([{ 'browserstack.wdioService': pkg.version, 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap }])
    })

    it('should reject if local.start throws an error', () => {
        const service = new BrowserstackLauncher(options as any, caps, config)
        mockStart.mockImplementationOnce((_: never, cb: Function) => cb(error))

        return expect(service.onPrepare(config, caps)).rejects.toThrow(error)
            .then(() => expect(service.browserstackLocal?.start).toHaveBeenCalled())
    })

    it('should successfully resolve if local.start is successful', async () => {
        const logInfoMock = vi.spyOn(log, 'info')
        const options: BrowserstackConfig = { browserstackLocal: true }
        const service = new BrowserstackLauncher(options as any, caps, config)

        await service.onPrepare(config, caps)
        expect(service.browserstackLocal?.start).toHaveBeenCalled()
        await sleep(100)
        expect(logInfoMock.mock.calls[1][0])
            .toContain('Browserstack Local successfully started after')
    })

    it('should correctly set up this-binding for local.start', async () => {
        const service = new BrowserstackLauncher(options as any, caps, config)
        await service.onPrepare(config, caps)
        expect(mockStart).toHaveBeenCalled()
        vi.clearAllMocks()
    })

    it('should launch testhub build when accessibility is true', async () => {
        const launchTestSessionSpy = vi.spyOn(utils, 'launchTestSession')
        const serviceOptions = { accessibility: true, testObservability: false }
        const service = new BrowserstackLauncher(serviceOptions as any, caps, config)
        vi.spyOn(service, '_updateObjectTypeCaps').mockImplementation(() => {})
        await service.onPrepare(config, caps)
        expect(launchTestSessionSpy).toHaveBeenCalledOnce()
    })

    it('should launch testhub build when observability is true', async () => {
        const launchTestSessionSpy = vi.spyOn(utils, 'launchTestSession')
        const serviceOptions = { accessibility: false, testObservability: true }
        const service = new BrowserstackLauncher(serviceOptions as any, caps, config)
        vi.spyOn(service, '_updateObjectTypeCaps').mockImplementation(() => {})
        await service.onPrepare(config, caps)
        expect(launchTestSessionSpy).toHaveBeenCalledOnce()
    })

    it('should add accessibility options after filtering not allowed caps', async () => {
        const launchTestSessionSpy = vi.spyOn(utils, 'launchTestSession')
        const caps: any = [{ 'bstack:options': {
            buildName: 'browserstack wdio build'
        } },
        { 'bstack:options': {
            buildName: 'browserstack wdio build'
        } }]
        const serviceOptions = { accessibility: true, accessibilityOptions: { wcagVersion: 'wcag2aa', includeTagsInTestingScope: ['@P1'] } }
        const service = new BrowserstackLauncher(serviceOptions as any, caps, config)
        const capabilities = [{ 'bstack:options': { buildName: 'browserstack wdio build' } }, { 'bstack:options': { buildName: 'browserstack wdio build' } }]
        await service.onPrepare(config, capabilities)
        expect(launchTestSessionSpy).toHaveBeenCalledOnce()
        expect(capabilities[0]['bstack:options']).toEqual({ buildName: 'browserstack wdio build', accessibilityOptions: { wcagVersion: 'wcag2aa' }, testhubBuildUuid: buildHashedId, buildProductMap: productMap })
        vi.clearAllMocks()
    })
})

describe('onComplete', () => {
    it('should do nothing if browserstack local is turned on, but not running', () => {
        const service = new BrowserstackLauncher({} as any, [{}] as any, {} as any)
        service.browserstackLocal = new Local()
        const BrowserstackLocalIsRunningSpy = vi.spyOn(service.browserstackLocal, 'isRunning')
        BrowserstackLocalIsRunningSpy.mockImplementationOnce(() => false)
        service.onComplete()
        expect(service.browserstackLocal.stop).not.toHaveBeenCalled()
    })

    it('should kill the process if forcedStop is true', async () => {
        const service = new BrowserstackLauncher({ forcedStop: true } as any, [{}] as any, {} as any)
        service.browserstackLocal = new Local()
        service.browserstackLocal.pid = 102

        const killSpy = vi.spyOn(process, 'kill').mockImplementationOnce((pid) => pid as any)
        expect(await service.onComplete()).toEqual(102)
        expect(killSpy).toHaveBeenCalled()
        expect(service.browserstackLocal.stop).not.toHaveBeenCalled()
    })

    it('should reject with an error, if local.stop throws an error', () => {
        const service = new BrowserstackLauncher({} as any, [{ browserName: '' }] as any, {} as any)
        service.browserstackLocal = new Local()
        const BrowserstackLocalStopSpy = vi.spyOn(service.browserstackLocal, 'stop')
        BrowserstackLocalStopSpy.mockImplementationOnce((cb) => cb(error))
        return expect(service.onComplete()).rejects.toThrow(error)
            .then(() => expect(service.browserstackLocal?.stop).toHaveBeenCalled())
    })

    it('should properly resolve if everything works', () => {
        const service = new BrowserstackLauncher({} as any, [{}] as any, {} as any)
        service.browserstackLocal = new Local()
        return expect(service.onComplete()).resolves.toBe(undefined)
            .then(() => expect(service.browserstackLocal?.stop).toHaveBeenCalled())
    })

    it('should stop accessibility test run on complete', () => {
        const stopBuildUpstreamSpy = vi.spyOn(utils, 'stopBuildUpstream')
        vi.spyOn(utils, 'isAccessibilityAutomationSession').mockReturnValue(true)

        const service = new BrowserstackLauncher({} as any, [{}] as any, {} as any)
        service.onComplete()
        expect(stopBuildUpstreamSpy).toHaveBeenCalledTimes(1)
    })
})

describe('constructor', () => {
    const options: BrowserstackConfig = { }
    const config = {
        user: 'foobaruser',
        key: '12345678901234567890',
        capabilities: [],
        specs: []
    }

    it('should add the "browserstack.wdioService" property to an array of capabilities if no "bstack:options"', async () => {
        const caps: any = [{}, {}]
        new BrowserstackLauncher(options as any, caps, config)

        expect(caps).toEqual([
            { 'browserstack.wdioService': pkg.version },
            { 'browserstack.wdioService': pkg.version }
        ])
    })

    it('should add the "wdioService" property to an array of capabilities inside "bstack:options" if "bstack:options" present', async () => {
        const caps: any = [{ 'bstack:options': {} }, { 'bstack:options': {} }]
        new BrowserstackLauncher(options as any, caps, config)

        expect(caps).toEqual([
            { 'bstack:options': { wdioService: pkg.version } },
            { 'bstack:options': { wdioService: pkg.version } }
        ])
    })

    it('should add the "wdioService" property to an array of capabilities inside "bstack:options" if any extension cap present', async () => {
        const caps: any = [{ 'moz:firefoxOptions': {} }, { 'goog:chromeOptions': {} }]
        new BrowserstackLauncher(options as any, caps, config)

        expect(caps).toEqual([
            { 'bstack:options': { wdioService: pkg.version }, 'moz:firefoxOptions': {} },
            { 'bstack:options': { wdioService: pkg.version }, 'goog:chromeOptions': {} }
        ])
    })

    it('should add the "wdioService" property to object of capabilities inside "bstack:options" if "bstack:options" present', async () => {
        const caps: any = { browserA: { capabilities: { 'goog:chromeOptions': {}, 'bstack:options': {} } } }
        new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)

        expect(caps).toEqual({ 'browserA': { 'capabilities': { 'bstack:options': { 'wdioService': pkg.version }, 'goog:chromeOptions': {} } } })
    })

    it('should add the "wdioService" property to object of capabilities inside "bstack:options" if any extension cap present', async () => {
        const caps: any = { browserA: { capabilities: { 'goog:chromeOptions': {} } } }
        new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)

        expect(caps).toEqual({ 'browserA': { 'capabilities': { 'bstack:options': { 'wdioService': pkg.version }, 'goog:chromeOptions': {} } } })
    })

    it('should add the "wdioService" property to object of capabilities inside "bstack:options" if any extension cap not present', async () => {
        const caps: any = { browserA: { capabilities: {} } }
        new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)

        expect(caps).toEqual({ 'browserA': { 'capabilities': { 'browserstack.wdioService': pkg.version } } })
    })

    it('should add the "accessibility" property to an array of capabilities inside "bstack:options" if "bstack:options" present', async () => {
        const serviceOptions = { accessibility: true }
        const caps: any = [{ 'bstack:options': {} }, { 'bstack:options': {} }]
        new BrowserstackLauncher(serviceOptions as any, caps, config)

        expect(caps).toEqual([
            { 'bstack:options': { wdioService: pkg.version, accessibility: true } },
            { 'bstack:options': { wdioService: pkg.version, accessibility: true } }
        ])
    })

    it('should add the "accessibility" property to an array of capabilities inside "bstack:options" if any extension cap present', async () => {
        const serviceOptions = { accessibility: true }
        const caps: any = [{ 'moz:firefoxOptions': {} }, { 'goog:chromeOptions': {} }]
        new BrowserstackLauncher(serviceOptions as any, caps, config)

        expect(caps).toEqual([
            { 'bstack:options': { wdioService: pkg.version, accessibility: true }, 'moz:firefoxOptions': {} },
            { 'bstack:options': { wdioService: pkg.version, accessibility: true }, 'goog:chromeOptions': {} }
        ])
    })

    it('should add the "accessibility" property to object of capabilities inside "bstack:options" if "bstack:options" present', async () => {
        const serviceOptions = { accessibility: true }
        const caps: any = { browserA: { capabilities: { 'goog:chromeOptions': {}, 'bstack:options': {} } } }
        new BrowserstackLauncher(serviceOptions as BrowserstackConfig & Options.Testrunner, caps, config)

        expect(caps).toEqual({ 'browserA': { 'capabilities': { 'bstack:options': { 'wdioService': pkg.version, 'accessibility': true }, 'goog:chromeOptions': {} } } })
    })

    it('should add the "accessibility" property to object of capabilities inside "bstack:options" if any extension cap present', async () => {
        const serviceOptions = { accessibility: true }
        const caps: any = { browserA: { capabilities: { 'goog:chromeOptions': {} } } }
        new BrowserstackLauncher(serviceOptions as BrowserstackConfig & Options.Testrunner, caps, config)

        expect(caps).toEqual({ 'browserA': { 'capabilities': { 'bstack:options': { 'wdioService': pkg.version, 'accessibility': true }, 'goog:chromeOptions': {} } } })
    })

    it('update spec list if it is a rerun', async () => {
        process.env[RERUN_ENV] = 'true'
        process.env[RERUN_TESTS_ENV] = 'demo1.test.js,demo2.test.js'

        const caps: any = [{ 'bstack:options': {} }, { 'bstack:options': {} }]
        new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)

        expect(config.specs).toEqual(['demo1.test.js', 'demo2.test.js'])

        delete process.env[RERUN_ENV]
        delete process.env[RERUN_TESTS_ENV]
    })

    describe('#non-bstack session', () => {
        const spy = vi.spyOn(utils, 'isBStackSession')
        it('should not add service version to caps', async () => {
            spy.mockImplementationOnce(() => false)
            const caps: any = [{}]
            new BrowserstackLauncher(options as any, caps, config)
            expect(caps).toEqual([{}])
        })
        spy.mockImplementation(() => true)
    })
})

describe('_updateCaps', () => {
    const options: BrowserstackConfig = { browserstackLocal: true }
    const caps: any = [{}]
    const config = {
        user: 'foobaruser',
        key: '12345678901234567890',
        capabilities: []
    }

    it('should throw an error if "capabilities" is not an object/array', () => {
        const service = new BrowserstackLauncher(options as any, caps, config)
        const capabilities = 1

        expect(() => service._updateCaps(capabilities as any, 'local'))
            .toThrow(new SevereServiceError('Capabilities should be an object or Array!'))
    })

    it('should update the local cap in capabilities', () => {
        const options: BrowserstackConfig = { browserstackLocal: true }
        const service = new BrowserstackLauncher(options as any, caps, config)

        service._updateCaps(caps, 'local')
        expect(caps[0]['browserstack.local']).toEqual(true)
    })

    it('should update the localIdentifier cap in capabilities if present in opts', () => {
        const options: BrowserstackConfig = { browserstackLocal: true, opts: { localIdentifier: 'wdio1' } }
        const service = new BrowserstackLauncher(options as any, caps, config)

        service._updateCaps(caps, 'localIdentifier', 'wdio1')
        expect(caps[0]['browserstack.localIdentifier']).toContain('wdio1')
    })

    it('should update the buildIdentifier cap in capabilities', () => {
        const options: BrowserstackConfig = { browserstackLocal: true }
        const service = new BrowserstackLauncher(options as any, caps, config)

        service._updateCaps(caps, 'buildIdentifier', '#1')
        expect(caps[0]['browserstack.buildIdentifier']).toEqual('#1')
    })

    it('should update the buildIdentifier cap if bstack:options is not present in caps array', () => {
        const options: BrowserstackConfig = { browserstackLocal: true }
        const caps: any = [{ 'ms:edgeOptions': {} }]
        const service = new BrowserstackLauncher(options as any, caps, config)

        service._updateCaps(caps, 'buildIdentifier', '#1')
        expect(caps[0]['bstack:options']['buildIdentifier']).toEqual('#1')
    })

    it('should update buildidentifier in caps object if bstack:options is present', () => {
        const options: BrowserstackConfig = { browserstackLocal: true }
        const caps = { chromeBrowser: { capabilities: { 'goog:chromeOptions': {}, 'bstack:options': { buildIdentifier: '123' } } } }
        const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)

        service._updateCaps(caps, 'buildIdentifier', '#1')
        expect(caps.chromeBrowser.capabilities['bstack:options']).toEqual({ 'wdioService': pkg.version, buildIdentifier: '#1' })
    })

    it('should update buildidentifier in caps object if bstack:options is not present', () => {
        const options: BrowserstackConfig = { browserstackLocal: true }
        const caps = { chromeBrowser: { capabilities: {} } }
        const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)

        service._updateCaps(caps, 'buildIdentifier', '#1')
        expect(caps.chromeBrowser.capabilities).toEqual({ 'browserstack.wdioService': pkg.version, 'browserstack.buildIdentifier': '#1' })
    })

    it('should delete buildidentifier in caps array if value not passed in _updateCaps', () => {
        const options: BrowserstackConfig = { browserstackLocal: true }
        const caps = [{ 'bstack:options': { buildIdentifier: '1234' } }]
        const service = new BrowserstackLauncher(options as any, caps as any, config)

        service._updateCaps(caps as any, 'buildIdentifier')
        expect(caps[0]['bstack:options']).toEqual({ 'wdioService': pkg.version })
    })

    it('should delete buildidentifier in caps object if value not passed in _updateCaps', () => {
        const options: BrowserstackConfig = { browserstackLocal: true }
        const caps = { chromeBrowser: { capabilities: { 'bstack:options': { buildIdentifier: '123' } } } }
        const service = new BrowserstackLauncher(options as any, caps as any, config)

        service._updateCaps(caps as any, 'buildIdentifier')
        expect(caps.chromeBrowser.capabilities['bstack:options']).toEqual({ 'wdioService': pkg.version })
    })

    it('should delete buildidentifier in caps object if value not passed in _updateCaps', () => {
        const options: BrowserstackConfig = { browserstackLocal: true }
        const caps = { chromeBrowser: { capabilities: { 'browserstack.buildIdentifier': '#1' } } }
        const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)

        service._updateCaps(caps, 'buildIdentifier')
        expect(caps.chromeBrowser.capabilities).toEqual({ 'browserstack.wdioService': pkg.version })
    })

    it('should delete buildidentifier in caps object if value not passed in _updateCaps', () => {
        const options: BrowserstackConfig = { browserstackLocal: true }
        const caps: any = [{ 'browserstack.buildIdentifier': '#1' }]
        const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)

        service._updateCaps(caps, 'buildIdentifier')
        expect(caps[0]).toEqual({ 'browserstack.wdioService': pkg.version })
    })

    it('should update localIdentifier in caps object if extension cap is present', () => {
        const options: BrowserstackConfig = { browserstackLocal: true, opts: { localIdentifier: 'wdio1' }  }
        const caps: any = { chromeBrowser: { capabilities: { 'goog:chromeOptions': {} } } }
        const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)

        service._updateCaps(caps, 'localIdentifier', 'wdio1')
        expect(caps.chromeBrowser.capabilities['bstack:options']).toEqual({ 'wdioService': pkg.version, localIdentifier: 'wdio1' })
    })
})

describe('_updateObjectTypeCaps', () => {
    const options: BrowserstackConfig = { accessibilityOptions: { wcagVersion: 'wcag2a' } }
    const caps: any = [{}]
    const config = {
        user: 'foobaruser',
        key: '12345678901234567890',
        capabilities: []
    }

    it('should update the accessibilityOptions cap in capabilities', () => {
        const service = new BrowserstackLauncher(options as any, caps, config)

        service._updateObjectTypeCaps(caps, 'accessibilityOptions', { includeIssueType: { bestPractice: true, needsReview: true } })
        expect(caps[0]['browserstack.accessibilityOptions'].includeIssueType).toEqual({ bestPractice: true, needsReview: true })
    })

    it('should update the accessibilityOptions cap if bstack:options is not present in caps array', () => {
        const caps: any = [{ 'ms:edgeOptions': {} }]
        const service = new BrowserstackLauncher(options as any, caps, config)

        service._updateObjectTypeCaps(caps, 'accessibilityOptions', { includeIssueType: { bestPractice: true, needsReview: true } })
        expect(caps[0]['bstack:options']['accessibilityOptions'].includeIssueType).toEqual({ bestPractice: true, needsReview: true })
    })

    it('should update accessibilityOptions in caps object if bstack:options is present', () => {
        const caps = { chromeBrowser: { capabilities: { 'goog:chromeOptions': {}, 'bstack:options': {} } } }
        const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)

        service._updateObjectTypeCaps(caps, 'accessibilityOptions', { includeIssueType: { bestPractice: true, needsReview: true } })
        expect(caps.chromeBrowser.capabilities['bstack:options']).toEqual({ 'wdioService': pkg.version, 'accessibilityOptions': { 'includeIssueType': { 'bestPractice': true, 'needsReview': true } } })
    })

    it('should update accessibilityOptions in caps object if bstack:options is not present', () => {
        const caps = { chromeBrowser: { capabilities: {} } }
        const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)

        service._updateObjectTypeCaps(caps, 'accessibilityOptions', { includeIssueType: { bestPractice: true, needsReview: true } })
        expect(caps.chromeBrowser.capabilities).toEqual({ 'browserstack.wdioService': pkg.version, 'browserstack.accessibilityOptions': { includeIssueType: { bestPractice: true, needsReview: true } } })
    })

    it('should set chromeOptions if capType is goog:chromeOptions and no existing options are present', () => {
        const value = { args: ['--disable-gpu'] }
        vi.spyOn(utils, 'validateCapsWithNonBstackA11y').mockImplementation(() => true)
        const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)
        service._updateObjectTypeCaps(caps, 'goog:chromeOptions', value)
        expect(caps[0]['goog:chromeOptions']).toEqual(value)
    })

    it('should merge chromeOptions if capType is goog:chromeOptions and value is provided', () => {
        const caps: any = [{ 'goog:chromeOptions': { args: ['--headless'] } }]
        const value = { args: ['--disable-gpu'] }
        vi.spyOn(utils, 'validateCapsWithNonBstackA11y').mockImplementation(() => true)
        const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)
        service._updateObjectTypeCaps(caps, 'goog:chromeOptions', value)
        expect(caps[0]['goog:chromeOptions']).toEqual({ args: ['--headless', '--disable-gpu'] })
    })

    it('should update goog:chromeOptions in caps object if value is provided', () => {
        const caps = { chromeBrowser: { capabilities: { 'goog:chromeOptions': { args: ['--headless'] }, 'bstack:options': {} } } }
        const value = { args: ['--disable-gpu'] }
        vi.spyOn(utils, 'validateCapsWithNonBstackA11y').mockImplementation(() => true)
        const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)
        service._updateObjectTypeCaps(caps, 'goog:chromeOptions', value)
        expect(caps.chromeBrowser.capabilities['goog:chromeOptions']).toEqual({ args: ['--headless', '--disable-gpu'] })
    })

    it('should delete accessibilityOptions in caps array if value not passed in _updateObjectTypeCaps', () => {
        const caps = [{ 'bstack:options': { accessibilityOptions: { wcagVersion: 'wcag2a' } } }]
        const service = new BrowserstackLauncher(options as any, caps as any, config)

        service._updateObjectTypeCaps(caps, 'accessibilityOptions')
        expect(caps[0]['bstack:options']).toEqual({ 'wdioService': pkg.version })
    })

    it('should delete accessibilityOptions in caps object if value not passed in _updateObjectTypeCaps', () => {
        const caps = { chromeBrowser: { capabilities: { 'bstack:options': { accessibilityOptions: { wcagVersion: 'wcag2a' } } } } }
        const service = new BrowserstackLauncher(options as any, caps as any, config)

        service._updateObjectTypeCaps(caps, 'accessibilityOptions')
        expect(caps.chromeBrowser.capabilities['bstack:options']).toEqual({ 'wdioService': pkg.version })
    })

    it('should delete accessibilityOptions in caps object if value not passed in _updateObjectTypeCaps', () => {
        const caps = { chromeBrowser: { capabilities: { 'browserstack.accessibilityOptions': { wcagVersion: 'wcag2a' } } } }
        const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)

        service._updateObjectTypeCaps(caps, 'accessibilityOptions')
        expect(caps.chromeBrowser.capabilities).toEqual({ 'browserstack.wdioService': pkg.version })
    })

    it('should delete accessibilityOptions in caps object if value not passed in _updateCaps', () => {
        const caps: any = [{ 'browserstack.accessibilityOptions': { wcagVersion: 'wcag2a' } }]
        const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)

        service._updateObjectTypeCaps(caps, 'accessibilityOptions')
        expect(caps[0]).toEqual({ 'browserstack.wdioService': pkg.version })
    })
})

describe('_validateApp', () => {
    const caps: any = [{}]
    const config = {
        user: 'foobaruser',
        key: '12345',
        capabilities: []
    }

    it('should use id as app value', async() => {
        const options: BrowserstackConfig = { app: { id: 'bs://<app-id>' } }
        const service = new BrowserstackLauncher(options as any, caps, config)

        const app:any = await service._validateApp(options.app as any)
        expect(app).toEqual({ app: 'bs://<app-id>', custom_id: undefined })
    })

    it('should throw error if more than two property passed in appConfig', async() => {
        const options: BrowserstackConfig = { app: { custom_id: 'custom_id', id: 'bs://<app-id>' } }
        const service = new BrowserstackLauncher(options as any, caps, config)

        try {
            await service._validateApp(options.app as any)
        } catch (err) {
            const e = err as Error
            expect(e.message).toEqual(`keys ${Object.keys(options.app as any)} can't co-exist as app values, use any one property from
                            {id<string>, path<string>, custom_id<string>, shareable_id<string>}, only "path" and "custom_id" can co-exist.`)
        }
    })

    it('should throw error if property not matches path and custom_id in appConfig', async() => {
        const options: BrowserstackConfig = { app: { custom_id: 'custom_id', id: 'bs://<app-id>' } }
        const service = new BrowserstackLauncher(options as any, caps, config)

        try {
            await service._validateApp(options.app as any)
        } catch (err) {
            const e = err as Error
            expect(e.message).toEqual(`keys ${Object.keys(options.app as any)} can't co-exist as app values, use any one property from
                            {id<string>, path<string>, custom_id<string>, shareable_id<string>}, only "path" and "custom_id" can co-exist.`)
        }
    })

    it('should throw error if appConfig is invalid format', async() => {
        const options: BrowserstackConfig = { app: {} }
        const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)

        try {
            await service._validateApp(options.app as any)
        } catch (e: any){
            expect(e.message).toEqual('[Invalid format] app should be string or an object')
        }
    })

    it('should throw error if appConfig is invalid format', async() => {
        const options: BrowserstackConfig = { app: { key1: '2' } } as any
        const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)

        try {
            await service._validateApp(options.app as any)
        } catch (e: any){
            expect(e.message).toEqual(`[Invalid app property] supported properties are {id<string>, path<string>, custom_id<string>, shareable_id<string>}.
                        For more details please visit https://www.browserstack.com/docs/app-automate/appium/set-up-tests/specify-app ')`)
        }
    })
})

describe('_uploadApp', () => {
    const options: BrowserstackConfig = { app: '/path/to/app.apk' }
    const caps: any = [{}]
    const config = {
        user: 'foobaruser',
        key: '12345',
        capabilities: []
    }

    it('should upload the app and return app_url', async() => {
        vi.mocked(fetch).mockReturnValueOnce(Promise.resolve(Response.json({ app_url: 'bs://<app-id>' })))
        const service = new BrowserstackLauncher(options as any, caps, config)
        const res = await service._uploadApp(options.app as any)
        expect(res).toEqual({ app_url: 'bs://<app-id>' })
    })

    it('throw SevereServiceError if upload fails', async() => {
        vi.mocked(fetch).mockReturnValueOnce(Promise.resolve(Response.json({}, { status: 500 })))
        const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)

        try {
            await service._uploadApp(options.app as any)
        } catch (e: any) {
            expect(vi.mocked(fetch).mock.calls[0][1]?.method).toEqual('POST')
            expect(e.name).toEqual('SevereServiceError')
        }
    })
})

describe('_handleBuildIdentifier', () => {
    const options: BrowserstackConfig = { browserstackLocal: true }
    const config = {
        user: 'foobaruser',
        key: '12345678901234567890',
        capabilities: []
    }

    it('should update ${BUILD_NUMBER}', async() => {
        const caps: any = [{
            'bstack:options': {
                buildName: 'browserstack wdio build',
                buildIdentifier: '#${BUILD_NUMBER}'
            }
        }]
        const service = new BrowserstackLauncher(options as any, caps, config)

        vi.spyOn(utils, 'getCiInfo').mockReturnValueOnce(null)
        vi.spyOn(service, '_getLocalBuildNumber').mockReturnValueOnce('1')
        vi.spyOn(service, '_updateLocalBuildCache').mockImplementation(() => {})
        service._handleBuildIdentifier(caps)
        expect(caps[0]['bstack:options']?.buildIdentifier).toEqual('#1')
    })

    it('should update ${DATE_TIME}', async() => {
        const caps: any = [{
            'bstack:options': {
                buildName: 'browserstack wdio build',
                buildIdentifier: '${DATE_TIME}'
            }
        }]
        const service = new BrowserstackLauncher(options as any, caps, config)

        vi.spyOn(service, '_getLocalBuildNumber').mockReturnValueOnce(null)
        vi.spyOn(service, '_updateLocalBuildCache').mockImplementation(() => {})
        service._handleBuildIdentifier(caps)

        expect(caps[0]['bstack:options']?.buildIdentifier).not.toEqual('${DATE_TIME}')
    })

    it('should update ${DATE_TIME} and ${BUILD_NUMBER}', async() => {
        const caps: any = [{
            'bstack:options': {
                buildName: 'browserstack wdio build',
                buildIdentifier: '#${BUILD_NUMBER} ${DATE_TIME}'
            }
        }]
        const service = new BrowserstackLauncher(options as any, caps, config)

        vi.spyOn(service, '_getLocalBuildNumber').mockReturnValueOnce('1')
        vi.spyOn(service, '_updateLocalBuildCache').mockImplementation(() => {})
        service._handleBuildIdentifier(caps)

        expect(caps[0]['bstack:options']?.buildIdentifier).not.toEqual('${DATE_TIME}')
        expect(caps[0]['bstack:options']?.buildIdentifier).toContain('#1')
    })

    it('should update ${BUILD_NUMBER} in case of CI', async() => {
        process.env.JENKINS_URL = 'https://jenkins-url'
        process.env.JENKINS_HOME = '~/.jenkins'
        process.env.BUILD_NUMBER = '121'
        const caps: any = [{
            'bstack:options': {
                buildName: 'browserstack wdio build',
                buildIdentifier: '${BUILD_NUMBER}'
            }
        }]
        const service = new BrowserstackLauncher(options as any, caps, config)

        service._handleBuildIdentifier(caps)
        expect(caps[0]['bstack:options']?.buildIdentifier).toContain('CI 121')

        delete process.env.JENKINS_URL
        delete process.env.JENKINS_HOME
        delete process.env.BUILD_NUMBER
    })

    it('should delete buildIdentifier if buildName is not present in caps', async() => {
        const caps: any = [{
            'bstack:options': {
                buildIdentifier: '#${BUILD_NUMBER}'
            }
        }]
        const updatedcaps: any = [{
            'bstack:options': {
                wdioService: pkg.version
            }
        }]
        const service = new BrowserstackLauncher(options as any, caps, config)

        service._handleBuildIdentifier(caps)
        expect(caps[0]).toMatchObject(updatedcaps[0])
    })

    it('should delete buildIdentifier if BROWSERSTACK_BUILD_NAME is defined as env var', async() => {
        process.env.BROWSERSTACK_BUILD_NAME = 'browserstack wdio build'
        const caps: any = [{
            'bstack:options': {
                buildIdentifier: '#${BUILD_NUMBER}'
            }
        }]
        const updatedcaps: any = [{
            'bstack:options': {
                wdioService: pkg.version
            }
        }]
        const service = new BrowserstackLauncher(options as any, caps, config)

        service._handleBuildIdentifier(caps)
        expect(caps[0]).toMatchObject(updatedcaps[0])
        delete process.env.BROWSERSTACK_BUILD_NAME
    })

    it('should not evaluate buildIdentifier if buildIdentifier is not present in the caps', async() => {
        const caps: any = [{}]
        const updatedcaps: any = [{ 'browserstack.wdioService': pkg.version }]
        const service = new BrowserstackLauncher(options as any, caps, config)

        service._handleBuildIdentifier(caps)
        expect(caps[0]).toMatchObject(updatedcaps[0])
    })

    it('should return if localBuildNumber is null', async() => {
        const caps: any = [{
            'bstack:options': {
                buildName: 'browserstack wdio build',
                buildIdentifier: '#${BUILD_NUMBER}'
            }
        }]
        const service = new BrowserstackLauncher(options as any, caps, config)
        vi.spyOn(service, '_getLocalBuildNumber').mockReturnValueOnce(null)
        vi.spyOn(utils, 'getCiInfo').mockReturnValueOnce(null)

        service._handleBuildIdentifier(caps)
        expect(caps[0]['bstack:options']?.buildIdentifier).toEqual('#${BUILD_NUMBER}')
    })
})

describe('_getLocalBuildNumber', () => {
    const options: BrowserstackConfig = { browserstackLocal: true }
    const config = {
        user: 'foobaruser',
        key: '12345',
        capabilities: []
    }
    const caps: any = [{
        'bstack:options': {
            buildName: 'browserstack wdio build',
            buildIdentifier: '#${BUILD_NUMBER}'
        }
    }]
    const service = new BrowserstackLauncher(options as any, caps, config)
    vi.spyOn(fs, 'existsSync').mockReturnValue(true)

    it('returns 1 in case of buildName key not present in json file', async() => {
        vi.spyOn(fs, 'existsSync').mockReturnValue(true)
        vi.spyOn(fs, 'readFileSync').mockReset().mockReturnValue(JSON.stringify({ 'browserstack wdio build test': { 'identifier': 2 } }))
        vi.spyOn(service, '_updateLocalBuildCache').mockImplementation(() => {})
        const buildNumber = service._getLocalBuildNumber()
        expect(buildNumber).toEqual('1')
    })

    it('returns new identifier in case of buildName key is present in json file', async() => {
        vi.spyOn(fs, 'existsSync').mockReturnValue(true)
        vi.spyOn(fs, 'readFileSync').mockReset().mockReturnValue(JSON.stringify({ 'browserstack wdio build': { 'identifier': 2 } }))
        vi.spyOn(service, '_updateLocalBuildCache').mockImplementation(() => {})
        const buildNumber = service._getLocalBuildNumber()
        expect(buildNumber).toEqual('3')
    })

    it('returns null in case of caught exception', async() => {
        vi.spyOn(fs, 'existsSync').mockReturnValue(true)
        vi.spyOn(service, '_updateLocalBuildCache').mockImplementation(() => { throw new Error('Unable to parse JSON file') })
        const buildNumber = service._getLocalBuildNumber()
        expect(buildNumber).toEqual(null)
    })
})

describe('_updateLocalBuildCache', () => {
    const options: BrowserstackConfig = { browserstackLocal: true }
    const config = {
        user: 'foobaruser',
        key: '12345',
        capabilities: []
    }
    const caps: any = [{
        'bstack:options': {
            buildName: 'browserstack wdio build test',
            buildIdentifier: '#${BUILD_NUMBER}'
        }
    }]
    const service = new BrowserstackLauncher(options as any, caps, config)

    it('updates buildIdentifier in json file', async() => {
        vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {})
        vi.spyOn(fs, 'readFileSync').mockReset().mockReturnValue(JSON.stringify({ 'browserstack wdio build test' : { 'identifier' : 3 } }))
        const browserstackFolderPath = path.join(os.homedir(), '.browserstack')
        const filePath = path.join(browserstackFolderPath, '.build-name-cache.json')

        service._updateLocalBuildCache(filePath, 'browserstack wdio build test', 3)
        const buildCacheFileData = fs.readFileSync(filePath)

        const parsedBuildCacheFileData = JSON.parse(buildCacheFileData.toString())
        expect(parsedBuildCacheFileData['browserstack wdio build test']['identifier']).toEqual(3)
    })

    it('should not update buildIdentifier in json file', async() => {
        const writeFileSyncSpy = vi.spyOn(fs, 'writeFileSync')
        writeFileSyncSpy.mockImplementation(() => {})
        vi.spyOn(fs, 'readFileSync').mockReset().mockReturnValue(JSON.stringify({ 'browserstack wdio build test' : { 'identifier' : 3 } }))
        const browserstackFolderPath = path.join(os.homedir(), '.browserstack')
        const filePath = path.join(browserstackFolderPath, '.build-name-cache.json')

        service._updateLocalBuildCache(filePath, undefined, 3)
        expect(writeFileSyncSpy).not.toHaveBeenCalled()
    })
})

describe('_uploadServiceLogs', () => {
    const options: BrowserstackConfig = { }
    const config = {
        user: 'foobaruser',
        key: '12345',
        capabilities: []
    }
    const caps: any = [{
        'bstack:options': {
            buildName: 'browserstack wdio build test',
            buildIdentifier: '#${BUILD_NUMBER}'
        }
    }]

    it('get observability build id', async() => {
        process.env[BROWSERSTACK_TESTHUB_UUID] = 'obs123'
        expect(service._getClientBuildUuid()).toEqual('obs123')
        delete process.env[BROWSERSTACK_TESTHUB_UUID]
    })

    const service = new BrowserstackLauncher(options as any, caps, config)
})

describe('_getClientBuildUuid', () => {
    const options: BrowserstackConfig = { }
    const config = {
        user: 'foobaruser',
        key: '12345',
        capabilities: []
    }
    const caps: any = [{
        'bstack:options': {
            buildName: 'browserstack wdio build test',
            buildIdentifier: '#${BUILD_NUMBER}'
        }
    }]

    const service = new BrowserstackLauncher(options as any, caps, config)

    it('get observability build id', async() => {
        process.env[BROWSERSTACK_TESTHUB_UUID] = 'obs123'
        expect(service._getClientBuildUuid()).toEqual('obs123')
        delete process.env[BROWSERSTACK_TESTHUB_UUID]
    })

    it('get randomly generated id if both the conditions fail', async() => {
        vi.mock('uuid', () => ({ v4: () => '123456789' }))
        expect(service._getClientBuildUuid()).toEqual('123456789')
    })
})