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.common.action.LabelType;
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.logging.LogEntry;
import org.openqa.selenium.logging.LogType;
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.NoSuchElementException;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static com.zjty.autotest.common.action.Action.*;
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("${screenshot-path}")
    private String screenshotPath;

    @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());
        driver.manage().window().maximize();
        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) + ".png";
                screenshot(screenshotName);
                screenshot = String.format("/%s", screenshotName);
            }
            //总合信息
            Set<String> messages = elementDetails.stream()
                    .map(ElementDetail::getMessage)
                    .filter(Objects::nonNull)
                    .collect(Collectors.toSet());
            StringBuilder message = new StringBuilder();
            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 = act(attributes, element, INPUT);
                elementDetails.add(elementDetail);
            }
        }
        for (Attributes attributes : elements.keySet()) {
            WebElement element = elements.get(attributes);
            if (!currentHistoryAttributes.contains(attributes) && isClickAble(element)) {
                ElementDetail elementDetail = act(attributes, element, CLICK);
                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);
                }
                if (nonNull(elementDetail)) {
                    elementDetails.add(elementDetail);
                }
            }
        }
    }

    private ElementDetail act(Attributes attributes, WebElement element, String actType) {
        String type = String.format("<%s>%s", element.getTagName(), element.getText());
        String message = null;
        long responseTime = -1L;
        boolean success = false;
        try {
            if (isEnabled(element)) {
                Boolean locationMatch = checkLocation(element);
                if (!locationMatch) {
                    message = "布局错误";
                    log.error("error:布局错误");
                    historyAttributes.add(attributes);
                    currentHistoryAttributes.add(attributes);
                    return new ElementDetail(
                            type,
                            (int) responseTime,
                            false,
                            message
                    );
                }
                long startTime = System.currentTimeMillis();
                if (Objects.equals(actType, INPUT)) {
                    log.info("Input操作 元素 ----- tag：[{}] ------ text:[{}] ---- attrs:[{}]", element.getTagName(), element.getText(), attributes);
                    if (!inputs.isEmpty()) {
                        Input input = inputs.remove(0);
                        element.sendKeys(input.getValue());
                    }
                    success = true;
                } else if (Objects.equals(actType, CLICK)) {
                    log.info("Click操作 元素 ----- tag：[{}] ------ text:[{}] ---- attrs:[{}]", element.getTagName(), element.getText(), attributes);
                    element.click();
                    sleep();
                    success = true;
                }
                long endTime = System.currentTimeMillis();
                responseTime = endTime - startTime;
                Alert alert = ExpectedConditions.alertIsPresent().apply(driver);
                String jsMsg = checkJsError();
                if (nonNull(alert)) {
                    message = "error:出现警告窗：" + alert.getText();
                    alert.accept();
                    success = false;
                } else if (nonNull(jsMsg)) {
                    message = jsMsg;
                    success = false;
                    log.error("error:出现js异常:{}", jsMsg);
                }
            }
        } catch (ElementNotInteractableException e) {
            log.error("error:可操作范围之外的元素");
            return null;
        } catch (TimeoutException e) {
            message = "页面超时";
            success = false;
            log.error("error:页面超时");
        } catch (NoSuchElementException 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,
                (int) responseTime,
                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(), LabelType.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);
        String path;
        if (screenshotPath.endsWith("/")) {
            path = screenshotPath + name;
        } else {
            path = screenshotPath + "/" + name;
        }
        FileUtil.output(bytes, path);
    }

    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;
    }

    private Boolean checkLocation(WebElement element) {
        try {
            Dimension size = driver.manage().window().getSize();
            Point elementLocation = element.getLocation();
            Dimension elementSize = element.getSize();
            boolean xMatch = (elementLocation.x) >= 0 && (elementLocation.x + elementSize.width <= size.width);
            boolean yMatch = (elementLocation.y) >= 0 && (elementLocation.y + elementSize.width <= size.height);
            return xMatch && yMatch;
        } catch (Exception e) {
            return false;
        }
    }

    @SuppressWarnings("OptionalGetWithoutIsPresent")
    private String checkJsError() {
        try {
            return analyseLogError(driver.manage().logs().get(LogType.BROWSER).getAll().stream().findFirst().get());
        } catch (Exception e) {
            return null;
        }
    }

    private String analyseLogError(LogEntry logEntry) {
        if (logEntry.getMessage().contains("Uncaught TypeError:")) {
            return "JS类型错误";
        } else if (logEntry.getMessage().contains("Failed to load resource: net::ERR_FILE_NOT_FOUND")) {
            return "资源加载错误";
        } else if (logEntry.getMessage().contains("Failed to load resource: net::ERR_CONNECTION_RESET")) {
            return "数据请求错误";
        } else if (logEntry.getMessage().contains("Failed to load resource: ")) {
            return "加载依赖错误";
        }
        return null;
    }

}
