package com.zjty.autotest.service.impl;

import com.google.common.collect.Lists;
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.openqa.selenium.*;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;

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 Queue<String> windowQueue = Queues.newLinkedBlockingDeque();

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

    private List<WebElement> elements = Lists.newArrayList();

    /**
     * 元素序号记录
     */
    private Integer elementIndex;

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

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

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

    public Report execute(Project project) {
        driver = WebDriverUtil.getWebDriver(project.getBrowser());
        driver.manage().timeouts().pageLoadTimeout(2L, TimeUnit.SECONDS);
        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);
                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;
            log.info("当前URL：{}  响应时间 {} ms  开始进行遍历...", currentUrl, responseTime);
            log.info("剩余URL队列：{}", urlQueue);
            log.info("正在获取当前网页所有元素...");
            //获取当前网页的所有元素并放入元素队列
            getAllElements(driver);
            log.info("获取完毕...共{}个元素...", elements.size());
            //元素序号重置
            elementIndex = 0;
            //开始遍历操作元素队列中的元素 返回各个元素测试信息
            List<ElementDetail> elementDetails = traversal();
            //如果全部元素通过则是通过
            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);
            log.info("遍历完毕...");
            measure = new Measure(
                    currentUrl,
                    success,
                    responseTime.intValue(),
                    message.toString(),
                    screenshot,
                    elementDetails
            );
        }
        return measure;
    }

    private List<ElementDetail> traversal() {
        List<ElementDetail> elementDetails = Lists.newArrayList();
        while (elementIndex < elements.size()) {
            WebElement element = elements.get(elementIndex);
            ElementDetail elementDetail = act(element);
            elementIndex++;
            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);
                }
                reload();
            }
            if (sourceChanged) {
                reload();
            }
            elementDetails.add(elementDetail);
        }
        return elementDetails;
    }

    private ElementDetail act(WebElement element) {
        String type = "";
        String message = null;
        Long responseTIme = 0L;
        boolean success = false;
        try {
            if (isEnabled(element)) {
                log.info("操作第{}个元素 ------ text:[{}] ----- tag：[{}]", elementIndex + 1, element.getText(), element.getTagName());
                if (isInputAble(element) && !inputs.isEmpty()) {
                    Input input = inputs.remove(0);
                    element.sendKeys(input.getValue());
                }
                element.click();
                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 (Exception e) {
            message = "预料之外的异常:" + e.getMessage() + "；";
            success = false;
            log.error("error:" + e);
        }
        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 void reload() {
        driver.get(currentUrl);
        currentSource = driver.getPageSource();
        getAllElements(driver);
    }

    @SuppressWarnings("unused")
    private void sleep() {
        if (waitTime > 0) {
            try {
                Thread.sleep(waitTime);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void getAllElements(WebDriver driver) {
        elements.clear();
        driver.findElements(By.xpath("*"))
                .forEach(this::getSubElements);
    }

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

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

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

    private Boolean isEnabled(WebElement element) {
        return nonNull(ExpectedConditions.elementToBeClickable(element).apply(driver));
    }

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

}
