import path from "path"
import resolveFrom from "resolve-from"
import {execSync} from "child_process"
import {Options as ChromeOptions} from "selenium-webdriver/chrome"
import {Options as SafariOptions} from "selenium-webdriver/safari"
import {Options as FireFoxOptions} from "selenium-webdriver/firefox"
import {Builder, By, ThenableWebDriver, until} from "selenium-webdriver"

import {BrowserInterface} from "./base"

const {
  BROWSERSTACK,
  BROWSERSTACK_USERNAME,
  BROWSERSTACK_ACCESS_KEY,
  HEADLESS,
  CHROME_BIN,
  LEGACY_SAFARI,
  SKIP_LOCAL_SELENIUM_SERVER,
} = process.env

if (process.env.ChromeWebDriver) {
  process.env.PATH = `${process.env.ChromeWebDriver}${path.delimiter}${process.env.PATH}`
}

let seleniumServer: any
let browserStackLocal: any
let browser: ThenableWebDriver

export async function quit() {
  await Promise.all([
    browser?.quit(),
    new Promise<void>((resolve) => {
      browserStackLocal ? browserStackLocal.killAllProcesses(() => resolve()) : resolve()
    }),
  ])
  seleniumServer?.kill()

  browser = undefined
  browserStackLocal = undefined
  seleniumServer = undefined
}

class Selenium extends BrowserInterface {
  private browserName: string

  // TODO: support setting locale
  async setup(browserName: string, locale?: string) {
    if (browser) return
    this.browserName = browserName

    let capabilities = {}
    const isSafari = browserName === "safari"
    const isFirefox = browserName === "firefox"
    const isIE = browserName === "internet explorer"
    const isBrowserStack = BROWSERSTACK
    const localSeleniumServer = SKIP_LOCAL_SELENIUM_SERVER !== "true"

    // install conditional packages globally so the entire
    // monorepo doesn't need to rebuild when testing
    let globalNodeModules: string

    if (isBrowserStack || localSeleniumServer) {
      globalNodeModules = execSync("npm root -g").toString().trim()
    }

    if (isBrowserStack) {
      const {Local} = require(resolveFrom(globalNodeModules, "browserstack-local"))
      browserStackLocal = new Local()

      const localBrowserStackOpts = {
        key: process.env.BROWSERSTACK_ACCESS_KEY,
        // Add a unique local identifier to run parallel tests
        // on BrowserStack
        localIdentifier: new Date().getTime(),
      }
      await new Promise<void>((resolve, reject) => {
        browserStackLocal.start(localBrowserStackOpts, (err) => {
          if (err) return reject(err)
          console.log("Started BrowserStackLocal", browserStackLocal.isRunning())
          resolve()
        })
      })

      const safariOpts = {
        os: "OS X",
        os_version: "Mojave",
        browser: "Safari",
      }
      const safariLegacyOpts = {
        os: "OS X",
        os_version: "Sierra",
        browserName: "Safari",
        browser_version: "10.1",
      }
      const ieOpts = {
        os: "Windows",
        os_version: "10",
        browser: "IE",
      }
      const firefoxOpts = {
        os: "Windows",
        os_version: "10",
        browser: "Firefox",
      }
      const sharedOpts = {
        "browserstack.local": true,
        "browserstack.video": false,
        "browserstack.user": BROWSERSTACK_USERNAME,
        "browserstack.key": BROWSERSTACK_ACCESS_KEY,
        "browserstack.localIdentifier": localBrowserStackOpts.localIdentifier,
      }

      capabilities = {
        ...capabilities,
        ...sharedOpts,

        ...(isIE ? ieOpts : {}),
        ...(isSafari ? (LEGACY_SAFARI ? safariLegacyOpts : safariOpts) : {}),
        ...(isFirefox ? firefoxOpts : {}),
      }
    } else if (localSeleniumServer) {
      console.log("Installing selenium server")
      const seleniumServerMod = require(resolveFrom(globalNodeModules, "selenium-standalone"))

      await new Promise<void>((resolve, reject) => {
        seleniumServerMod.install((err) => {
          if (err) return reject(err)
          resolve()
        })
      })

      console.log("Starting selenium server")
      await new Promise<void>((resolve, reject) => {
        seleniumServerMod.start((err, child) => {
          if (err) return reject(err)
          seleniumServer = child
          resolve()
        })
      })
      console.log("Started selenium server")
    }

    let chromeOptions = new ChromeOptions()
    let firefoxOptions = new FireFoxOptions()
    let safariOptions = new SafariOptions()

    if (HEADLESS) {
      const screenSize = {width: 1280, height: 720}
      chromeOptions = chromeOptions.headless().windowSize(screenSize) as any
      firefoxOptions = firefoxOptions.headless().windowSize(screenSize)
    }

    if (CHROME_BIN) {
      chromeOptions = chromeOptions.setChromeBinaryPath(path.resolve(CHROME_BIN))
    }

    let seleniumServerUrl

    if (isBrowserStack) {
      seleniumServerUrl = "http://hub-cloud.browserstack.com/wd/hub"
    } else if (localSeleniumServer) {
      seleniumServerUrl = `http://localhost:4444/wd/hub`
    }

    browser = new Builder()
      .usingServer(seleniumServerUrl)
      .withCapabilities(capabilities)
      .forBrowser(browserName)
      .setChromeOptions(chromeOptions)
      .setFirefoxOptions(firefoxOptions)
      .setSafariOptions(safariOptions)
      .build()
  }

  async get(url: string): Promise<void> {
    return browser.get(url)
  }

  async loadPage(url: string) {
    // in chrome we use a new tab for testing
    if (this.browserName === "chrome") {
      const initialHandle = await browser.getWindowHandle()

      await browser.switchTo().newWindow("tab")
      const newHandle = await browser.getWindowHandle()

      await browser.switchTo().window(initialHandle)
      await browser.close()
      await browser.switchTo().window(newHandle)

      // clean-up extra windows created from links and such
      for (const handle of await browser.getAllWindowHandles()) {
        if (handle !== newHandle) {
          await browser.switchTo().window(handle)
          await browser.close()
        }
      }
      await browser.switchTo().window(newHandle)
    } else {
      await browser.get("about:blank")
    }
    return browser.get(url)
  }

  back(): BrowserInterface {
    return this.chain(() => {
      return browser.navigate().back()
    })
  }
  forward(): BrowserInterface {
    return this.chain(() => {
      return browser.navigate().forward()
    })
  }
  refresh(): BrowserInterface {
    return this.chain(() => {
      return browser.navigate().refresh()
    })
  }
  setDimensions({width, height}: {height: number; width: number}): BrowserInterface {
    return this.chain(() => browser.manage().window().setRect({width, height, x: 0, y: 0}))
  }
  addCookie(opts: {name: string; value: string}): BrowserInterface {
    return this.chain(() => browser.manage().addCookie(opts))
  }
  deleteCookies(): BrowserInterface {
    return this.chain(() => browser.manage().deleteAllCookies())
  }

  elementByCss(selector: string) {
    return this.chain(() => {
      return browser.findElement(By.css(selector)).then((el: any) => {
        el.selector = selector
        el.text = () => el.getText()
        el.getComputedCss = (prop) => el.getCssValue(prop)
        el.type = (text) => el.sendKeys(text)
        el.getValue = () =>
          browser.executeScript(`return document.querySelector('${selector}').value`)
        return el
      })
    })
  }

  elementById(sel) {
    return this.elementByCss(`#${sel}`)
  }

  getValue() {
    return this.chain((el) =>
      browser.executeScript(`return document.querySelector('${el.selector}').value`),
    ) as any
  }

  text() {
    return this.chain((el) => el.getText()) as any
  }

  type(text) {
    return this.chain((el) => el.sendKeys(text))
  }

  moveTo() {
    return this.chain((el) => {
      return browser
        .actions()
        .move({origin: el})
        .perform()
        .then(() => el)
    })
  }

  async getComputedCss(prop: string) {
    return this.chain((el) => {
      return el.getCssValue(prop)
    }) as any
  }

  async getAttribute(attr) {
    return this.chain((el) => el.getAttribute(attr))
  }

  async hasElementByCssSelector(selector: string) {
    return this.eval(`!!document.querySelector('${selector}')`) as any
  }

  click() {
    return this.chain((el) => {
      return el.click().then(() => el)
    })
  }

  elementsByCss(sel) {
    return this.chain(() => browser.findElements(By.css(sel))) as any as BrowserInterface[]
  }

  waitForElementByCss(sel, timeout) {
    return this.chain(() => browser.wait(until.elementLocated(By.css(sel)), timeout))
  }

  waitForCondition(condition, timeout) {
    return this.chain(() =>
      browser.wait(async (driver) => {
        return driver.executeScript("return " + condition).catch(() => false)
      }, timeout),
    )
  }

  async eval(snippet) {
    if (typeof snippet === "string" && !snippet.startsWith("return")) {
      snippet = `return ${snippet}`
    }
    return browser.executeScript(snippet)
  }

  async evalAsync(snippet) {
    if (typeof snippet === "string" && !snippet.startsWith("return")) {
      snippet = `return ${snippet}`
    }
    return browser.executeAsyncScript(snippet)
  }

  async log() {
    return this.chain(() => browser.manage().logs().get("browser")) as any
  }

  async url() {
    return this.chain(() => browser.getCurrentUrl()) as any
  }
}

export default Selenium
