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

import cz.moneta.test.harness.connectors.Connector;
import cz.moneta.test.harness.context.StoreAccessor;
import cz.moneta.test.harness.endpoints.smartbanka.MobilePlatform;
import cz.moneta.test.harness.exception.HarnessException;
import cz.moneta.test.harness.support.mobile.MobileLookup;
import cz.moneta.test.harness.support.web.TextContainer;
import cz.moneta.test.harness.support.web.Until;
import io.appium.java_client.AppiumDriver;
import io.appium.java_client.MobileBy;
import io.appium.java_client.remote.MobileCapabilityType;
import io.appium.java_client.touch.offset.PointOption;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.openqa.selenium.*;
import org.openqa.selenium.remote.CapabilityType;
import org.openqa.selenium.remote.DesiredCapabilities;
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.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;

import static io.appium.java_client.touch.offset.PointOption.point;
import static java.time.Duration.ofMillis;
import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
import static org.openqa.selenium.support.ui.ExpectedConditions.alertIsPresent;

public abstract class AppiumMobileConnector<A extends AppiumDriver> implements Connector {
    private static final Logger logger = LogManager.getLogger(AppiumMobileConnector.class);
    private static final int LAZY_ELEMENT_RENDER_TIMEOUT = 5;
    protected String deviceName;
    protected String udid;
    protected String bundleId;
    protected String appBinaryPath;
    protected String host;
    protected final StoreAccessor store;
    private final Function<String, By> defaultLookup;

    protected abstract A getDriver();

    public abstract void captureVideo(String snapshotFileName);

    public abstract void resetApp();

    public abstract void close();

    public AppiumMobileConnector(Function<String, By> defaultLookup, StoreAccessor store, String host, String appBinaryPath) {
        this.store = store;
        this.host = host;
        this.defaultLookup = defaultLookup;
        this.appBinaryPath = appBinaryPath;
    }

    public String getDeviceName() {
        return deviceName;
    }

    public StoreAccessor getStore() {
        return store;
    }

    protected void setDesiredCapabilities(Platform platformName, String deviceName, String udid, String automationName, DesiredCapabilities capabilities) {
        capabilities.setCapability(CapabilityType.PLATFORM_NAME, platformName);
      //  capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, deviceName);
      //  capabilities.setCapability(MobileCapabilityType.NEW_COMMAND_TIMEOUT, 150);
      //  capabilities.setCapability(MobileCapabilityType.NO_RESET, true);
      //  capabilities.setCapability(MobileCapabilityType.UDID, udid);
      //  capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, automationName);
      //  capabilities.setCapability(MobileCapabilityType.APP, appBinaryPath);
    }

    protected Optional<A> launchExistingApp(A driver) {
        // return Optional.of(driver)
        //        .map(app -> {
        //             driver.launchApp();
        //             return driver;
        //         });
        return Optional.of(driver);
        // TODO: actually not supported
    }

    private void installApp(String path, A driver) {
//        Optional.of(bundleId)
//                .filter(app -> !driver.isAppInstalled(app))
//                .map(installApp -> {
//                    logger.info("App was not found on the device " + deviceName + " and will be installed");
//                    try {
//                        driver.installApp(path);
//                    } catch (WebDriverException e) {
//                        throw new HarnessException("The application at " + path + " does not exist or is not accessible. " +
//                                "Please, check if the path is correct");
//                    }
//                    return driver;
//                });
        // TODO: actually not supported
    }

    protected void mobileLanguage(DesiredCapabilities capabilities, String ENPlatformLocale, String CZPlatformLocale) {
        String languageKey = store.getConfig("appium.language", "cs");
        switch (languageKey) {
            case "en":
                capabilities.setCapability(MobileCapabilityType.LANGUAGE, "en");
                capabilities.setCapability(MobileCapabilityType.LOCALE, ENPlatformLocale);
                break;
            case "cs":
                capabilities.setCapability(MobileCapabilityType.LANGUAGE, "cs");
                capabilities.setCapability(MobileCapabilityType.LOCALE, CZPlatformLocale);
                break;
            default:
                throw new HarnessException("Please, define the appium.language parameter. Available language is cs or en");
        }
    }

    protected Boolean checkVideoRecord() {
        return Boolean.parseBoolean(store.getConfig("appium.video.record", "false"));
    }

    public void click(String path, MobileLookup mobileLookup) {
//        new TouchAction<>(getDriver())
//                .tap(tapOptions().withElement(element(waitForLazyElement(path, mobileLookup))))
//                .perform();
        // TODO: actually not supported
    }

    protected WebElement waitForLazyElement(String path, MobileLookup mobileLookup) {
        doWaitForLazyElement(path, mobileLookup);
        return getDriver().findElement(resolveMobileLookup(mobileLookup, path));
    }

    List<WebElement> waitForLazyElements(String path, MobileLookup mobileLookup) {
        doWaitForLazyElement(path, mobileLookup);
        return getDriver().findElements(resolveMobileLookup(mobileLookup, path));
    }

    private void doWaitForLazyElement(String path, MobileLookup mobileLookup) {
        new FluentWait<>(getDriver())
                .withTimeout(Duration.ofSeconds(LAZY_ELEMENT_RENDER_TIMEOUT))
                .pollingEvery(ofMillis(300))
                .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 @AndroidWait or @IosWait annotation. " + path)
                .until(driver -> !driver.findElements(resolveMobileLookup(mobileLookup, path)).isEmpty());
    }

    private Alert waitForAlert(int timeout) {
        return new FluentWait<>(getDriver())
                .withTimeout(Duration.ofSeconds(timeout))
                .pollingEvery(Duration.ofMillis(300))
                .withMessage("Alert is not present after " + timeout + " seconds timeout.")
                .until(driver -> alertIsPresent().apply(getDriver()));
    }

    public void acceptAlert(int timeout) {
        waitForAlert(timeout).accept();
    }

    public void dismissAlert(int timeout) {
        waitForAlert(timeout).dismiss();
    }

    public String getText(String path, MobileLookup mobileLookup) {
        return waitForLazyElement(path, mobileLookup).getText();
    }

    public void waitForElements(int timeoutSeconds, MobileLookup mobileLookup, Until until, String... elementsToCheck) {
        WebDriverWait wait = new WebDriverWait(getDriver(), Duration.ofSeconds(timeoutSeconds));
        Arrays.stream(elementsToCheck)
                .forEach(path -> wait.until(resolveUntil(until).apply(resolveMobileLookup(mobileLookup, path))));
    }

    public void scrollRight() {
        horizontalScroll(0.2, 0.8, 0.5);
    }

    public void scrollLeft() {
        horizontalScroll(0.8, 0.2, 0.5);
    }

    public void swipeViewLeft() {
        horizontalScroll(0.9, 0.02, 0.88);
    }

    public void swipeViewRight() {
        horizontalScroll(0.02, 0.9, 0.88);
    }

    private void horizontalScroll(double startPercentage, double endPercentage, double anchorPercentage) {
        Dimension size = getDriver().manage().window().getSize();
        int anchor = (int) (size.height * anchorPercentage);
        int startPoint = (int) (size.width * startPercentage);
        int endPoint = (int) (size.width * endPercentage);

        doScroll(point(startPoint, anchor), point(endPoint, anchor));
    }

    public void scrollDown() {
        verticalScroll(0.5, 0.2);
    }

    public void scrollUp() {
        verticalScroll(0.2, 0.5);
    }

    public void scrollLeftUntil(String path, MobileLookup mobileLookup) {
        doScrollUntil(path, mobileLookup, this::scrollLeft);
    }

    public void scrollRightUntil(String path, MobileLookup mobileLookup) {
        doScrollUntil(path, mobileLookup, this::scrollRight);
    }

    public void scrollDownUntil(String path, MobileLookup mobileLookup) {
        doScrollUntil(path, mobileLookup, this::scrollDown);
    }

    public void scrollUpUntil(String path, MobileLookup mobileLookup) {
        doScrollUntil(path, mobileLookup, this::scrollUp);
    }

    public void swipeFromToElement(String source, String target, MobileLookup mobileLookup) {
//        Point fromElementPosition = waitForLazyElement(source, mobileLookup).getCenter();
//        Point toElementPosition = getDriver().findElement(resolveMobileLookup(mobileLookup, target)).getCenter();
//        doScroll(point(fromElementPosition.x, fromElementPosition.y), point(toElementPosition.x, toElementPosition.y));
        // TODO: actually not supported
    }

    private void doScrollUntil(String path, MobileLookup mobileLookup, Runnable scroll) {
        Stream.generate(() -> scroll)
                .limit(10)
                .peek(Runnable::run)
                .filter(r -> isNotEmpty(getDriver().findElements(resolveMobileLookup(mobileLookup, path))))
                .findFirst()
                .orElseThrow(() ->
                        new HarnessException(String.format("Element '%s' not present after 10 swipes", path)));
    }

    private void verticalScroll(double startPercentage, double endPercentage) {
        Dimension size = getDriver().manage().window().getSize();
        int anchor = (int) (size.width * 0.3);
        int startPoint = (int) (size.height * startPercentage);
        int endPoint = (int) (size.height * endPercentage);

        doScroll(point(anchor, startPoint), point(anchor, endPoint));
    }

    private void doScroll(PointOption from, PointOption to) {
//        new TouchAction(getDriver())
//                .press(from)
//                .waitAction(waitOptions(ofMillis(1000)))
//                .moveTo(to)
//                .release().perform();
        // TODO: acctualy not supported
    }

    public void takeSnapshot(String snapshotFileName) {
        String snapshotsPath = store.getConfig("appium.snapshots.path", "target/screenshots");
        File file = Paths.get(snapshotsPath, snapshotFileName).toFile();
        try {
            FileUtils.writeByteArrayToFile(file, getDriver().getScreenshotAs(OutputType.BYTES));
        } catch (IOException e) {
            throw new IllegalStateException("Unable to write " + snapshotFileName + " to " + snapshotsPath, e);
        }
    }

    public void saveSource(String sourceFileName) {
        String sourcesPath = store.getConfig("appium.view.sources.path", "target/sources");
        File file = Paths.get(sourcesPath, sourceFileName).toFile();
        try {
            FileUtils.writeByteArrayToFile(file, getDriver().getPageSource().getBytes());
        } catch (IOException e) {
            throw new IllegalStateException("Unable to write " + sourceFileName + " to " + sourcesPath, e);
        }
    }

    public void type(TextContainer input, String text, boolean clear, MobileLookup mobileLookup) {
        if (clear) {
            clearInput(input, mobileLookup);
        }
        waitForLazyElement(input.getPath(), mobileLookup).sendKeys(text);
    }

    private void clearInput(TextContainer input, MobileLookup mobileLookup) {
        waitForLazyElement(input.getPath(), mobileLookup).clear();
    }

    public WebElementsCheck elementsCheck() {
        return new WebElementsCheck(this);
    }

    private By resolveMobileLookup(MobileLookup mobileLookup, String path) {
        if (MobilePlatform.isAndroid(getStore())) {
            path = mobileLookup.equals(MobileLookup.ANDROID_TEXT) ? "new UiSelector().textContains(\"" + path + "\")" : path;
            path = mobileLookup.equals(MobileLookup.ID) ? bundleId + ":id/" + path : path;
        }
        return resolveLookup(mobileLookup).apply(path);
    }

    private Function<String, By> resolveLookup(MobileLookup mobileLookup) {
        switch (mobileLookup) {
            case XPATH:
                return MobileBy::xpath;
            case ID:
                return MobileBy::id;
            case IOS_NAME:
                return MobileBy::name;
            case ANDROID_TEXT:
                return MobileBy::AndroidUIAutomator;
            default:
                return defaultLookup;
        }
    }

    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;
            default:
                return ExpectedConditions::presenceOfElementLocated;
        }
    }
}
