import {ISingleAlertSettings} from "../interfaces.js";
import {waitForTimeout, isEnvEnabled} from "./common-service.js";
import log from "./log.js"
import kleur from "kleur";
import {
    AddAlertInvocationError,
    InvalidSymbolError,
    NoInputFoundError,
    SelectionError,
    ErrorWithScreenShot
} from "../classes.js";
import RegexParser from "regex-parser"
import {accessSync, constants, writeFileSync} from "fs";
import puppeteer, {Browser, executablePath} from "puppeteer";
import path from "path";
import {mkdir} from "fs/promises";
import exp from "constants";

// data-dialog-name="gopro"

const screenshot = isEnvEnabled(process.env.SCREENSHOT)


const doXpath = async (page, selector: string) => {

    /* istanbul ignore next */
    const response = await page.evaluate((sel) => {
        const el = document.evaluate(sel,
            document,
            null,
            XPathResult.FIRST_ORDERED_NODE_TYPE,
            null
        ).singleNodeValue as HTMLElement

        return !!el

    }, selector)
    return response
}

export const isXpathVisible = async (page, selector: string, screenShotOnFail = false) => {
    log.debug(kleur.gray(`...isXpathVisible?: ${kleur.yellow(selector)}`))
    const visible = await doXpath(page, selector)
    log.debug(`..isXpathVisible: ${visible}`)
    return visible
}

export const fetchFirstXPath = async (page, selector: string, timeout = 20000, screenshotOnFail = true) => {
    log.debug(kleur.gray(`...selector: ${kleur.yellow(selector)}`))
    try {
        await page.waitForSelector("xpath/." + selector, {timeout})
    } catch (e) {
        if (screenshotOnFail) await takeScreenshot(page, "waitForSelectorFailed")
        throw (e)
    }
    const elements = await page.$$("xpath/." + selector)
    return elements[0]
}


export const takeScreenshot = async (page, name: string = "unnamed") => {
    if (screenshot) {

        const username = await page.evaluate(() => {
            // @ts-ignore
            const user = window.user
            if (user) {
                return "_" + user.username
            } else {
                return ""
            }
        })

        const screenshotPath = `screenshot_${new Date().getTime()}${username}_${name}`
        log.debug(`saving screenshot: ${screenshotPath}`)
        await page.screenshot({
            path: screenshotPath + ".webp",
        });
        const html = await page.content()
        writeFileSync(screenshotPath + ".xml", html, {encoding: "utf-8"})
    }
}

export const minimizeFooterChartPanel = async (page) => {
    log.debug(`minimizing footer chart panel`)

    try {
        const footerPanelMinimizeButton = await fetchFirstXPath(page, `//div[@id='footer-chart-panel']//button[@data-name='toggle-visibility-button' and @data-active='false']`, 5000)
        footerPanelMinimizeButton.click()
        await waitForTimeout(.4);
    } catch (e) {
        log.debug("no minimize button found, footer chart panel must be hidden already")
    }
}

export const convertIntervalForTradingView = (interval: string) => {
    return interval.split("").filter((val) => val !== "m").join("")
}


export const configureInterval = async (interval: string, page) => {
    log.debug(`set ${kleur.blue("interval")}: ${kleur.yellow(interval)}`)
    await page.keyboard.press(",")
    await waitForTimeout(.5, "after pressing interval shortcut key");
    try {
        convertIntervalForTradingView(interval).split("").map((char) => page.keyboard.press(char))
    } catch (e) {
        throw Error("configuration: interval specified incorrectly, should be something like '5m' or '1h' - see documentation")
    }
    await page.keyboard.press('Enter')
}

// queries used on the alert conditions
const dropdownXpathQueries = {
    primaryLeft: "//span[@data-name='main-series-select']",
    primaryRight: "//span[@data-name='main-series-plot-select']",
    secondary: "//span[@data-name='operator-select']",

    // Only get listbox when there's either just one value
    // "//*[contains(@class, 'operatorRow-')]/..//span[@data-name='start-band-select']"
    // Or get when there's an upper and lower bound
    // "//legend[text()='Upper Bound']/../..//span[@data-name='start-band-select']"

    tertiaryLeft: "//*[contains(@class, 'operatorRow-')]/..//span[@data-name='start-band-select'] | //legend[text()='Upper bound']/../..//span[@data-name='start-band-select']",
    tertiaryRight: "//*[contains(@class, 'operatorRow-')]/..//span[@data-name='end-band-select'] | //legend[text()='Upper bound']/../..//span[@data-name='end-band-select']",

    quaternaryLeft: "//legend[text()='Lower bound']/../..//span[@data-name='start-band-select']",
    quaternaryRight: "//legend[text()='Lower bound']/../..//span[@data-name='end-band-select']",
}


const dropdownSoundXpathQueries = {
    nameTarget: "//button[@role='button' and @data-name='sound-title-select']",
    classicSection: "//button[@role='group' and @aria-expanded='false']//div[text()='Classic']",
    nameListItems: "//div[@data-name='popup-menu-container']//div[@role='option']//div[contains(@class, 'title-')]//div[contains(@class, 'title-')]",
    durationTarget: "//button[@data-name='sound-duration-select']",
    durationListItems: "//div[@data-name='popup-menu-container']//div[@role='option']//div[contains(@class, 'title-')]",
}


const inputXpathQueries = {

    tertiaryLeft: "//*[contains(@class, 'operatorRow-')]/..//input[@data-property-id='start-band-range'] | //legend[text()='Upper bound']/../..//input[@data-property-id='start-band-range']",
    tertiaryRight: "//*[contains(@class, 'operatorRow-')]/..//input[@data-property-id='end-band-range'] | //legend[text()='Upper bound']/../..//input[@data-property-id='end-band-range']",


    quaternaryLeft: "//*[contains(@class, 'operatorRow-')]/..//input[@data-property-id='start-band-range'] | //legend[text()='Lower bound']/../..//input[@data-property-id='start-band-range']",
    quaternaryRight: "//*[contains(@class, 'operatorRow-')]/..//input[@data-property-id='end-band-range'] | //legend[text()='Lower bound']/../..//input[@data-property-id='end-band-range']",
}

const readOnlyInputQueries = {
    tertiaryLeft: "(//div[contains(@class, 'tv-alert-dialog__group-item--left ') and contains(@class, 'js-second-operand-')])[1]//input[@type='text']",
    quaternaryLeft: "(//div[contains(@class, 'tv-alert-dialog__group-item--left ') and contains(@class, 'js-second-operand-')])[2]//input[@type='text']"
}


const alertActionCorresponding = {
    notifyOnApp: "notify-on-app",
    showPopup: "show-popup",
    sendEmail: "send-email",
    webhook: "webhook",
    playSound: "play-sound"
}


const clickInputAndDelete = async (page, inputElement) => {

    await inputElement.click()
    await waitForTimeout(.5)
    /* istanbul ignore next */
    await page.evaluate((el) => {
        el.value = ""
    }, inputElement)
    await waitForTimeout(.3)
}

export const launchBrowser = async (headless: boolean, url?: string): Promise<Browser> => {

    const userDataDir = path.join(process.cwd(), "user_data") // where chrome will store it's stuff

    try {
        accessSync(userDataDir, constants.W_OK)
    } catch {
        log.info(`Attempting to create directory for Chrome user data\n ${kleur.yellow(userDataDir)}`)
        await mkdir(userDataDir)
    }

    return puppeteer.launch({
        executablePath: executablePath(),
        headless: headless, userDataDir,
        defaultViewport: {width: 1920, height: 1080, isMobile: false, hasTouch: false},
        args: ['--no-sandbox',
            '--enable-experimental-web-platform-features', // adds support for :has selector in styleOverrides. In theory its not experimental in chrome 105
            '--disable-setuid-sandbox',
            '--disable-dev-shm-usage',
            '--disable-accelerated-2d-canvas',
            '--no-first-run',
            '--no-zygote',
            // '--single-process', // will cause it to die
            '--disable-gpu',
            headless && !url ? "" : `--app=${url}`,
            '--window-size=1920,1080', // otherwise headless doesn't work
            // '--incognito'
        ]
    })
}


export const login = async (page, username, pass, backupCode) => {

    try {
        const emailSignInButton = await fetchFirstXPath(page, `//div[@data-dialog-name='sign-in']//button[@name='Email']`, 5000)
        emailSignInButton.click()
        await waitForTimeout(1);
    } catch (e) {

        log.warn("no email toggle button showing!")
    }

    const usernameInput = await fetchFirstXPath(page, '//input[@name=\'id_username\']')
    await usernameInput.type(`${username}`)
    await waitForTimeout(.5);
    await takeScreenshot(page, "shouldbe_before_password_entry")
    const passwordInput = await fetchFirstXPath(page, '//input[@name=\'id_password\']')
    log.debug("typing password")
    await passwordInput.type(pass)
    await waitForTimeout(.5);
    const submitButton = await fetchFirstXPath(page, "//div[@data-dialog-name='sign-in']//button[contains(@class, 'submitButton')]")
    log.debug("clicking submit button")
    submitButton.click()

    await waitForTimeout(2);


    let twoFactorOrBackupCodeInput = null

    try {
        twoFactorOrBackupCodeInput = await fetchFirstXPath(page, "//div[@data-dialog-name='sign-in']//input[@id='id_code']", 5000)
    } catch (e) {
        log.warn("Two Factor Authentication / Backup Code input not showing")
    }

    if (twoFactorOrBackupCodeInput !== null) {
        if (!backupCode) {
            log.warn(`${kleur.yellow("MANUAL ACTION NEEDED: ")} You have 30 seconds to enter your 2FA or backup code`)
            await waitForTimeout(30 * 1000);
        } else {
            await twoFactorOrBackupCodeInput.type(`${backupCode}`)
            await waitForTimeout(3);
        }
    }

    let possibleErrorElement = null

    try {
        possibleErrorElement = await fetchFirstXPath(page,
            "//div[@data-dialog-name='sign-in']//div[contains(@class, 'mainProblem')]//div[contains(@class,'text-wrap')]/span",
            5000)
    } catch (e) {
        log.warn("doesn't seem like there's an error..")
    }

    if (possibleErrorElement) {
        const errorText = await page.evaluate(element => element.innerText, possibleErrorElement);
        await takeScreenshot(page, "loginError")
        throw new ErrorWithScreenShot(errorText, "loginError")
    }

}

export const logout = async (page) => {

    /* istanbul ignore next */
    await page.evaluate(() => {

        fetch("/accounts/logout/", {
            method: "POST",
            headers: {accept: "html"},
            credentials: "same-origin"
        }).then(res => {
            log.info(`Logged out of TradingView`)
        });
    })

    await page.reload({
        waitUntil: 'networkidle2'
    });
}

export const checkForInvalidSymbol = async (page, symbol: string) => {
    // this function could be used when navigating by typing or by the url using ?symbol=ASDF
    // now see if it's invalid symbol, could be multi-chart so check active
    if (await isXpathVisible(page, "//div[contains(@class,'chart-container') and contains(@class,' active')]//*/div[contains(@class, 'invalidSymbol') and not(contains(@class, 'js-hidden'))]")) {
        log.error("currently showing an invalid symbol")
        const invalidSymbolError = new InvalidSymbolError()
        invalidSymbolError.symbol = symbol
        throw invalidSymbolError
    }
}

export const navigateToSymbol = async (page, symbol: string) => {

    await page.keyboard.press('Escape')
    await waitForTimeout(.5);
    await page.keyboard.press('Escape')
    await waitForTimeout(.5);
    await page.keyboard.type(`A`, {delay: 0.3}) // just type a letter <- allows formulas to work, eg. 1/USD...
    await page.keyboard.press('Backspace');
    await waitForTimeout(.3);
    await page.keyboard.type(`${symbol}`, {delay: 0.3})
    await waitForTimeout(.3);
    await page.keyboard.press('Enter')

    await waitForTimeout(1.5);

}

const isMatch = (needle: string, haystack: string) => {

    if (needle.startsWith("/")) {
        log.debug(`Parsing what appears to be regular expression: ${kleur.yellow(needle)} ... Haystack: ${kleur.gray(haystack)}`)
        const regexp: RegExp = RegexParser(needle)
        return !!regexp.exec(haystack)
    } else {
        return haystack.indexOf(needle) > -1
    }
}

export const configureSingleAlertSettings = async (page, singleAlertSettings: ISingleAlertSettings) => {

    const {condition, name, expireOpenEnded, expireInterval, option, message, actions} = singleAlertSettings

    await takeScreenshot(page, "alert_begin_configure")

    try {
        const gotItButton = await fetchFirstXPath(page, "//button//span[contains(@data-overflow-tooltip-text, 'Got it')]", 1000, false)
        log.warn("Clicking 'Got it' informational bubble...")
        gotItButton.evaluate(e => e.click())
    } catch (e) {
        log.trace("Checked for 'Got it' button, but found nothing")
    }


    const selectFromDropDown = async (conditionToMatchArg, selector: string) => {
        log.debug(`..selectFromDropDown() using selector: ${kleur.yellow(selector)}`)

        await page.waitForSelector("xpath/." + selector, {timeout: 8000})
        const elements = await page.$$("xpath/." + selector)

        if (elements.length == 0) {
            log.warn("zero dropdown options found with selector")
            await takeScreenshot(page, "zero_dropdown_options")
        }

        let conditionToMatch = conditionToMatchArg
        let targetOccurrence = 0
        const match = conditionToMatch.match(/(.*?)\[(\d+)\]$/)
        // if match is not null, then the number to look for should be match[1]

        if (match) {
            conditionToMatch = match[1]
            targetOccurrence = Number.parseInt(match[2])
            log.debug(`Indexed condition used: ${kleur.yellow(conditionToMatchArg)}\n Setting occurrence to ${kleur.blue(targetOccurrence)}`)
        }

        log.debug(`searching menu for ${kleur.yellow(conditionToMatch)}`)

        let found = false
        let foundOptions = []
        let occurrenceCount = 0
        for (const el of elements) {
            /* istanbul ignore next */
            let optionText = await page.evaluate(element => element.innerText, el);
            optionText = optionText.replace(/[\u200B]/g, ''); // this is to remove invisible "zero width" characters like for the following:
            // Loner S​/​R (modified, 28, 5, Standard, -20, modified, 21, 3, 40, 10, 20, 5, 64, 1.5, both)
            foundOptions.push(optionText)
            if (isMatch(conditionToMatch, optionText)) {
                if (occurrenceCount == targetOccurrence) {
                    log.debug(`Found! Clicking ${kleur.yellow(optionText)}`)
                    found = true
                    el.click()
                    return;
                } else {
                    log.debug(`Matching option found, but not occurrenceCount ${kleur.blue(occurrenceCount)} not matching targetOccurrence `)
                    occurrenceCount += 1
                }

            }
        }
        if (!found) throw new SelectionError(conditionToMatch, foundOptions)

    }


    const performActualEntry = async (key) => {
        const conditionOrInputValue = String(condition[key]);
        log.debug(`Processing ${kleur.blue(key)}: ${kleur.yellow(conditionOrInputValue)}`)

        await waitForTimeout(.8);

        if (conditionOrInputValue !== "null" && String(conditionOrInputValue).length > 0) {

            try {
                log.debug(`Looking for DROPDOWN xpath of ${kleur.yellow(key)}`)
                const targetElement = await fetchFirstXPath(page, dropdownXpathQueries[key], 3000)
                // must be a dropdown...
                log.debug(`Found dropdown! Clicking element of ${kleur.yellow(key)}`)
                targetElement.click()
                await waitForTimeout(.9, "let dropdown populate");
                await selectFromDropDown(conditionOrInputValue, "//div[@data-name='popup-menu-container']//div[@role='option']/span/span/div/div/span")
                await waitForTimeout(.4, "after selecting from dropdown");

            } catch (e) {

                if (e.constructor.name === "TimeoutError") {
                    if (!inputXpathQueries[key]) throw (new NoInputFoundError(`Unable to find dropdown xpath target for primaryLeft/secondary. Make sure chart layout is SAVED with an indicator that contains/matches this: ${conditionOrInputValue}`))

                    log.debug(`Timed out looking for dropdown. Looking for INPUT xpath of ${kleur.yellow(key)}`)
                    try {
                        const valueInput = await fetchFirstXPath(page, inputXpathQueries[key], 1000)
                        log.debug(`Typing value: ${kleur.blue(conditionOrInputValue)}`)
                        await clickInputAndDelete(page, valueInput)
                        await valueInput.type(String(conditionOrInputValue))
                    } catch (inputError) {

                        if (inputError.constructor.name === "TimeoutError") {
                            if (!readOnlyInputQueries[key]) throw (new NoInputFoundError(`Unable to find 'readonlyInput' xpath target for ${key} which doesn't have inputs, so won't even try`))
                            log.debug(`Timed out looking for input. Looking for READ-ONLY INPUT xpath of ${kleur.yellow(key)}`)

                            try {
                                const valueReadonlyInput = await fetchFirstXPath(page, readOnlyInputQueries[key], 1000)
                                /* istanbul ignore next */
                                const readOnlyValue = await page.evaluate((el) => el.value, valueReadonlyInput)

                                if (readOnlyValue === conditionOrInputValue) {
                                    log.debug(`looks like the readonly input is actually ${conditionOrInputValue} as expected`)
                                } else {
                                    throw new Error(`Read only input value for ${key} is ${readOnlyValue}, but expected ${conditionOrInputValue}`)
                                }
                            } catch (readOnlyInputError) {
                                if (readOnlyInputError.constructor.name === "TimeoutError") {
                                    throw new NoInputFoundError(`Unable to find any inputs for '${key}' for configured value: '${conditionOrInputValue}'`)
                                } else {
                                    throw readOnlyInputError
                                }
                            }

                        } else {
                            throw inputError
                        }
                    }

                } else {
                    throw e
                }
            }
        }

    }

    await performActualEntry("primaryLeft")
    try {
        await performActualEntry("primaryRight")
        await performActualEntry("secondary")
    } catch (e) {
        if (e instanceof NoInputFoundError) {
            log.debug("NoInputFoundError, maybe we need to send secondary before setting primaryRight")
            // sometimes the secondary must be set first before the primaryRight shows up
            await performActualEntry("secondary")
            await performActualEntry("primaryRight")
        } else {
            throw e
        }
    }
    await performActualEntry("tertiaryLeft")
    await performActualEntry("tertiaryRight")

    await performActualEntry("quaternaryLeft")
    await performActualEntry("quaternaryRight")

    await waitForTimeout(.4);

    // if (!!option) {
    //     log.debug(`Looking for option: ${kleur.blue(option)}`)
    //
    //     try {
    //
    //         const targetElement = await fetchFirstXPath(page, "//fieldset[@aria-label='Trigger']//span[@role='button']", 3000)
    //         log.debug(`Found dropdown! Clicking element of ${kleur.yellow(option)}`)
    //         targetElement.evaluate((b) => b.click())
    //         await waitForTimeout(.3)
    //         await selectFromDropDown(option, "//div[@data-name='popup-menu-container']//div[@role='option']/span/span/div")
    //
    //     } catch (e) {
    //         if (e.constructor.name === "TimeoutError") {
    //             throw new Error(`No fire rate 'option' available, but one was specified in alert configuration: ${option}`)
    //         } else {
    //             throw e
    //         }
    //     }
    //
    // }

    if (!!option) {
        log.debug(`Looking for option: ${kleur.blue(option)}`)

        const selector = "//legend[text()='Trigger']/../..//button//span[contains(@class,'ellipsisContainer')]"

        try {
            await page.waitForSelector("xpath/." + selector, {timeout: 8000})
        } catch (e) {
            if (e.constructor.name === "TimeoutError") {
                throw new Error(`No fire rate 'option' available, but one was specified in alert configuration: ${option}`)
            } else {
                throw e
            }
        }

        const elements = await page.$$("xpath/." + selector)

        let found = false
        let foundOptions = []
        for (const el of elements) {
            /* istanbul ignore next */
            let optionText = await page.evaluate(element => element.innerText, el);
            foundOptions.push(optionText.trim())
            if (optionText === option && !found) {
                log.debug(`Found! Clicking ${kleur.yellow(optionText)}`)
                found = true
                await waitForTimeout(.4);
                await el.click()
                // /* istanbul ignore next */
                // await page.evaluate((text) => {
                //     const el = document.evaluate(`//*[@class='js-fire-rate-row']//div[@data-title='${text}']`,
                //         document,
                //         null,
                //         XPathResult.FIRST_ORDERED_NODE_TYPE,
                //         null
                //     ).singleNodeValue as HTMLElement
                //     el.click()
                //
                // }, option)
                await waitForTimeout(.4);
                // const justClickedEl = await fetchFirstXPath(page, `//*[@class='js-fire-rate-row']//div[@data-title='${option}']/..`)
                //
                // /* istanbul ignore next */
                // const className = await page.evaluate(el => el.className, justClickedEl);
                //
                // if (className.indexOf("i-active") < 0) {
                //     log.error("option element was clicked, but it's parent does not have the 'i-active' class assigned")
                //     throw Error("Unable to select option correctly...a bug in the system")
                // }

            }
        }
        if (!found) throw new SelectionError(option, foundOptions)

    }

    if (expireOpenEnded !== undefined) {

        log.debug("set whether to expire open ended")
        await waitForTimeout(.1);

        const expDropdownTarget = await fetchFirstXPath(page, "//legend[text()='Expiration']/../..//button")
        await expDropdownTarget.click()


        await waitForTimeout(.4);
        const openEndedCheckbox = await fetchFirstXPath(page, `//input[@id='unexpired-date']`)
        const isOpenEnded = await page.evaluate(element => element.checked, openEndedCheckbox)

        if (expireOpenEnded != isOpenEnded) {
            openEndedCheckbox.click()
        }

        await waitForTimeout(.3);

        if (!expireOpenEnded && expireInterval) {

            log.info(`Set Expiration ${kleur.blue(expireInterval)} hours in the future`)
            const dateInput = await fetchFirstXPath(page, "//div[contains(@class, 'pickerInput-')]//input")
            const timeInput = await fetchFirstXPath(page, "//div[contains(@class, 'time-')]//input")

            const dateISO = await page.evaluate((expireInterval) => {
                console.log("expireInterval: " + expireInterval)
                const currentDate = new Date()
                currentDate.setTime(currentDate.getTime() - (currentDate.getTimezoneOffset() * 60000) + expireInterval * 60 * 60 * 1000);
                return currentDate.toISOString()
            }, expireInterval)

            const [splitDate, timeFull] = dateISO.split("T")
            const [hours, min, secs] = timeFull.split(":")

            const expDate = splitDate.replaceAll("-", "")
            const expTime = `${hours}${min}`
            log.debug(`exp_date: ${expDate}`)
            log.debug(`exp_time: ${expTime}`)

            await dateInput.click()
            await waitForTimeout(50);
            await page.keyboard.press('End');
            for (const l of "yyyy-mm-dd".split("")) {
                await waitForTimeout(50);
                await page.keyboard.press('Backspace');
            }
            await dateInput.type(String(expDate))
            await waitForTimeout(50);
            await timeInput.click()
            await waitForTimeout(50);
            await page.keyboard.press('End');
            for (const l of "hh:mm".split("")) {
                await waitForTimeout(50);
                await page.keyboard.press('Backspace');
            }
            await timeInput.type(String(expTime))
            await waitForTimeout(.2);
        }

        await waitForTimeout(.2);
        const expSubmitButton = await fetchFirstXPath(page, "//div[@data-name='popup-menu-container']//div[@data-name='menu-inner']/div/button")
        await expSubmitButton.click()

    }

    if (!!name) {
        log.debug(`Setting Alert Name: ${kleur.blue(name)}`)

        try {
            const customAlertNameButton = await fetchFirstXPath(page, "//button[@id='alert-name']", 1000, false)
            log.warn("Clicking custom alert name button")
            customAlertNameButton.evaluate(e => e.click())
        } catch (e) {
            log.trace("Tried to click custom alert name button, but got nothing")
        }

        const nameInput = await fetchFirstXPath(page, "//input[@id='alert-name']")
        await clickInputAndDelete(page, nameInput)
        await nameInput.type(name)
        await waitForTimeout(.5);
    }


    if (!!message) {
        log.debug(`Setting message: ${kleur.blue(message)}`)
        try {
            const messageTextarea = await fetchFirstXPath(page, "//textarea[@id='alert-message']", 1000, false)
            await clickInputAndDelete(page, messageTextarea)
            await messageTextarea.type(message)
        } catch (e) {
            if (e.constructor.name === "TimeoutError") {
                log.warn(`No message box found, but message specified: ${message}`)
            } else {
                throw e
            }
        }
    }

    await waitForTimeout(.2);

    const notificationsTab = await fetchFirstXPath(page, "//button[@id='alert-dialog-tabs__notifications']")

    await notificationsTab.click()

    await waitForTimeout(.4);

    // alert actions

    for (const [configKey, elementInputName] of Object.entries(alertActionCorresponding)) {

        if (!!actions && !!actions[configKey] !== undefined) {
            await waitForTimeout(.3)
            const el = await fetchFirstXPath(page, `//input[@data-name='${elementInputName}']`)
            /* istanbul ignore next */
            const isChecked = await page.evaluate(element => element.checked, el)

            if (configKey === "webhook") {
                if (isChecked != actions.webhook.enabled) {
                    log.debug(`setting ${kleur.blue("webhook")} as checked`)
                    el.click()
                    await waitForTimeout(.3)
                }
                if (actions.webhook.enabled && actions.webhook.url) {
                    await waitForTimeout(.3)
                    log.debug(`typing webhook url: ${kleur.blue(actions.webhook.url)}`)
                    const webhookUrlEl = await fetchFirstXPath(page, `//input[contains(@placeholder, 'alert-hook')]`, 1000)
                    await clickInputAndDelete(page, webhookUrlEl)
                    await webhookUrlEl.type(String(actions.webhook.url))
                }

            } else if (configKey === "playSound") {

                if (actions.playSound?.enabled !== undefined && isChecked != actions.playSound?.enabled) {
                    log.debug(`setting ${kleur.blue("play-sound")} input as checked`)
                    el.click()
                    await waitForTimeout(.3)
                }
                if (actions.playSound?.enabled && actions.playSound?.name && actions.playSound?.duration) {
                    {
                        await waitForTimeout(.5)

                        try {
                            const classicSoundsSectionButton = await fetchFirstXPath(page, dropdownSoundXpathQueries["classicSection"], 3000, false)
                            classicSoundsSectionButton.evaluate((b) => b.click())
                            log.trace("Clicked to expand 'Classic' sounds")
                        } catch (e) {
                            log.trace("'Classic' sounds section already open")

                        }

                        await waitForTimeout(.1)

                        log.debug(`Looking for DROPDOWN xpath of ${kleur.yellow("playSound.name")}`)
                        const targetElement = await fetchFirstXPath(page, dropdownSoundXpathQueries["nameTarget"], 3000)
                        log.debug(`Found dropdown! Clicking element of ${kleur.yellow(actions.playSound.name)}`)
                        targetElement.evaluate((b) => b.click())
                        await waitForTimeout(.3)
                        await selectFromDropDown(actions.playSound.name, dropdownSoundXpathQueries["nameListItems"])
                    }
                    {
                        await waitForTimeout(.5)
                        log.debug(`Looking for DROPDOWN xpath of ${kleur.yellow("playSound.duration")}`)
                        const targetElement = await fetchFirstXPath(page, dropdownSoundXpathQueries["durationTarget"], 3000)
                        log.debug(`Found dropdown! Clicking element of ${kleur.yellow(actions.playSound.duration)}`)
                        targetElement.evaluate((b) => b.click())
                        await waitForTimeout(.3)
                        await selectFromDropDown(actions.playSound.duration, dropdownSoundXpathQueries["durationListItems"])
                    }
                    await waitForTimeout(.5)
                }

            } else {
                if (isChecked != actions[configKey]) {
                    log.debug(`setting ${kleur.blue(configKey)} as checked`)
                    el.click()
                }
            }
        }

    }


}

export const clickSubmit = async (page) => {
    log.debug("clickSubmit()")
    const submitButton = await fetchFirstXPath(page, "//div[contains(@data-name, 'alerts-create-edit-dialog')]//button[@data-name='submit']")
    submitButton.evaluate((b) => b.click())
}

// sometimes there's a warning of "this alert may trigger differently than expected"
export const clickContinueIfWarning = async (page) => {
    try {
        log.debug("clickContinueIfWarning()")
        const continueAnywayButton = await fetchFirstXPath(page, `//button[@name='continue']`,
            3000, false)
        continueAnywayButton.evaluate((b) => b.click())
        await waitForTimeout(4, "waiting after clicking 'continue anyway' button");
    } catch (error) {
        log.debug("No warning dialog")
    }
}


export const addAlert = async (page, singleAlertSettings: ISingleAlertSettings) => {
    log.debug("addAlert()")

    const typeShortcutForAlertDialog = async () => {
        log.debug("addAlert()...pressing shortcut key")

        await page.keyboard.press('Escape')
        await waitForTimeout(.5);
        await page.keyboard.press('Escape')
        await waitForTimeout(.5);
        await page.keyboard.down('AltLeft')
        await page.keyboard.press("a")
        await page.keyboard.up('AltLeft')
        await waitForTimeout(.5, "after keyboard shortcut for new alert dialog");
    }

    await typeShortcutForAlertDialog()


    await waitForTimeout(.7, "..make sure we're showing the alert dialog");

    const isNotShowingAlertDialog = async () => {
        return !(await isXpathVisible(page, "//div[contains(@data-name, 'alerts-create-edit-dialog')]"))
    }

    if (await isNotShowingAlertDialog()) {
        log.warn("NOT showing alert dialog! maybe invalid symbol?")
        if (await isXpathVisible(page, "//*[text()=\"Can't create alert on invalid symbol\"]")) {
            log.error("Looks like we tried to create alert on invalid symbol, throwing error")
            throw new InvalidSymbolError()
        }

        const MAX_TRIES = 3
        let retryCount = 1
        while ((await isNotShowingAlertDialog()) && retryCount <= MAX_TRIES + 1) {
            if (retryCount == MAX_TRIES) {
                await takeScreenshot(page, "unable_to_bring_up_alert_dialog")
                throw new AddAlertInvocationError()
            }
            log.debug("Attempting to show alert dialog again...")
            await waitForTimeout(retryCount, "pausing a little")
            await typeShortcutForAlertDialog()
            retryCount += 1
        }
    }

    await configureSingleAlertSettings(page, singleAlertSettings)

    await waitForTimeout(.5);

    await takeScreenshot(page, "before_submitting_alert")

    await clickSubmit(page)

    await waitForTimeout(2);

    await clickContinueIfWarning(page)

    await waitForTimeout(2, "waiting a little after adding")

}

