/*
 * Copyright 2019-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.vividus.bdd.steps.integration;

import java.io.IOException;
import java.net.URI;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.inject.Inject;

import org.apache.commons.lang3.tuple.Pair;
import org.jbehave.core.annotations.Then;
import org.jbehave.core.model.ExamplesTable;
import org.jsoup.nodes.Element;
import org.jsoup.select.Selector.SelectorParseException;
import org.vividus.http.HttpMethod;
import org.vividus.http.HttpRequestExecutor;
import org.vividus.http.HttpTestContext;
import org.vividus.http.client.HttpResponse;
import org.vividus.reporter.event.AttachmentPublisher;
import org.vividus.softassert.SoftAssert;
import org.vividus.testcontext.ContextCopyingExecutor;
import org.vividus.ui.web.configuration.WebApplicationConfiguration;
import org.vividus.util.HtmlUtils;
import org.vividus.util.UriUtils;
import org.vividus.validator.ResourceValidator;
import org.vividus.validator.model.CheckStatus;
import org.vividus.validator.model.ResourceValidation;

public class ResourceCheckSteps
{
    private static final Set<String> ALLOWED_SCHEMES = Set.of("http", "https");
    private static final String EXCLUDE_PATTERN = "#";

    @Inject private ResourceValidator resourceValidator;
    @Inject private AttachmentPublisher attachmentPublisher;
    @Inject private HttpRequestExecutor httpRequestExecutor;
    @Inject private SoftAssert softAssert;
    @Inject private WebApplicationConfiguration webApplicationConfiguration;
    @Inject private ContextCopyingExecutor executor;
    @Inject private HttpTestContext httpTestContext;

    private URI mainApplicationPageURI;
    private Pattern excludeHrefsPattern;

    private Optional<String> uriToIgnoreRegex;

    public void init()
    {
        mainApplicationPageURI = webApplicationConfiguration.getMainApplicationPageUrl();
        excludeHrefsPattern = Pattern.compile(uriToIgnoreRegex.map(p -> p + "|" + EXCLUDE_PATTERN)
                                                           .orElse(EXCLUDE_PATTERN));
    }

    /**
     * Step purpose is checking of resources availability;
     * Steps follows next logic:
     *     1. Gathers all the elements by CSS selector
     *     2. For each element executes HEAD request;
     *         a. If status code acceptable than check considered as passed;
     *         b. If status code not acceptable but one of (404, 405, 501, 503) then GET request will be sent;
     *         c. If GET status code acceptable than check considered as passed otherwise failed;
     *     3. If element doesn't contain href or src attribute fail assertion will be recorded
     * @param cssSelector to locate resources
     * @param html to validate
     * @throws InterruptedException when a thread is interrupted
     * @throws ExecutionException  when exception thrown before result get
     */
    @Then("all resources by selector `$cssSelector` from $html are valid")
    public void checkResources(String cssSelector, String html) throws InterruptedException, ExecutionException
    {
        execute(() -> {
            Stream<Element> resourcesToValidate = getElements(cssSelector, html);
            Stream<ResourceValidation> validations = createResourceValidations(resourcesToValidate,
                p -> new ResourceValidation(p.getLeft(), p.getRight()));
            validateResources(validations);
        });
    }

    private void validateResources(Stream<ResourceValidation> resourceValidation)
    {
        Set<ResourceValidation> results = resourceValidation
               .map(this::validate)
               .collect(Collectors.toCollection(TreeSet::new));
        attachmentPublisher.publishAttachment("resources-validation-result.ftl", Map.of("results", results),
                "Resource validation results");
    }

    private ResourceValidation validate(ResourceValidation r)
    {
        return r.getUri() == null || CheckStatus.FILTERED == r.getCheckStatus()
                ? r
                : resourceValidator.perform(r);
    }

    private Stream<Element> getElements(String cssSelector, String html)
    {
        return HtmlUtils.getElements(html, cssSelector).parallelStream();
    }

    private Stream<ResourceValidation> createResourceValidations(Stream<Element> elements,
            Function<Pair<URI, String>, ResourceValidation> resourceValidationFactory)
    {
        return elements.map(e ->
            Pair.of(getElementAttribute(e, "href").orElseGet(() -> e.attr("src")).trim(), getSelector(e)))
                       .filter(p -> !p.getKey().isEmpty() || softAssert.recordFailedAssertion(
                        "Element by selector " + p.getValue() + " doesn't contain href/src attributes"))
                       .map(p -> Pair.of(createUri(p.getKey()), p.getValue()))
                       .map(resourceValidationFactory)
                       .peek(rv ->
                       {
                           if (!Optional.ofNullable(rv.getUri().getScheme()).map(ALLOWED_SCHEMES::contains)
                                   .orElse(false) || excludeHrefsPattern.matcher(rv.toString()).matches())
                           {
                               rv.setCheckStatus(CheckStatus.FILTERED);
                           }
                       });
    }

    private String getSelector(Element element)
    {
        try
        {
            return element.cssSelector();
        }
        catch (SelectorParseException exception)
        {
            return "N/A";
        }
    }

    private Optional<String> getElementAttribute(Element element, String attributeKey)
    {
        return element.hasAttr(attributeKey) ? Optional.of(element.attr(attributeKey)) : Optional.empty();
    }

    private URI createUri(String uri)
    {
        URI uriToCheck = URI.create(uri);
        if (uri.charAt(0) != '/' || uriToCheck.isAbsolute())
        {
            return uriToCheck;
        }
        return UriUtils.buildNewUrl(mainApplicationPageURI, uriToCheck.getPath());
    }

    /**
     * Step purpose is checking of resources availability;
     * Steps follows next logic for each page URL:
     *     1. Gathers all the elements by CSS selector
     *     2. For each element executes HEAD request;
     *         a. If status code acceptable than check considered as passed;
     *         b. If status code not acceptable but one of (404, 405, 501, 503) then GET request will be sendt;
     *         c. If GET status code acceptable than check considered as passed otherwise failed;
     * <b>Example</b>
     * Then all resources by selector a are valid on:
     * |pages|
     * |https://vividus.org|
     * |/test-automation-made-awesome|
     * @param cssSelector to locate resources
     * @param pages where resources will be validated
     * @throws InterruptedException when a thread is interrupted
     * @throws ExecutionException  when exception thrown before result get
     */
    @Then("all resources by selector `$cssSelector` are valid on:$pages")
    public void checkResources(String cssSelector, ExamplesTable pages) throws InterruptedException, ExecutionException
    {
        execute(() -> {
            Stream<ResourceValidation> resourcesToValidate =
                pages.getRows()
                     .parallelStream()
                     .map(m -> m.get("pages"))
                     .map(this::createUri)
                     .map(URI::toString)
                     .flatMap(pageURL ->
                     {
                         try
                         {
                             httpRequestExecutor.executeHttpRequest(HttpMethod.GET, pageURL, Optional.empty());
                             return Optional.ofNullable(httpTestContext.getResponse())
                                            .map(HttpResponse::getResponseBodyAsString)
                                            .map(b -> createResourceValidations(getElements(cssSelector, b),
                                                p -> new ResourceValidation(p.getLeft(), p.getRight(), pageURL)))
                                            .orElseGet(() ->
                                                        Stream.of(brokenResourceValidation(pageURL, Optional.empty())));
                         }
                         catch (IOException toReport)
                         {
                             return Stream.of(brokenResourceValidation(pageURL, Optional.of(toReport)));
                         }
                     });
            validateResources(resourcesToValidate);
        });
    }

    private void execute(Runnable executable) throws InterruptedException, ExecutionException
    {
        executor.execute(executable,
            (t, e) -> softAssert.recordFailedAssertion("Exception occured in thread with name: " + t.getName(), e));
    }

    private ResourceValidation brokenResourceValidation(String pageURL, Optional<Exception> exception)
    {
        ResourceValidation resourceValidation = new ResourceValidation();
        resourceValidation.setCheckStatus(CheckStatus.BROKEN);
        resourceValidation.setPageURL(pageURL);
        String message = "Unable to get page with URL: " + pageURL;
        exception.ifPresentOrElse(e -> softAssert.recordFailedAssertion(message, e),
            () -> softAssert.recordFailedAssertion(message + "; Response is received without body;"));
        return resourceValidation;
    }

    public void setUriToIgnoreRegex(Optional<String> uriToIgnoreRegex)
    {
        this.uriToIgnoreRegex = uriToIgnoreRegex;
    }
}