package cz.moneta.test.harness.connectors.web;

import cz.moneta.test.harness.exception.HarnessException;
import cz.moneta.test.harness.support.web.Lookup;
import cz.moneta.test.harness.support.web.Until;
import cz.moneta.test.harness.utils.expectedconditions.BooleanExpectedConditions;
import org.apache.commons.lang3.ArrayUtils;
import org.openqa.selenium.*;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.FluentWait;
import org.openqa.selenium.support.ui.WebDriverWait;

import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;

public class Waits {

    private final SeleniumWebConnector connector;
    private static final int LAZY_ELEMENT_RENDER_TIMEOUT = 5;

    public Waits(SeleniumWebConnector connector) {
        this.connector = connector;
    }

    public void waitForElements(int timeoutSeconds, Lookup lookup, Until until, String... elementsToCheck) {
        WebDriverWait wait = new WebDriverWait(connector.getDriver(), Duration.ofSeconds(timeoutSeconds));
        wait.ignoring(WebDriverException.class);

        Arrays.stream(elementsToCheck)
                .forEach(path -> wait.until(resolveUntil(until).apply(byLocator(lookup, path))));
    }

    public void waitForAtLeastOneElement(int timeoutSeconds, Lookup lookup, String... elementsToCheck) {
        WebDriverWait wait = new WebDriverWait(connector.getDriver(), Duration.ofSeconds(timeoutSeconds));

        ExpectedCondition[] expectedConditions = Arrays.stream(elementsToCheck)
                .map(path ->
                        ExpectedConditions.visibilityOfElementLocated(byLocator(lookup, path)))
                .toArray(ExpectedCondition[]::new);
        wait.until(ExpectedConditions.or(expectedConditions));
    }

    WebElement waitForLazyElement(String path, Lookup lookup) {
        doWaitForLazyElement(path, lookup);
        return connector.getDriver()
                .findElement(byLocator(lookup, path));
    }

    List<WebElement> waitForLazyElements(String path, Lookup lookup) {
        doWaitForLazyElement(path, lookup);
        return connector.getDriver()
                .findElements(byLocator(lookup, path));
    }

    private void doWaitForLazyElement(String path, Lookup lookup) {
        new FluentWait<>(connector.getDriver())
                .withTimeout(Duration.ofSeconds(LAZY_ELEMENT_RENDER_TIMEOUT))
                .pollingEvery(Duration.ofMillis(500))
                .ignoring(NoSuchElementException.class)
                .ignoring(StaleElementReferenceException.class)
                .withMessage("Element/s not found within " + LAZY_ELEMENT_RENDER_TIMEOUT
                        + " seconds timeout. If this error occurs, you might want to consider using explicit @Wait annotation. " + path)
                .until(driver -> !driver.findElements(byLocator(lookup, path)).isEmpty());
    }

    public void waitForElementAndRefresh(String waitElementPath, String refreshElementPath, Lookup waitElementLookup,
                                         Lookup refreshElementLookup, int timeoutSeconds, int pollingEverySeconds) {
        Runnable refreshFunction = () -> connector.clicks().click(() -> refreshElementPath, refreshElementLookup);
        waitForElementAndRefresh(waitElementPath, waitElementLookup, refreshFunction, timeoutSeconds,
                pollingEverySeconds, true);
//        waitForLazyElement(refreshElementPath, refreshElementLookup).click();
    }

    public void waitForElementAndRefresh(String waitElementPath, Lookup waitElementLookup, Runnable refreshFunction,
                                         int timeoutSeconds, int pollingEverySeconds, boolean shouldBeWaitElementVisible) {
        new FluentWait<>(connector.getDriver())
                .withTimeout(Duration.ofSeconds(timeoutSeconds))
                .pollingEvery(Duration.ofSeconds(pollingEverySeconds))
                .ignoring(NoSuchElementException.class)
                .ignoring(StaleElementReferenceException.class)
                .ignoring(TimeoutException.class)
                .withMessage("Element/s not present within " + timeoutSeconds + " seconds timeout. Wait and refreshed " +
                        "every: " + pollingEverySeconds + " seconds.\n For element: " + waitElementPath)
                .until(driver -> {
                    if (shouldBeWaitElementVisible == true) {
                        return checkIfElementPresentAndRefresh(waitElementPath, waitElementLookup, refreshFunction, driver);
                    } else {
                        return checkIfElementDisappearAndRefresh(waitElementPath, waitElementLookup, refreshFunction, driver);
                    }
                });
    }

    private WebElement checkIfElementPresentAndRefresh(String waitElementPath, Lookup waitElementLookup, Runnable refreshFunction, WebDriver driver) {
        Optional.of(driver.findElements(byLocator(waitElementLookup, waitElementPath)))
                .filter(Collection::isEmpty)
                .ifPresent(col -> refreshFunction.run());
        return driver.findElement(byLocator(waitElementLookup, waitElementPath));
    }

    private boolean checkIfElementDisappearAndRefresh(String waitElementPath, Lookup waitElementLookup, Runnable refreshFunction, WebDriver driver) {
        Optional.of(driver.findElements(byLocator(waitElementLookup, waitElementPath)))
                .ifPresent(col -> refreshFunction.run());
        return driver.findElements(byLocator(waitElementLookup, waitElementPath)).size() == 0;
    }

    public void waitOrFailOnErrorElement(int timeoutSeconds, String pathForWaiting, String pathToFail, Lookup lookup) {
        WebDriverWait wait = new WebDriverWait(connector.getDriver(), Duration.ofSeconds(timeoutSeconds));
        wait.withTimeout(Duration.ofSeconds(timeoutSeconds))
                .pollingEvery(Duration.ofSeconds(3))
                .withMessage("Element " + pathForWaiting + " or " + pathToFail + " isn't present after " + timeoutSeconds + " seconds")
                .until(ExpectedConditions.or(
                        visibilityOfElementLocated(lookup, pathForWaiting),
                        visibilityOfElementLocated(lookup, pathToFail)));
        if (connector.elementsChecks().isElementPresent(pathToFail, lookup)) {
            throw new HarnessException(pathToFail + " is present instead of " + pathForWaiting);
        }
    }

    /**
     * Method for wait if all elements passed as parameter in defined state (Visible, Gone, Present in DOM) and there is not
     * visible fail element. Fail element is for example error screen.
     * <p>
     * Wait is implemented as fluent wait with 500ms polling and all parameters are transformed into expected conditions.
     * These conditions are used in fluent wait.
     * </p>
     * <p>
     * In case of fail element visible is thrown FailElementDisplayedException.
     * </p>
     *
     * @param timeoutSeconds  - wait timeout
     * @param lookup          - define locator strategy
     * @param until           - state to check - Visible, Gone, Present in DOM
     * @param xpathToFail     - xpath for fail element/error screen
     * @param elementsToCheck - elements to be wait for
     */
    public void waitOrFailOnErrorXpath(int timeoutSeconds, Lookup lookup, Until until, String xpathToFail, String... elementsToCheck) {
        ExpectedCondition[] conditionsToCheck = Arrays.stream(elementsToCheck)
                .map(elementToCheck -> resolveUntil(until).apply(byLocator(lookup, elementToCheck)))
                .toArray(ExpectedCondition[]::new);

        ExpectedCondition[] failElementNotDisplayed = new ExpectedCondition[]{BooleanExpectedConditions.failElementNotDisplayed(byLocator(Lookup.XPATH, xpathToFail))};

        ExpectedCondition[] conditionsToEvaluate = ArrayUtils.addAll(failElementNotDisplayed, conditionsToCheck);

        WebDriverWait wait = new WebDriverWait(connector.getDriver(), Duration.ofSeconds(timeoutSeconds));
        wait.withTimeout(Duration.ofSeconds(timeoutSeconds))
                .pollingEvery(Duration.ofMillis(500L))
                .withMessage(getErrorElementsMessage(timeoutSeconds, elementsToCheck))
                .until(ExpectedConditions.and(conditionsToEvaluate));
    }

    private String getErrorElementsMessage(int timeoutSeconds, String... elements) {
        String elementsInString = String.join(",", elements);
        return "Some of listed elements isn't present after " + timeoutSeconds + " seconds. Elements: " + elementsInString;
    }

    public By byLocator(Lookup lookup, String path) {
        return connector.resolveLookup(lookup).apply(path);
    }

    private ExpectedCondition<WebElement> visibilityOfElementLocated(Lookup lookup, String path) {
        return ExpectedConditions.visibilityOfElementLocated(byLocator(lookup, path));
    }

    private Function<By, ExpectedCondition<?>> resolveUntil(Until until) {
        switch (until) {
            case VISIBLE:
                return ExpectedConditions::visibilityOfElementLocated;
            case PRESENT_IN_DOM:
                return ExpectedConditions::presenceOfElementLocated;
            case GONE:
                return ExpectedConditions::invisibilityOfElementLocated;
            case CLICKABLE:
                return ExpectedConditions::elementToBeClickable;
            default:
                return ExpectedConditions::presenceOfElementLocated;
        }
    }
}