package old.core;

import java.time.Duration;
import java.util.List;
import java.util.function.Supplier;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

/**
 * @author evgeny
 */
@SuppressWarnings("UnusedDeclaration")
public abstract class BasePage {
  private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(20);
  private static final int PROGRESS_VISIBILITY_TIMEOUT_MS = 700;
  private static final By ERROR = By.xpath("//div[contains(@class, 'toast-error')]");
  private static final By INPUT_IS_ACTIVE = By.xpath("//div[contains(@class, 'input-active')]");
  protected static final By PROGRESS_BAR = By.xpath("//div[@class='progress']");

  protected final WebDriver driver;
  protected final String uri;

  public BasePage(WebDriver driver, String uri) {
    this.driver = driver;
    this.uri = uri;

    // Check that we're on the right page.
    if (!isAtUri(uri))
      throw new IllegalStateException("This is not the '" + uri + "' page");

    PageFactory.initElements(driver, this);
  }

  protected Duration getDefaultTimeout() {
    return DEFAULT_TIMEOUT;
  }

  public static By getProgressBar() {
    return PROGRESS_BAR;
  }

  public static By getErrorBy() {
    return ERROR;
  }

  public boolean isAtUri(String uri) {
    if (((RemoteWebDriver) driver).getCapabilities().getBrowserName().equalsIgnoreCase("internet explorer")) {
      return true;
    }
    else {
      return waitForCorrectUrl(uri, getDefaultTimeout());
    }
  }

  /**
   * Ожидание того, что URL текущей страницы содержит определенный текст.
   *
   * @param uri              адрес текущей страницы
   * @param timeout timeout ожидания
   * @return <code>true</code> при корректном url
   */
  private boolean waitForCorrectUrl(String uri, Duration timeout) {
    return new WebDriverWait(driver, timeout).until(ExpectedConditions.urlContains(uri));
  }

  /**
   * Ожидание видимости элемента, присутствующего в DOM.
   * Видимость означает не только то, что элемент отобразился, но и то, что высота и ширина элемента больше 0.
   *
   * @param webElement       элемент, который присутствует в DOM
   * @param timeout timeout ожидания
   * @return веб-элемент, как только он видим
   */
  public WebElement waitForVisibilityOf(WebElement webElement, Duration timeout) {
    return new WebDriverWait(driver, timeout).until(ExpectedConditions.visibilityOf(webElement));
  }

  /**
   * Ожидание видимости элемента, присутствующего в DOM.
   * Видимость означает не только то, что элемент отобразился, но и то, что высота и ширина элемента больше 0.
   *
   * @param webElement элемент, который присутствует в DOM
   * @return веб-элемент, как только он видим
   */
  public WebElement waitForVisibilityOf(WebElement webElement) {
    return waitForVisibilityOf(webElement, getDefaultTimeout());
  }

  /**
   * Ожидание появления элемента в DOM и его видимости.
   * Видимость означает не только то, что элемент отобразился, но и то, что высота и ширина элемента больше 0.
   *
   * @param locator          локатор, по которому находится элемент
   * @param timeout timeout ожидания
   * @return веб-элемент, как только он найден и видим
   */
  public WebElement waitForVisibilityOfElementLocated(By locator, Duration timeout) {
    return new WebDriverWait(driver, timeout).until(ExpectedConditions.visibilityOfElementLocated(locator));
  }

  /**
   * Ожидание появления элемента в DOM и его видимости.
   * Видимость означает не только то, что элемент отобразился, но и то, что высота и ширина элемента больше 0.
   *
   * @param locator локатор, по которому находится элемент
   * @return веб-элемент, как только он найден и видим
   */
  public WebElement waitForVisibilityOfElementLocated(By locator) {
    return waitForVisibilityOfElementLocated(locator, getDefaultTimeout());
  }

  /**
   * Ожидание появления элемента в DOM и его видимости.
   * Видимость означает не только то, что элемент отобразился, но и то, что высота и ширина элемента больше 0.
   *
   * @param locator          локатор, по которому находится элемент
   * @param timeout          timeout ожидания 
   * @param sleep    длительность между опросами
   * @return веб-элемент, как только он найден и видим
   */
  public WebElement waitForVisibilityOfElementLocated(By locator, Duration timeout, Duration sleep) {
    return new WebDriverWait(driver, timeout, sleep).until(ExpectedConditions.visibilityOfElementLocated(locator));
  }

  /**
   * Ожидание того, что элемент невидим или не представлен в DOM.
   *
   * @param locator          локатор, по которому находится элемент
   * @param timeout timeout ожидания
   * @return <code>true</code>, когда элемент невидим или не найден в DOM
   */
  public boolean waitForInvisibilityOfElementLocated(By locator, Duration timeout) {
    return new WebDriverWait(driver, timeout).until(ExpectedConditions.invisibilityOfElementLocated(locator));
  }

  /**
   * Ожидание того, что элемент невидим или не представлен в DOM.
   *
   * @param locator локатор, по которому находится элемент
   * @return <code>true</code>, когда элемент невидим или не найден в DOM
   */
  public boolean waitForInvisibilityOfElementLocated(By locator) {
    return waitForInvisibilityOfElementLocated(locator, getDefaultTimeout());
  }

  /**
   * Ожидание того, что элемент больше не прикреплен к DOM
   *
   * @param webElement       проверяемый элемент
   * @param timeout timeout ожидания
   * @return <code>true</code>, когда элемент больше не прикреплен к DOM
   */
  public boolean waitForStalenessOf(WebElement webElement, Duration timeout) {
    return new WebDriverWait(driver, timeout).until(ExpectedConditions.stalenessOf(webElement));
  }

  /**
   * Ожидание того, что элемент больше не прикреплен к DOM
   *
   * @param webElement проверяемый элемент
   * @return <code>true</code>, когда элемент больше не прикреплен к DOM
   */
  public boolean waitForStalenessOf(WebElement webElement) {
    return waitForStalenessOf(webElement, getDefaultTimeout());
  }

  /**
   * Ожидание наличия текста в элементе, соответствующем указанному локатору.
   *
   * @param locator          локатор, по которому находится элемент
   * @param text             проверяемый текст
   * @param timeout timeout ожидания
   * @return <code>true</code> при наличии текста в элементе
   */
  public boolean waitForTextToBePresentInElementLocated(By locator, String text, Duration timeout) {
    return new WebDriverWait(driver, timeout).until(ExpectedConditions.textToBePresentInElementLocated(locator, text));
  }

  /**
   * Ожидание наличия текста в элементе, соответствующем указанному локатору.
   *
   * @param locator локатор, по которому находится элемент
   * @param text    проверяемый текст
   * @return <code>true</code> при наличии текста в элементе
   */
  public boolean waitForTextToBePresentInElementLocated(By locator, String text) {
    return waitForTextToBePresentInElementLocated(locator, text, getDefaultTimeout());
  }

  /**
   * Ожидание наличия текста в определенном элементе
   *
   * @param webElement       проверяемый элемент
   * @param text             проверяемый текст
   * @param timeout timeout ожидания
   * @return <code>true</code> при наличии текста в элементе
   */
  public boolean waitForTextToBePresentInElement(WebElement webElement, String text, Duration timeout) {
    return new WebDriverWait(driver, timeout).until(ExpectedConditions.textToBePresentInElement(webElement, text));
  }

  /**
   * Ожидание наличия текста в определенном элементе
   *
   * @param webElement проверяемый элемент
   * @param text       проверяемый текст
   * @return <code>true</code> при наличии текста в элементе
   */
  public boolean waitForTextToBePresentInElement(WebElement webElement, String text) {
    return waitForTextToBePresentInElement(webElement, text, getDefaultTimeout());
  }

  /**
   * Ожидание видимости и доступности присутствующего в DOM элемента для нажатия.
   *
   * @param webElement       проверяемый элемент
   * @param timeout timeout ожидания
   * @return веб-элемент, как только он доступен для нажатия
   */
  public WebElement waitForElementToBeClickable(WebElement webElement, Duration timeout) {
    return new WebDriverWait(driver, timeout).until(ExpectedConditions.elementToBeClickable(webElement));
  }

  /**
   * Ожидание видимости и доступности присутствующего в DOM элемента для нажатия.
   *
   * @param webElement проверяемый элемент
   * @return веб-элемент, как только он доступен для нажатия
   */
  public WebElement waitForElementToBeClickable(WebElement webElement) {
    return waitForElementToBeClickable(webElement, getDefaultTimeout());
  }

  /**
   * Ожидание появления элемента в DOM, видимости и доступности элемента для нажатия.
   *
   * @param locator          локатор, по которому находится элемент
   * @param timeout timeout ожидания
   * @return веб-элемент, как только он доступен для нажатия
   */
  public WebElement waitForElementToBeClickable(By locator, Duration timeout) {
    return new WebDriverWait(driver, timeout).until(ExpectedConditions.elementToBeClickable(locator));
  }

  /**
   * Ожидание появления элемента в DOM, видимости и доступности элемента для нажатия.
   *
   * @param locator локатор, по которому находится элемент
   * @return веб-элемент, как только он доступен для нажатия
   */
  public WebElement waitForElementToBeClickable(By locator) {
    return waitForElementToBeClickable(locator, getDefaultTimeout());
  }

  /**
   * Ожидание определенного количества элементов с указанным локатором.
   *
   * @param locator          локатор, по которому находится элемент
   * @param number           ожидаемое число элементов
   * @param timeout timeout ожидания
   * @return список элементов с определенным количеством
   */
  public List<WebElement> waitForNumberOfElementsToBe(By locator, int number, Duration timeout) {
    return new WebDriverWait(driver, timeout).until(ExpectedConditions.numberOfElementsToBe(locator, number));
  }

  /**
   * Ожидание определенного количества элементов с указанным локатором.
   *
   * @param locator локатор, по которому находится элемент
   * @param number  ожидаемое число элементов
   * @return список элементов с определенным количеством
   */
  public List<WebElement> waitForNumberOfElementsToBe(By locator, int number) {
    return waitForNumberOfElementsToBe(locator, number, getDefaultTimeout());
  }

  /**
   * Ожидание того, что как минимум один элемент с указанным локатором представлен на странице.
   * Полезно, когда нужно подождать динамический элемент.
   *
   * @param locator          локатор, по которому находится элемент
   * @param timeout timeout ожидания
   * @return список элементов с указанным локатором
   */
  public List<WebElement> waitForPresenceOfAllElementsLocatedBy(By locator, Duration timeout) {
    return new WebDriverWait(driver, timeout).until(ExpectedConditions.presenceOfAllElementsLocatedBy(locator));
  }

  /**
   * Ожидание того, что как минимум один элемент с указанным локатором представлен на странице.
   * Полезно, когда нужно подождать динамический элемент.
   *
   * @param locator локатор, по которому находится элемент
   * @return список элементов с указанным локатором
   */
  public List<WebElement> waitForPresenceOfAllElementsLocatedBy(By locator) {
    return waitForPresenceOfAllElementsLocatedBy(locator, getDefaultTimeout());
  }

  /**
   * Ожидание того, что указзнный элемент содержит атрибут с определенным значением
   *
   * @param webElement проверяемый элемент
   * @param attribute проверяемый атрибут
   * @param value проверяемое значение
   * @param timeout timeout ожидания
   * @return <code>true</code> при наличии значения в атрибуте
   */
  public boolean waitForAttributeContains(WebElement webElement, String attribute, String value, Duration timeout) {
    return new WebDriverWait(driver, timeout).until(ExpectedConditions.attributeContains(webElement, attribute, value));
  }

  /**
   * Ожидание того, что указзнный элемент содержит атрибут с определенным значением
   *
   * @param webElement проверяемый элемент
   * @param attribute проверяемый атрибут
   * @param value проверяемое значение
   * @return <code>true</code> при наличии значения в атрибуте
   */
  public boolean waitForAttributeContains(WebElement webElement, String attribute, String value) {
    return waitForAttributeContains(webElement, attribute, value, getDefaultTimeout());
  }

  public void sleep(long millis) {
    BaseTest.sleep(millis);
  }

  private void waitForCondition(Supplier<Boolean> condition, long timeOutInMillis) {
    long startTime = System.currentTimeMillis();
    while (!condition.get()) {
      if (System.currentTimeMillis() - startTime >= timeOutInMillis) {
        break;
      }
      sleep(100);
    }
  }

  private void waitProgressBarVisible(long timeOutInMillis) {
    waitForCondition(
        () -> driver.findElements(PROGRESS_BAR).size() > 0,
        timeOutInMillis
    );
  }

  //ожидается, что последний инпут, по которому был клик, выбран
  public void waitForInputFieldSelected() {
    waitForNumberOfElementsToBe(INPUT_IS_ACTIVE, 1);
  }

  //ожидается пока пропадет прогресс бар
  public void waitProgressBarNotVisible(Duration timeout) {
    waitForInvisibilityOfElementLocated(PROGRESS_BAR, timeout);
  }

  //ожидается пока пропадет прогресс бар с таймаутом по умолчанию
  public void waitProgressBarNotVisible() {
    waitForInvisibilityOfElementLocated(PROGRESS_BAR, getDefaultTimeout());
  }

  //проверяется отсутствие ошибок
  public boolean thereIsNoError() {
    return getErrors().isEmpty();
  }

  //получить первую ошибку
  public WebElement getError() {
    return getErrors().get(0);
  }

  //получить все ошибки
  public List<WebElement> getErrors() {
    waitForCondition(
        () -> driver.findElements(ERROR).size() > 0,
        500
    );
    return driver.findElements(ERROR);
  }

  //переключиться на другую вкладку
  public void goToAnotherTab(String tab) {
    waitProgressBarNotVisible(Duration.ofSeconds(10));
    driver.switchTo().window(tab);
  }

  //обновить страницу
  public void refreshPage() {
    driver.navigate().refresh();
    waitProgressBarNotVisible();
  }

  public void waitForPageLoad() {
    waitProgressBarNotVisible();
    waitProgressBarVisible(PROGRESS_VISIBILITY_TIMEOUT_MS);
    waitProgressBarNotVisible();
  }
}
