package com.zjty.autotest.service.impl;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Queues;
import com.google.common.collect.Sets;
import com.zjty.autotest.pojo.report.ElementDetail;
import com.zjty.autotest.pojo.report.Measure;
import com.zjty.autotest.pojo.report.Report;
import com.zjty.autotest.pojo.test.Input;
import com.zjty.autotest.pojo.test.Project;
import com.zjty.autotest.util.FileUtil;
import com.zjty.autotest.util.WebDriverUtil;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Attributes;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.openqa.selenium.*;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.Sleeper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static com.zjty.autotest.common.action.LabelType.INPUT;
import static java.util.Objects.nonNull;

/**
 * @author C
 */
@SuppressWarnings("FieldCanBeLocal")
@Slf4j
@Service
public class SeleniumExecutor {

    @Value("${selenium.executor.wait-after-click}")
    private Long waitTime;

    @Value("${selenium.executor.screenshot.url-host}")
    private String screenshotPrefixHost;

    @Value("${selenium.executor.screenshot.url-port}")
    private String screenshotPrefixPort;

    private WebDriver driver;

    private List<Input> inputs = Lists.newArrayList();

    private Set<String> historyUrls = Sets.newHashSet();

    private Set<Attributes> historyAttributes = Sets.newHashSet();

    private Set<Attributes> currentHistoryAttributes = Sets.newHashSet();

    private Queue<String> windowQueue = Queues.newLinkedBlockingDeque();

    private Queue<String> urlQueue = Queues.newLinkedBlockingDeque();

    /**
     * 当前进行的窗口页
     */
    private String currentWindow;

    /**
     * 当前测试的url
     */
    private String currentUrl;

    /**
     * 当前测试url的源码
     */
    private String currentSource;

    public Report execute(Project project) {
        driver = WebDriverUtil.getWebDriver(project.getBrowser());
        inputs = project.getInputs();
        List<Measure> measures = Lists.newArrayList();
        String os = "";
        currentUrl = project.getUrl();
        currentWindow = driver.getWindowHandle();
        while (nonNull(currentWindow)) {
            driver.switchTo().window(currentWindow);
            while (nonNull(currentUrl)) {
                Measure measure = testUrl(currentUrl);
                if (nonNull(measure)){
                    measures.add(measure);
                }
                currentUrl = urlQueue.poll();
            }
            currentWindow = windowQueue.poll();
            driver.close();
            checkWindow();
        }
        return new Report(
                null,
                project.getId(),
                project.getName(),
                project.getBrowser(),
                os,
                measures
        );
    }

    private Measure testUrl(String currentUrl) {
        Measure measure = null;
        //如果该网页是未曾进入过的网页则加入历史记录并进行遍历测试
        if (!historyUrls.contains(currentUrl)) {
            historyUrls.add(driver.getCurrentUrl());
            //打开网页 记录响应时间
            long startTime = System.currentTimeMillis();
            driver.get(currentUrl);
            currentSource = driver.getPageSource();
            long endTime = System.currentTimeMillis();
            Long responseTime = endTime - startTime;
            sleep();
            log.info("当前URL：{}  响应时间 {} ms  开始进行遍历...", currentUrl, responseTime);
            log.info("剩余URL队列：{}", urlQueue);
            log.info("正在获取当前网页所有元素...");
            //获取当前网页的所有元素并放入元素队列
            Map<Attributes, WebElement> elements = getAllElements(driver);
            log.info("获取完毕...共{}个元素...", elements.size());
            //元素序号重置
            currentHistoryAttributes.clear();
            //开始遍历操作元素队列中的元素 返回各个元素测试信息
            List<ElementDetail> elementDetails = Lists.newArrayList();
            traversal(elements, elementDetails);
            //如果全部元素通过则是通过
            Boolean success = elementDetails.stream().allMatch(ElementDetail::getSuccess);
            //如果出错 则提供截图
            String screenshot = null;
            if (!success) {
                String screenshotName = alterUrlName(currentUrl);
                screenshot(screenshotName);
                screenshot = String.format("http://%s:%s/screenshots/%s", screenshotPrefixHost, screenshotPrefixPort, screenshotName);
            }
            //总合信息
            HashSet<String> messages = Sets.newHashSet();
            elementDetails.forEach(elementDetail -> messages.add(elementDetail.getMessage()));
            StringBuilder message = new StringBuilder();
            messages.forEach(message::append);
            for (String msg : messages) {
                message.append(msg);
                message.append(';');
            }
            log.info("遍历完毕...");
            measure = new Measure(
                    currentUrl,
                    success,
                    responseTime.intValue(),
                    message.toString(),
                    screenshot,
                    elementDetails
            );
        }
        return measure;
    }

    private void traversal(Map<Attributes, WebElement> elements, List<ElementDetail> elementDetails) {
        elements = removeHistoryElements(elements);
        for (Attributes attributes : elements.keySet()) {
            WebElement element = elements.get(attributes);
            if (!currentHistoryAttributes.contains(attributes) && isInputAble(element)) {
                ElementDetail elementDetail = actInput(attributes, element);
                elementDetails.add(elementDetail);
            }
        }
        for (Attributes attributes : elements.keySet()) {
            WebElement element = elements.get(attributes);
            if (!currentHistoryAttributes.contains(attributes) && isClickAble(element)) {
                ElementDetail elementDetail = actClick(attributes, element);
                boolean urlChanged = checkURl();
                boolean sourceChanged = checkSource();
                if (urlChanged) {
                    boolean newPage = !historyUrls.contains(driver.getCurrentUrl())
                            && !Sets.newHashSet(urlQueue).contains(driver.getCurrentUrl())
                            && sourceChanged;
                    if (newPage) {
                        log.info("检测到新url:{} 加入队列 ", driver.getCurrentUrl());
                        urlQueue.add(driver.getCurrentUrl());
                        urlQueue = queueDuplicateRemoval(urlQueue);
                    }
                    Map<Attributes, WebElement> reload = reload();
                    traversal(reload, elementDetails);
                }
                elementDetails.add(elementDetail);
            }
        }
    }

    private ElementDetail actInput(Attributes attributes, WebElement element) {
        String type = String.format("<%s>%s", element.getTagName(), element.getText());
        String message = "";
        Long responseTime = -1L;
        boolean success = false;
        try {
            if (isEnabled(element)) {
                log.info("Input元素 ----- tag：[{}] ------ text:[{}] ---- attrs:[{}]", element.getTagName(), element.getText(), attributes);
                long startTime = System.currentTimeMillis();
                if (!inputs.isEmpty()) {
                    Input input = inputs.remove(0);
                    element.sendKeys(input.getValue());
                }
                long endTime = System.currentTimeMillis();
                responseTime = endTime - startTime;
                success = true;
                Alert alert = ExpectedConditions.alertIsPresent().apply(driver);
                if (nonNull(alert)) {
                    message = "出现警告窗：" + alert.getText();
                    alert.accept();
                    success = false;
                }
            }
        } catch (ElementNotInteractableException e) {
            message = "异常的布局";
            success = false;
            log.error("error:存在可操作范围之外的元素");
        } catch (TimeoutException e) {
            message = "页面超时";
            success = false;
            log.error("error:存在可操作范围之外的元素");
        } catch (WebDriverException e) {
            message = "无法访问的地址";
            success = false;
            log.error("error:无法访问的地址");
        } catch (Exception e) {
            message = "出现预料之外的错误";
            success = false;
            log.error("error:" + e);
        }
        historyAttributes.add(attributes);
        currentHistoryAttributes.add(attributes);
        return new ElementDetail(
                type,
                responseTime.intValue(),
                success,
                message
        );
    }

    private ElementDetail actClick(Attributes attributes, WebElement element) {
        String type = String.format("<%s>%s", element.getTagName(), element.getText());
        String message = "";
        Long responseTIme = 0L;
        boolean success = false;
        try {
            if (isEnabled(element)) {
                log.info("Input元素 ----- tag：[{}] ------ text:[{}] ---- attrs:[{}]", element.getTagName(), element.getText(), attributes);
                element.click();
                sleep();
                success = true;
                Alert alert = ExpectedConditions.alertIsPresent().apply(driver);
                if (nonNull(alert)) {
                    message = "出现警告窗：" + alert.getText();
                    alert.accept();
                    success = false;
                }
            }
        } catch (ElementNotInteractableException e) {
            message = "异常的布局";
            success = false;
            log.error("error:存在可操作范围之外的元素");
        } catch (TimeoutException e) {
            message = "页面超时";
            success = false;
            log.error("error:存在可操作范围之外的元素");
        } catch (WebDriverException e) {
            message = "无法访问的地址";
            success = false;
            log.error("error:无法访问的地址");
        } catch (Exception e) {
            message = "出现预料之外的错误";
            success = false;
            log.error("error:" + e);
        }
        historyAttributes.add(attributes);
        currentHistoryAttributes.add(attributes);
        return new ElementDetail(
                type,
                responseTIme.intValue(),
                success,
                message
        );
    }

    private Boolean checkURl() {
        String practiseUrl = driver.getCurrentUrl();
        return !Objects.equals(currentUrl, practiseUrl);
    }

    private Boolean checkSource() {
        String practise = driver.getPageSource();
        return !Objects.equals(practise, currentSource);
    }

    private Queue<String> queueDuplicateRemoval(Queue<String> queue) {
        HashSet<String> set = Sets.newHashSet(queue);
        return Queues.newLinkedBlockingDeque(set);
    }

    @SuppressWarnings("unused")
    private String getMainUrl(String url) {
        int end = url.indexOf("?");
        return url.substring(0, end);
    }

    @SuppressWarnings("unused")
    private void inputValue(WebElement element, List<Input> inputs) {
        Predicate<Input> inputMatch = input -> Objects.equals(element.getAttribute(input.getAttrName()), input.getAttrValue());
        Optional<Input> inputOptional = inputs.stream().filter(inputMatch).findAny();
        if (inputOptional.isPresent()) {
            Input input = inputOptional.get();
            element.sendKeys(input.getValue());
        }
    }

    private void checkWindow() {
        if (!windowQueue.isEmpty()) {
            windowQueue = Queues.newLinkedBlockingQueue(driver.getWindowHandles());
        }
    }

    private Map<Attributes, WebElement> reload() {
        driver.get(currentUrl);
        sleep();
        currentSource = driver.getPageSource();
        return getAllElements(driver);
    }

    @SuppressWarnings("unused")
    private void sleep() {
        try {
            Sleeper.SYSTEM_SLEEPER.sleep(Duration.ofSeconds(waitTime));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private Map<Attributes, WebElement> getAllElements(WebDriver driver) {
        List<WebElement> webElements = Lists.newArrayList();
        driver.findElements(By.xpath("*"))
                .forEach(element -> getSubElements(element, webElements));
        Document document = Jsoup.parse(driver.getPageSource());
        List<Element> jElements = document.getAllElements().stream()
                .filter(element -> element.childrenSize() == 0)
                .collect(Collectors.toList());
        Map<Attributes, WebElement> result = Maps.newHashMap();
        for (int i = 0; i < webElements.size(); i++) {
            WebElement webElement = webElements.get(i);
            Element jElement = jElements.get(i);
            if (elementMatch(webElement, jElement)) {
                result.put(jElement.attributes(), webElement);
            }
        }
        return result;
    }

    private void getSubElements(WebElement element, List<WebElement> elements) {
        try {
            List<WebElement> results = element.findElements(By.xpath("*"));
            if (results.isEmpty()) {
                elements.add(element);
            } else {
                results.forEach(webElement -> getSubElements(webElement, elements));
            }
        } catch (StaleElementReferenceException ignored) {
        }
    }

    private Boolean isInputAble(WebElement element) {
        return Objects.equals(element.getTagName(), INPUT);
    }

    @SuppressWarnings("unused")
    private Boolean isClickAble(WebElement element) {
        try {
            return nonNull(ExpectedConditions.elementToBeClickable(element).apply(driver));
        } catch (Exception e) {
            return false;
        }
    }

    private Boolean isEnabled(WebElement element) {
        try {
            return nonNull(ExpectedConditions.elementToBeClickable(element).apply(driver));
        } catch (Exception e) {
            return false;
        }
    }

    private String alterUrlName(String fileName) {
        // /\?*:"<>|
        return fileName.replace("/", "")
                .replace("\\", "")
                .replace("#", "")
                .replace("?", "")
                .replace("*", "")
                .replace(":", "")
                .replace("<", "")
                .replace(".", "")
                .replace(">", "")
                .replace("|", "");
    }

    private void screenshot(String name) {
        byte[] bytes = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
        FileUtil.output(bytes, FileUtil.WORK_PATH + "screenshots\\" + name + ".png");
    }

    private Boolean elementMatch(WebElement webElement, Element element) {
        try {
            boolean attributesMatch = element.attributes().asList().stream()
                    .allMatch(attribute -> Objects.equals(attribute.getValue(), webElement.getAttribute(attribute.getKey())));
            boolean tagMatch = Objects.equals(webElement.getTagName(), element.tagName());
            boolean textMatch = Objects.equals(webElement.getText(), element.text());
            if (element.attributes().isEmpty()) {
                if (element.hasText()) {
                    return tagMatch && textMatch;
                } else {
                    return tagMatch;
                }
            } else {
                if (element.hasText()) {
                    return attributesMatch && tagMatch && textMatch;
                } else {
                    return attributesMatch && tagMatch;
                }
            }
        } catch (StaleElementReferenceException e) {
            return false;
        }
    }

    private Map<Attributes, WebElement> removeHistoryElements(Map<Attributes, WebElement> elements) {
        Map<Attributes, WebElement> result = Maps.newHashMap(elements);
        elements.forEach((attributes, element) -> {
            if (historyAttributes.contains(attributes)) {
                result.remove(attributes, element);
            }
        });
        return result;
    }

}
