import {
    Browser as ChromeBrowser,
    computeExecutablePath,
    detectBrowserPlatform,
    resolveBuildId,
} from "@puppeteer/browsers"
import exp from "constants"
import {accessSync, constants, rmSync, writeFileSync} from "fs"
import {mkdir} from "fs/promises"
import kleur from "kleur"
import path from "path"
import type {Browser} from "puppeteer-core"
import puppeteer from "puppeteer-core"
import RegexParser from "regex-parser"
import {
    AddAlertInvocationError,
    ErrorWithScreenShot,
    InvalidSymbolError,
    NoInputFoundError,
    SelectionError,
} from "../classes.js"
import {ISingleAlertSettings} from "../interfaces.js"
import {isEnvEnabled, waitForTimeout} from "./common-service.js"
import log from "./log.js"

// 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-expect-error
            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(0.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(0.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[contains(@data-qa-id, 'main-series-select')]",
    primaryRight: "//span[contains(@data-qa-id, 'main-series-plot-select')]",
    secondary: "//button[contains(@data-qa-id, 'operator-dropdown')]",

    // 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[contains(@data-qa-id, 'start-band-select')] | //legend[text()='Upper bound']/../..//span[@data-name='start-band-select']",
    tertiaryRight:
        "//*[contains(@class, 'operatorRow-')]/..//span[contains(@data-qa-id, 'end-band-select')] | //legend[text()='Upper bound']/../..//span[@data-name='end-band-select']",

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

const dropdownOptionsXpathQueries = {
    primaryLeft: "//div[@role='option']//div[@data-qa-id='main-series-select-title']/span",
    primaryRight: "//div[@role='option']//div[@data-qa-id='main-series-select-plot-title']/span",
    secondary: "//div[@role='option']//div[contains(@class, 'title-')]",
    tertiaryLeft: "//div[contains(@data-qa-id, 'start-band-select')]//div[@data-qa-id='start-band-select-title']/span",
}

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

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

    quaternaryLeft:
        "//*[contains(@class, 'operatorRow-')]/..//input[contains(@data-qa-id, 'start-band-range')] | //legend[text()='Lower bound']/../..//input[contains(@data-qa-id, 'start-band-range')]",
    quaternaryRight:
        "//*[contains(@class, 'operatorRow-')]/..//input[contains(@data-qa-id, 'end-band-range')] | //legend[text()='Lower bound']/../..//input[contains(@data-qa-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(0.5)
    /* istanbul ignore next */
    await page.evaluate((el) => {
        el.value = ""
    }, inputElement)
    await waitForTimeout(0.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)
    }

    const chromeBuildId = await resolveBuildId(ChromeBrowser.CHROME, detectBrowserPlatform(), "stable")
    const chromeExecutable = computeExecutablePath({
        browser: ChromeBrowser.CHROME,
        buildId: chromeBuildId,
        cacheDir: process.cwd(),
        platform: detectBrowserPlatform(),
    })

    const 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",
        "--disable-gpu",
        "--disable-session-crashed-bubble",
        "--disable-infobars",
        "--disable-restore-session-state",
        "--window-size=1920,1080", // otherwise headless doesn't work
        !url ? "" : `${url}`,
    ]

    return puppeteer.launch({
        executablePath: chromeExecutable,
        headless: headless,
        userDataDir,
        defaultViewport: {width: 1920, height: 1080, isMobile: false, hasTouch: false},
        args,
    })
}

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(0.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(0.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(0.5)
    await page.keyboard.press("Escape")
    await waitForTimeout(0.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(0.3)
    await page.keyboard.type(`${symbol}`, {delay: 0.3})
    await waitForTimeout(0.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.toLowerCase().indexOf(needle.toLowerCase()) > -1
    }
}

export const configureSingleAlertSettings = async (page, singleAlertSettings: ISingleAlertSettings) => {
    const {condition, name, expireOpenEnded, expireInterval, trigger, 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: string, 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
        const 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(0.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()

                try {
                    const showMoreElement = await fetchFirstXPath(
                        page,
                        "//button[@data-qa-id='ui-lib-title-popover-item']",
                        600,
                        false
                    )
                    log.warn("Clicking 'Show More' button...")
                    showMoreElement.evaluate((e) => e.click())
                } catch (e) {
                    log.trace("Checked for 'Show More' button, but found nothing")
                }

                await waitForTimeout(0.9, "let dropdown populate")
                let selector = dropdownOptionsXpathQueries[key];
                if (selector === undefined || selector == null) {
                    console.error(`Oops, no selector found for key: ${key}`)
                }
                await selectFromDropDown(conditionOrInputValue, selector)
                await waitForTimeout(0.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(0.4)

    // if (!!option) {
    //     log.debug(`Looking for trigger: ${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 (trigger) {
        log.debug(`Looking for trigger: ${kleur.blue(trigger)}`)
        try {
            const triggerDropdownButton = await fetchFirstXPath(
                page,
                "//button[contains(@data-qa-id, 'trigger-dropdown-button')]"
            )
            triggerDropdownButton.click()
            await waitForTimeout(0.3)
            await selectFromDropDown(
                trigger,
                "//div[@data-qa-id='trigger-dropdown-button-container']//div[@role='option']//div[contains(@class, 'title-')]"
            )
        } catch (e) {
            if (e.constructor.name === "TimeoutError") {
                throw new Error(`Trigger option available, but one was specified in alert configuration: ${trigger}`)
            } else {
                throw e
            }
        }
    }

    // if (!!option) {
    //     log.debug(`Looking for trigger: ${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.toLowerCase() === option.toLowerCase() && !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(0.1)

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

        await waitForTimeout(0.3)

        const expirationDropdownSelector = "//div[@data-qa-id='expiration-time-dropdown-container']//div[contains(@data-qa-id, 'expiration-time-dropdown-item')]//div[contains(@class, 'title-')]"

        if (!expireOpenEnded && expireInterval) {
            log.info(`Set custom expiration date`)
            await selectFromDropDown("Custom date", expirationDropdownSelector)
            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(500)

            for (let i = 0; i < 11; i++) {
                await page.keyboard.press('ArrowRight');
                // Optional: tiny delay if the site is very laggy / virtualized
                await waitForTimeout(20)
            }
            for (const l of "yyyy-mm-dd".split("")) {
                await waitForTimeout(50)
                await page.keyboard.press("Backspace")
            }
            await dateInput.type(String(expDate), {delay: 40})
            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), {delay: 40})

            await waitForTimeout(1000)
            const submitTimeButton = await fetchFirstXPath(page, "//button[contains(@data-qa-id, 'expiration-set-button')]")
            submitTimeButton.click()

        } else if (expireOpenEnded) {
            await selectFromDropDown("Open-ended", expirationDropdownSelector)
        }
    }

    await waitForTimeout(0.2)

    // Click message tab
    const messageTab = await fetchFirstXPath(page, "//button[@data-qa-id='alert-message-button']")

    await messageTab.click()

    await waitForTimeout(0.4)

    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(0.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(0.2)

    const submitMessageForm = await fetchFirstXPath(page, "//button[@data-qa-id='submit']")

    await submitMessageForm.click()

    await waitForTimeout(0.7)

    // Click notifications tab

    const notificationsTab = await fetchFirstXPath(page, "//button[@data-qa-id='alert-notifications-button']")

    await notificationsTab.click()

    await waitForTimeout(0.4)

    // alert actions

    for (const [configKey, elementInputName] of Object.entries(alertActionCorresponding)) {
        if (!!actions && !!actions[configKey] !== undefined) {
            await waitForTimeout(0.3)
            const el = await fetchFirstXPath(page, `//label[@data-qa-id='${elementInputName}']//input`)
            /* 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(0.3)
                }
                if (actions.webhook.enabled && actions.webhook.url) {
                    await waitForTimeout(0.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(0.3)
                }
                if (actions.playSound?.enabled && actions.playSound?.name && actions.playSound?.duration) {
                    {
                        await waitForTimeout(0.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(0.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(0.3)
                        await selectFromDropDown(actions.playSound.name, dropdownSoundXpathQueries["nameListItems"])
                    }
                    {
                        await waitForTimeout(0.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(0.3)
                        await selectFromDropDown(actions.playSound.duration, dropdownSoundXpathQueries["durationListItems"])
                    }
                    await waitForTimeout(0.5)
                }
            } else {
                if (isChecked !== actions[configKey]) {
                    log.debug(`setting ${kleur.blue(configKey)} as checked`)
                    el.click()
                }
            }
        }
    }

    await waitForTimeout(0.2)

    const submitAlertsForm = await fetchFirstXPath(page, "//button[@data-qa-id='submit']")

    await submitAlertsForm.click()

    await waitForTimeout(0.5)

}

export const clickSubmit = async (page) => {
    log.debug("clickSubmit()")
    const submitButton = await fetchFirstXPath(
        page,
        "//div[contains(@data-qa-id, 'alerts-create-edit-dialog')]//button[@data-qa-id='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(0.5)
        await page.keyboard.press("Escape")
        await waitForTimeout(0.5)
        await page.keyboard.down("AltLeft")
        await page.keyboard.press("a")
        await page.keyboard.up("AltLeft")
        await waitForTimeout(0.5, "after keyboard shortcut for new alert dialog")
    }

    await typeShortcutForAlertDialog()

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

    const isNotShowingAlertDialog = async () => {
        return !(await isXpathVisible(page, "//div[contains(@data-qa-id, '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(0.5)

    await takeScreenshot(page, "before_submitting_alert")

    await clickSubmit(page)

    await waitForTimeout(2)

    await clickContinueIfWarning(page)

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