/*
 * Copyright (c) 2012-2018 Red Hat, Inc.
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *   Red Hat, Inc. - initial API and implementation
 */
package org.eclipse.che.selenium.core;

import static java.lang.String.format;
import static org.eclipse.che.selenium.core.constant.TestTimeoutsConstants.APPLICATION_START_TIMEOUT_SEC;
import static org.eclipse.che.selenium.core.constant.TestTimeoutsConstants.LOADER_TIMEOUT_SEC;
import static org.eclipse.che.selenium.core.utils.WaitUtils.sleepQuietly;
import static org.openqa.selenium.support.ui.ExpectedConditions.frameToBeAvailableAndSwitchToIt;
import static org.openqa.selenium.support.ui.ExpectedConditions.visibilityOfElementLocated;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import java.io.Closeable;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.logging.Level;
import org.eclipse.che.api.core.BadRequestException;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.ForbiddenException;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.UnauthorizedException;
import org.eclipse.che.api.core.rest.HttpJsonRequestFactory;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.selenium.core.constant.TestBrowser;
import org.eclipse.che.selenium.core.utils.DockerUtil;
import org.openqa.selenium.By;
import org.openqa.selenium.Dimension;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.interactions.HasInputDevices;
import org.openqa.selenium.interactions.Keyboard;
import org.openqa.selenium.interactions.Mouse;
import org.openqa.selenium.logging.LogType;
import org.openqa.selenium.logging.LoggingPreferences;
import org.openqa.selenium.remote.CapabilityType;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Wrapper for {@link WebDriver} to have ability to use in Guice container.
 *
 * @author Anatolii Bazko
 */
@Singleton
public class SeleniumWebDriver
    implements Closeable, WebDriver, JavascriptExecutor, TakesScreenshot, HasInputDevices {
  private static final Logger LOG = LoggerFactory.getLogger(SeleniumWebDriver.class);
  private static final int MAX_ATTEMPTS = 5;
  private static final int DELAY_IN_SECONDS = 5;

  private TestBrowser browser;
  private boolean gridMode;
  private String gridNodeContainerId;
  private String webDriverPort;
  private final RemoteWebDriver driver;
  private final HttpJsonRequestFactory httpJsonRequestFactory;
  private final DockerUtil dockerUtil;
  private final String downloadDir;

  @Inject
  public SeleniumWebDriver(
      @Named("sys.browser") TestBrowser browser,
      @Named("sys.driver.port") String webDriverPort,
      @Named("sys.grid.mode") boolean gridMode,
      HttpJsonRequestFactory httpJsonRequestFactory,
      DockerUtil dockerUtil,
      @Named("tests.tmp_dir") String downloadDir) {
    this.browser = browser;
    this.webDriverPort = webDriverPort;
    this.gridMode = gridMode;
    this.httpJsonRequestFactory = httpJsonRequestFactory;
    this.dockerUtil = dockerUtil;
    this.downloadDir = downloadDir;

    try {
      URL webDriverUrl =
          new URL(format("http://localhost:%s%s", webDriverPort, gridMode ? "/wd/hub" : ""));
      this.driver = createDriver(webDriverUrl);
    } catch (MalformedURLException e) {
      throw new RuntimeException("Error of construction URL to web driver.", e);
    }
  }

  @Override
  public void get(String url) {
    driver.get(url);
  }

  @Override
  public String getCurrentUrl() {
    return driver.getCurrentUrl();
  }

  @Override
  public String getTitle() {
    return driver.getTitle();
  }

  @Override
  public List<WebElement> findElements(By by) {
    return driver.findElements(by);
  }

  @Override
  public WebElement findElement(By by) {
    return driver.findElement(by);
  }

  @Override
  public String getPageSource() {
    return driver.getPageSource();
  }

  @Override
  public void close() {
    driver.close();
  }

  @Override
  public void quit() {
    driver.quit();
  }

  @Override
  public Set<String> getWindowHandles() {
    return driver.getWindowHandles();
  }

  @Override
  public String getWindowHandle() {
    return driver.getWindowHandle();
  }

  @Override
  public TargetLocator switchTo() {
    return driver.switchTo();
  }

  @Override
  public Navigation navigate() {
    return driver.navigate();
  }

  @Override
  public Options manage() {
    return driver.manage();
  }

  @Override
  public Object executeScript(String script, Object... args) {
    return driver.executeScript(script, args);
  }

  @Override
  public Object executeAsyncScript(String script, Object... args) {
    return driver.executeAsyncScript(script, args);
  }

  @Override
  public <X> X getScreenshotAs(OutputType<X> target) throws WebDriverException {
    return driver.getScreenshotAs(target);
  }

  @Override
  public Keyboard getKeyboard() {
    return driver.getKeyboard();
  }

  @Override
  public Mouse getMouse() {
    return driver.getMouse();
  }

  private RemoteWebDriver createDriver(URL webDriverUrl) {
    for (int i = 1; ; ) {
      try {
        return doCreateDriver(webDriverUrl);
      } catch (WebDriverException e) {
        if (i++ >= MAX_ATTEMPTS) {
          throw e;
        }
      }

      sleepQuietly(DELAY_IN_SECONDS);
    }
  }

  /**
   * Read supported version info from official site.
   *
   * @param webDriverOfficialNotes address of official page with Google driver info
   * @return string with supported version range (for example, "36-40"), or null if version info
   *     doesn't found inside the official notes.
   * @throws IOException
   */
  @Nullable
  private String readSupportedVersionInfoForGoogleDriver(URL webDriverOfficialNotes)
      throws IOException {
    try (Scanner scanner = new Scanner(webDriverOfficialNotes.openStream(), "UTF-8")) {
      while (scanner.hasNextLine()) {
        String versionLine = scanner.findInLine("Supports Chrome v([\\d-]+)");
        if (versionLine != null) {
          return scanner.match().group(1);
        }

        scanner.nextLine();
      }
    }

    return null;
  }

  private RemoteWebDriver doCreateDriver(URL webDriverUrl) {
    DesiredCapabilities capability;

    switch (browser) {
      case GOOGLE_CHROME:
        LoggingPreferences loggingPreferences = new LoggingPreferences();
        loggingPreferences.enable(LogType.PERFORMANCE, Level.ALL);
        loggingPreferences.enable(LogType.BROWSER, Level.ALL);

        ChromeOptions options = new ChromeOptions();
        options.addArguments("--no-sandbox");
        options.addArguments("--dns-prefetch-disable");
        options.addArguments("--ignore-certificate-errors");

        // set parameters required for automatic download capability
        Map<String, Object> chromePrefs = new HashMap<>();
        chromePrefs.put("download.default_directory", downloadDir);
        chromePrefs.put("download.prompt_for_download", false);
        chromePrefs.put("download.directory_upgrade", true);
        chromePrefs.put("safebrowsing.enabled", true);
        chromePrefs.put("profile.default_content_settings.popups", 0);
        chromePrefs.put("plugins.plugins_disabled", "['Chrome PDF Viewer']");
        options.setExperimentalOption("prefs", chromePrefs);

        capability = DesiredCapabilities.chrome();
        capability.setCapability(ChromeOptions.CAPABILITY, options);
        capability.setCapability(CapabilityType.LOGGING_PREFS, loggingPreferences);
        capability.setCapability(CapabilityType.ACCEPT_SSL_CERTS, true);
        break;

      default:
        capability = DesiredCapabilities.firefox();
        capability.setCapability("dom.max_script_run_time", 240);
        capability.setCapability("dom.max_chrome_script_run_time", 240);
    }

    RemoteWebDriver driver = new RemoteWebDriver(webDriverUrl, capability);
    if (driver.getErrorHandler().isIncludeServerErrors()
        && driver.getCapabilities().getCapability("message") != null) {
      String errorMessage =
          format(
              "Web driver creation error occurred: %s",
              driver.getCapabilities().getCapability("message"));
      LOG.error(errorMessage);
      throw new RuntimeException(errorMessage);
    }

    driver.manage().window().setSize(new Dimension(1920, 1080));

    return driver;
  }

  /** wait while in a browser appears more the 1 window */
  public void waitOpenedSomeWin() {
    new WebDriverWait(this, 30)
        .until(
            (ExpectedCondition<Boolean>)
                input -> {
                  Set<String> driverWindows = getWindowHandles();
                  return (driverWindows.size() > 1);
                });
  }

  /**
   * calculate name of workspace from browser url cut symbols from end of slash symbol ("/") to end
   */
  public String getWorkspaceNameFromBrowserUrl() {
    String currentUrl = getCurrentUrl();
    return currentUrl.substring(currentUrl.lastIndexOf("/") + 1, currentUrl.length());
  }

  /**
   * switch to the next browser window (this means that if opened 2 windows, and we are in the
   * window 1, we will be switched into the window 2 )
   *
   * @param currentWindowHandler
   */
  public void switchToNoneCurrentWindow(String currentWindowHandler) {
    waitOpenedSomeWin();
    for (String handle : getWindowHandles()) {
      if (!currentWindowHandler.equals(handle)) {
        switchTo().window(handle);
        break;
      }
    }
  }

  public void switchFromDashboardIframeToIde() {
    switchFromDashboardIframeToIde(APPLICATION_START_TIMEOUT_SEC);
  }

  public void switchFromDashboardIframeToIde(int timeout) {
    wait(timeout).until(visibilityOfElementLocated(By.id("ide-application-iframe")));

    wait(LOADER_TIMEOUT_SEC)
        .until(
            (ExpectedCondition<Boolean>)
                driver ->
                    (((JavascriptExecutor) driver)
                            .executeScript("return angular.element('body').scope().showIDE"))
                        .toString()
                        .equals("true"));

    wait(timeout).until(frameToBeAvailableAndSwitchToIt(By.id("ide-application-iframe")));
  }

  private WebDriverWait wait(int timeOutInSeconds) {
    return new WebDriverWait(this, timeOutInSeconds);
  }

  public String getGridNodeContainerId() throws IOException {
    if (!gridMode) {
      throw new UnsupportedOperationException("We can't get grid node container id in local mode.");
    }

    if (gridNodeContainerId == null) {
      String getGridNodeInfoUrl =
          format(
              "http://localhost:%s/grid/api/testsession?session=%s",
              webDriverPort, driver.getSessionId());

      Map<String, String> gridNodeInfo;
      try {
        gridNodeInfo = httpJsonRequestFactory.fromUrl(getGridNodeInfoUrl).request().asProperties();
      } catch (ServerException
          | UnauthorizedException
          | ForbiddenException
          | NotFoundException
          | ConflictException
          | BadRequestException e) {
        throw new IOException(e);
      }

      if (!gridNodeInfo.containsKey("proxyId")) {
        throw new IOException("Proxy ID of grid node wasn't found.");
      }

      URL proxyId = new URL(gridNodeInfo.get("proxyId"));
      gridNodeContainerId = dockerUtil.findGridNodeContainerByIp(proxyId.getHost());
    }

    return gridNodeContainerId;
  }
}
