package br.ufrgs.inf.prosoft.shopizer;

import br.ufrgs.inf.prosoft.shopizer.pagecontext.*;
import com.gargoylesoftware.htmlunit.SilentCssErrorHandler;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.javascript.background.JavaScriptJobManager;
import com.google.common.base.Function;
import org.apache.commons.lang3.RandomStringUtils;
import org.openqa.selenium.*;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.htmlunit.HtmlUnitDriver;
import org.openqa.selenium.support.ui.FluentWait;
import org.openqa.selenium.support.ui.Wait;
import org.reflections.Reflections;
import org.reflections.scanners.MethodAnnotationsScanner;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import static org.testng.Assert.assertNotNull;

public class WorkloadSimulation {

    private String baseUrl = "http://localhost:8080/sm-shop/shop/";
    private StringBuffer verificationErrors = new StringBuffer();
    private String currentPage = "home";
    private Reflections reflections;
    private PageRestrictionsValue logged = PageRestrictionsValue.NO;
    private PageRestrictionsValue emptyCart = PageRestrictionsValue.YES;

    //workload mix of read and write, set read probabilities between 0 and 1
    private double workloadMixRead = 0.8;
    //private double workloadMixRead = 0.5;

    public WebElement fluentWait(final By locator, WebDriver driver) {
        Wait<WebDriver> wait = new FluentWait<WebDriver>(driver)
                .withTimeout(10, TimeUnit.SECONDS)
                .pollingEvery(2, TimeUnit.SECONDS)
                .ignoring(NoSuchElementException.class, InvalidElementStateException.class);

        WebElement webelement = wait.until(new Function<WebDriver, WebElement>() {
            public WebElement apply(WebDriver driver) {
                return driver.findElement(locator);
            }
        });

        return webelement;
    }

    protected class SilentHtmlUnitDriver extends HtmlUnitDriver {
        SilentHtmlUnitDriver() {
            super();
            this.getWebClient().setCssErrorHandler(new SilentCssErrorHandler());
        }
    }

    @org.testng.annotations.Test(invocationCount = 1, threadPoolSize = 1)
    public void randomNavigation() throws Exception {
        System.setProperty("webdriver.gecko.driver", "/home/jhonnymertz/Downloads/geckodriver");
        WebDriver driver = new FirefoxDriver();

//        WebDriver driver = new HtmlUnitDriver(true);
        driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);

        reflections = new Reflections(
                new ConfigurationBuilder()
                        .setUrls(ClasspathHelper.forPackage(this.getClass().getPackage().getName()))
                        .setScanners(new MethodAnnotationsScanner()));

        Client c = new Client();

        Calendar startCal = Calendar.getInstance();
        long start = startCal.getTimeInMillis();

        Calendar endCal = startCal;
        endCal.add(Calendar.SECOND, (int) c.getSessionTimeInSeconds());
        long end = endCal.getTimeInMillis();

        System.out.println("Session time in seconds: " + c.getSessionTimeInSeconds());
        System.out.println("Session will end: " + endCal.getTime());

        try {
            driver.get(baseUrl);

            //random click to focus on page
            List<WebElement> featuredItems = driver.findElements(By.className("product-box"));
            int position = new Random().nextInt(featuredItems.size());
            featuredItems.get(position).findElement(By.linkText("Details")).click();


            Thread.sleep(2000);
            verifyRestrictionsAndGoTo(this.getClass().getMethod("login", WebDriver.class), driver);
            Thread.sleep(3000);
            verifyRestrictionsAndGoTo(this.getClass().getMethod("logout", WebDriver.class), driver);
            Thread.sleep(3000);

            //session time control
//            while (Calendar.getInstance().getTimeInMillis() < end) {
//                double thinkTime = c.getThinkingTimeInSeconds();
//                System.out.println("Thinking for " + c.getThinkingTimeInSeconds());
//                Thread.sleep((long) (thinkTime * 1000));
////                Thread.sleep(500);
//                nextAction(driver);
//            }
            driver.quit();
        } catch (Exception e) {
            e.printStackTrace();
            driver.quit();
        }
    }

    private void nextAction(WebDriver driver) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Set<Method> resources = reflections.getMethodsAnnotatedWith(HeaderMenu.class);
        Method nextPageMethod;

        switch (currentPage) {
            case "home":
                resources.addAll(reflections.getMethodsAnnotatedWith(FromHome.class));
                resources.addAll(reflections.getMethodsAnnotatedWith(UserMenu.class));
                nextPageMethod = getRandomNextPageMethod(getFilteredMethods(resources));
                verifyRestrictionsAndGoTo(nextPageMethod, driver);
                break;
            case "productList":
                resources.addAll(reflections.getMethodsAnnotatedWith(FromProductList.class));
                resources.addAll(reflections.getMethodsAnnotatedWith(UserMenu.class));
                nextPageMethod = getRandomNextPageMethod(getFilteredMethods(resources));
                verifyRestrictionsAndGoTo(nextPageMethod, driver);
                break;
            case "searchProduct":
                resources.addAll(reflections.getMethodsAnnotatedWith(FromProductList.class));
                resources.addAll(reflections.getMethodsAnnotatedWith(UserMenu.class));
                nextPageMethod = getRandomNextPageMethod(getFilteredMethods(resources));
                verifyRestrictionsAndGoTo(nextPageMethod, driver);
                break;
            case "product":
                resources.addAll(reflections.getMethodsAnnotatedWith(FromProduct.class));
                resources.addAll(reflections.getMethodsAnnotatedWith(UserMenu.class));
                nextPageMethod = getRandomNextPageMethod(getFilteredMethods(resources));
                verifyRestrictionsAndGoTo(nextPageMethod, driver);
                break;
            case "shopCart":
                resources.addAll(reflections.getMethodsAnnotatedWith(FromShopCart.class));
                nextPageMethod = getRandomNextPageMethod(getFilteredMethods(resources));
                verifyRestrictionsAndGoTo(nextPageMethod, driver);
                break;
            case "register":
                resources.addAll(reflections.getMethodsAnnotatedWith(FromHome.class));
                resources.addAll(reflections.getMethodsAnnotatedWith(UserMenu.class));
                nextPageMethod = getRandomNextPageMethod(getFilteredMethods(resources));
                verifyRestrictionsAndGoTo(nextPageMethod, driver);
                break;
        }
    }

    private Set<Method> getFilteredMethods(Set<Method> resources) {
        Set<Method> filteredResources = new HashSet<>();

        double d = Math.random();
        if (d < workloadMixRead) {
            for (Method method : resources) {
                if (method.getAnnotation(Mix.class).pageContext().equals(PageContext.READ))
                    filteredResources.add(method);
            }
        } else {
            for (Method method : resources) {
                if (method.getAnnotation(Mix.class).pageContext().equals(PageContext.WRITE))
                    filteredResources.add(method);
            }
        }

        if (filteredResources.isEmpty())
            return resources;

        return filteredResources;
    }

    private void verifyRestrictionsAndGoTo(Method nextPageMethod, WebDriver driver) throws IllegalAccessException, InvocationTargetException {
        System.out.println("Trying to access " + nextPageMethod.getName());
        PageRestrictions pageRestrictions = nextPageMethod.getAnnotation(PageRestrictions.class);
        if (pageRestrictions == null) {
            goNextPage(nextPageMethod, driver);
        } else {
            if ((logged == pageRestrictions.logged() || pageRestrictions.logged() == PageRestrictionsValue.NIL) && (emptyCart == pageRestrictions.emptyCart() || pageRestrictions.emptyCart() == PageRestrictionsValue.NIL)) {
                goNextPage(nextPageMethod, driver);
            } else {
                System.out.println("Page " + nextPageMethod.getName() + " no accessed due to restrictions. Expected: logged = " + pageRestrictions.logged() + " emptyCart = " + pageRestrictions.emptyCart() + " Found: logged = " + logged + " emptyCart = " + emptyCart);
            }
        }
    }

    private void goNextPage(Method nextPage, WebDriver driver) throws IllegalAccessException, InvocationTargetException {
        Object next = nextPage.invoke(this, driver);
        if (next == null)
            currentPage = nextPage.getName();
        else currentPage = (String) next;
    }

    private Method getRandomNextPageMethod(Set<Method> resources) {
        int item = new Random().nextInt(resources.size()); // In real life, the Random object should be rather more shared than this
        int i = 0;
        Method nextPage;
        for (Method obj : resources) {
            if (i == item)
                return obj;
            i = i + 1;
        }
        return null;
    }

    //available navigation

    @HeaderMenu
    @Mix
    public void home(WebDriver driver) {
        driver.findElement(By.linkText("Home")).click();
    }

    @HeaderMenu
    @Mix
    public void searchProduct(WebDriver driver) throws Exception {
        driver.findElement(By.id("searchField")).clear();
        String[] keywords = {"computer", "tech", "doll", "book", "spring", "des", "product"};
        driver.findElement(By.id("searchField")).sendKeys(keywords[randInt(keywords.length)]);
//        driver.findElement(By.id("searchButton")).click();
    }

    @HeaderMenu
    @Mix
    public String selectCategory(WebDriver driver) throws Exception {
        List<WebElement> featuredCategories = driver.findElement(By.id("mainMenu")).findElements(By.className("current"));
        featuredCategories.get(new Random().nextInt(featuredCategories.size())).click();
        return "productList";
    }

    @UserMenu
    @PageRestrictions(emptyCart = PageRestrictionsValue.NO, logged = PageRestrictionsValue.NIL)
    @Mix(pageContext = PageContext.WRITE)
    public String shopCart(WebDriver driver) throws Exception {
        driver.findElement(By.id("open-cart")).click();

        WebElement element;
        if (driver.findElement(By.id("shoppingcart")).isDisplayed()) {
            element = driver.findElement(By.className("checkoutButton"));
            if (element.isDisplayed())
                element.click();
            else
                return currentPage;
        } else {
            driver.findElement(By.id("open-cart")).click();
            System.out.println("No items on shopcart, doing another thing...");
            return currentPage;
        }

        return "shopCart";
    }

    @FromShopCart
    @Mix(pageContext = PageContext.WRITE)
    private String checkout(WebDriver driver) {
        fluentWait(By.id("checkoutButton"), driver).click();

        fluentWait(By.id("submitOrder"), driver);

        if (!driver.findElement(By.cssSelector("a.dropdown-toggle.noboxshadow")).getText().contains("Welcome")) {
            driver.findElement(By.id("customer.firstName")).clear();
            driver.findElement(By.id("customer.firstName")).sendKeys("random");
            driver.findElement(By.id("customer.lastName")).clear();
            driver.findElement(By.id("customer.lastName")).sendKeys("random");
            driver.findElement(By.id("customer.emailAddress")).clear();
            driver.findElement(By.id("customer.emailAddress")).sendKeys("random@random.com");
            driver.findElement(By.id("customer.billing.company")).clear();
            driver.findElement(By.id("customer.billing.company")).sendKeys("random");
            driver.findElement(By.id("customer.billing.address")).clear();
            driver.findElement(By.id("customer.billing.address")).sendKeys("random");
            driver.findElement(By.id("customer.billing.city")).clear();
            driver.findElement(By.id("customer.billing.city")).sendKeys("random");
            driver.findElement(By.id("billingPostalCode")).clear();
            driver.findElement(By.id("billingPostalCode")).sendKeys("J4B 8J9");
        }

        driver.findElement(By.id("customer.billing.phone")).clear();
        driver.findElement(By.id("customer.billing.phone")).sendKeys("4523112323");
        driver.findElement(By.id("submitOrder")).click();

        try {
            assertNotNull(fluentWait(By.xpath("//*[contains(text(),'Order completed')]"), driver));
            home(driver);
            emptyCart = PageRestrictionsValue.YES;
        } catch (Error e) {
            verificationErrors.append(e.toString());
        }
        return "home";
    }


    @FromHome
    @FromProductList
    @Mix
    private String openProduct(WebDriver driver) {
        try {
            fluentWait(By.className("product-box"), driver);

            List<WebElement> featuredItems = driver.findElements(By.className("product-box"));
            int pos = 0;
            if (featuredItems.size() > 1)
                pos = new Random().nextInt(featuredItems.size());
            featuredItems.get(pos).findElement(By.linkText("Details")).click();
            return "product";
        } catch (NoSuchElementException | ArrayIndexOutOfBoundsException | TimeoutException e) {
            System.out.println("No items on page, trying another thing...");
            return currentPage;
        }
    }

    @FromProductList
    @FromProduct
    @Mix(pageContext = PageContext.WRITE)
    private String addItemToCart(WebDriver driver) {

        try {
            fluentWait(By.className("addToCart"), driver);

            List<WebElement> featuredItems = driver.findElements(By.className("addToCart"));
            int pos = 0;
            if (featuredItems.size() > 1)
                pos = new Random().nextInt(featuredItems.size());
            featuredItems.get(pos).click();
            emptyCart = PageRestrictionsValue.NO;
        } catch (NoSuchElementException | ArrayIndexOutOfBoundsException | TimeoutException e) {
        }

        System.out.println("No items on page, trying another thing...");

        return currentPage;
    }

    @UserMenu
    @PageRestrictions(logged = PageRestrictionsValue.NO, emptyCart = PageRestrictionsValue.NIL)
    @Mix
    public String login(WebDriver driver) {
//        assertEquals("Signin", driver.findElement(By.id("signinDrop")).getText());
        try {
            fluentWait(By.id("signinDrop"), driver).click();
            int id = randInt(400);
            driver.findElement(By.id("signin_userName")).clear();
            driver.findElement(By.id("signin_userName")).sendKeys("customer" + id);
            driver.findElement(By.id("signin_password")).clear();
            driver.findElement(By.id("signin_password")).sendKeys("customer" + id);
            driver.findElement(By.id("login-button")).click();
            logged = PageRestrictionsValue.YES;
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("Couldnt login");
        }
        return "home";
    }

    @UserMenu
    @PageRestrictions(logged = PageRestrictionsValue.NO, emptyCart = PageRestrictionsValue.NIL)
    @Mix(pageContext = PageContext.WRITE)
    public void register(WebDriver driver) {
        driver.findElement(By.id("signinDrop")).click();
        driver.findElement(By.linkText("Not yet registered?")).click();

        try {
            fluentWait(By.id("recaptcha_response_field"), driver);

            String name = RandomStringUtils.randomAlphanumeric(8);
            String pass = RandomStringUtils.randomAlphanumeric(10);

            driver.findElement(By.id("firstName")).clear();
            driver.findElement(By.id("firstName")).sendKeys(name);
            driver.findElement(By.id("lastName")).clear();
            driver.findElement(By.id("lastName")).sendKeys(name);
            driver.findElement(By.id("userName")).clear();
            driver.findElement(By.id("userName")).sendKeys(name);
            driver.findElement(By.id("email")).clear();
            driver.findElement(By.id("email")).sendKeys(name + "@gmia.com");
            driver.findElement(By.id("password")).clear();
            driver.findElement(By.id("password")).sendKeys(pass);
            driver.findElement(By.id("passwordAgain")).clear();
            driver.findElement(By.id("passwordAgain")).sendKeys(pass);
            driver.findElement(By.id("recaptcha_response_field")).clear();
            driver.findElement(By.id("recaptcha_response_field")).sendKeys("123123");
            driver.findElement(By.id("submitRegistration")).click();
            logged = PageRestrictionsValue.YES;
            home(driver);
        } catch (Exception e) {

        }
    }

    @UserMenu
    @PageRestrictions(logged = PageRestrictionsValue.YES, emptyCart = PageRestrictionsValue.NIL)
    @Mix
    public String logout(WebDriver driver) {
        try {
            driver.findElement(By.cssSelector("#fat-menu > a.dropdown-toggle.noboxshadow")).click();
            fluentWait(By.linkText("Logout"), driver).click();
            logged = PageRestrictionsValue.NO;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "home";
    }

    @Admin
    @Mix(pageContext = PageContext.WRITE)
    public void addProduct(WebDriver driver) {
        driver.get(baseUrl + "/sm-shop/admin/logon.html");
        driver.findElement(By.id("j_username")).clear();
        driver.findElement(By.id("j_username")).sendKeys("admin");
        driver.findElement(By.id("j_password")).clear();
        driver.findElement(By.id("j_password")).sendKeys("password");
        driver.findElement(By.id("formSubmitButton")).click();
        driver.findElement(By.linkText("Catalogue")).click();
        driver.findElement(By.linkText("Products")).click();
        driver.findElement(By.id("catalogue-products-create-link")).click();
        driver.findElement(By.id("sku")).clear();
        driver.findElement(By.id("sku")).sendKeys("123123123123123asdas2");
        driver.findElement(By.id("name0")).clear();
        driver.findElement(By.id("name0")).sendKeys("123123123123");
        driver.findElement(By.cssSelector("button.btn.btn-success")).click();

        try {
            fluentWait(By.id("store.success"), driver);
        } catch (Exception e) {

        }
    }

    public int randInt(int max) {
        return ThreadLocalRandom.current().nextInt(max);
    }
}
