/*******************************************************************************
 *
 *    Copyright 2020 Adobe. All rights reserved.
 *    This file is licensed to you 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 http://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 REPRESENTATIONS
 *    OF ANY KIND, either express or implied. See the License for the specific language
 *    governing permissions and limitations under the License.
 *
 ******************************************************************************/

package com.adobe.cq.commerce.core.examples.servlets;

import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;

import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.scripting.SlingBindings;
import org.apache.sling.api.wrappers.ValueMapDecorator;
import org.apache.sling.models.spi.ImplementationPicker;
import org.apache.sling.servlethelpers.MockRequestPathInfo;
import org.apache.sling.servlethelpers.MockSlingHttpServletRequest;
import org.apache.sling.servlethelpers.MockSlingHttpServletResponse;
import org.apache.sling.testing.mock.sling.ResourceResolverType;
import org.apache.sling.xss.XSSAPI;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mockito;

import com.adobe.cq.commerce.core.components.internal.services.UrlProviderImpl;
import com.adobe.cq.commerce.core.components.models.categorylist.FeaturedCategoryList;
import com.adobe.cq.commerce.core.components.models.common.ProductListItem;
import com.adobe.cq.commerce.core.components.models.navigation.Navigation;
import com.adobe.cq.commerce.core.components.models.product.Asset;
import com.adobe.cq.commerce.core.components.models.product.Product;
import com.adobe.cq.commerce.core.components.models.product.Variant;
import com.adobe.cq.commerce.core.components.models.productcarousel.ProductCarousel;
import com.adobe.cq.commerce.core.components.models.productlist.ProductList;
import com.adobe.cq.commerce.core.components.models.productteaser.ProductTeaser;
import com.adobe.cq.commerce.core.components.models.searchresults.SearchResults;
import com.adobe.cq.commerce.core.components.services.ComponentsConfiguration;
import com.adobe.cq.commerce.core.components.services.UrlProvider;
import com.adobe.cq.commerce.core.search.internal.services.SearchFilterServiceImpl;
import com.adobe.cq.commerce.core.search.internal.services.SearchResultsServiceImpl;
import com.adobe.cq.commerce.graphql.client.GraphqlClient;
import com.adobe.cq.commerce.graphql.client.GraphqlRequest;
import com.adobe.cq.commerce.graphql.client.GraphqlResponse;
import com.adobe.cq.commerce.magento.graphql.CategoryTree;
import com.adobe.cq.commerce.magento.graphql.FilterEqualTypeInput;
import com.adobe.cq.commerce.magento.graphql.Operations;
import com.adobe.cq.commerce.magento.graphql.ProductAttributeFilterInput;
import com.adobe.cq.commerce.magento.graphql.ProductInterfaceQueryDefinition;
import com.adobe.cq.commerce.magento.graphql.ProductPriceQueryDefinition;
import com.adobe.cq.commerce.magento.graphql.Products;
import com.adobe.cq.commerce.magento.graphql.Query;
import com.adobe.cq.commerce.magento.graphql.QueryQuery;
import com.adobe.cq.commerce.magento.graphql.gson.Error;
import com.adobe.cq.commerce.magento.graphql.gson.QueryDeserializer;
import com.adobe.cq.sightly.SightlyWCMMode;
import com.day.cq.wcm.api.LanguageManager;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.designer.Style;
import com.day.cq.wcm.msm.api.LiveRelationshipManager;
import com.day.cq.wcm.scripting.WCMBindingsConstants;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
import com.google.gson.reflect.TypeToken;
import io.wcm.testing.mock.aem.junit.AemContext;
import io.wcm.testing.mock.aem.junit.AemContextCallback;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class GraphqlServletTest {

    @Rule
    public final AemContext context = createContext("/context/jcr-content.json");

    private static final ValueMap MOCK_CONFIGURATION = new ValueMapDecorator(ImmutableMap.of("cq:graphqlClient", "default", "magentoStore",
        "my-store"));

    private static final ComponentsConfiguration MOCK_CONFIGURATION_OBJECT = new ComponentsConfiguration(MOCK_CONFIGURATION);

    private static AemContext createContext(String contentPath) {
        return new AemContext(
            (AemContextCallback) context -> {
                // Load page structure
                context.load().json(contentPath, "/content");
                context.registerService(ImplementationPicker.class, new ResourceTypeImplementationPicker());

                UrlProviderImpl urlProvider = new UrlProviderImpl();
                urlProvider.activate(new MockUrlProviderConfiguration());
                context.registerService(UrlProvider.class, urlProvider);

                context.registerInjectActivateService(new SearchFilterServiceImpl());
                context.registerInjectActivateService(new SearchResultsServiceImpl());
                context.registerAdapter(Resource.class, ComponentsConfiguration.class,
                    (Function<Resource, ComponentsConfiguration>) input -> MOCK_CONFIGURATION_OBJECT);
            },
            ResourceResolverType.JCR_MOCK);
    }

    private static final String PAGE = "/content/page";
    private static final String PRODUCT_RESOURCE = PAGE + "/jcr:content/root/responsivegrid/product";
    private static final String PRODUCT_LIST_RESOURCE = PAGE + "/jcr:content/root/responsivegrid/productlist";
    private static final String PRODUCT_CAROUSEL_RESOURCE = PAGE + "/jcr:content/root/responsivegrid/productcarousel";
    private static final String PRODUCT_TEASER_RESOURCE = PAGE + "/jcr:content/root/responsivegrid/productteaser";
    private static final String RELATED_PRODUCTS_RESOURCE = PAGE + "/jcr:content/root/responsivegrid/relatedproducts";
    private static final String UPSELL_PRODUCTS_RESOURCE = PAGE + "/jcr:content/root/responsivegrid/upsellproducts";
    private static final String CROSS_SELL_PRODUCTS_RESOURCE = PAGE + "/jcr:content/root/responsivegrid/crosssellproducts";
    private static final String SEARCH_RESULTS_RESOURCE = PAGE + "/jcr:content/root/responsivegrid/searchresults";
    private static final String FEATURED_CATEGORY_LIST_RESOURCE = PAGE + "/jcr:content/root/responsivegrid/featuredcategorylist";
    private static final String NAVIGATION_RESOURCE = PAGE + "/jcr:content/root/responsivegrid/navigation";

    private static final String CIF_DAM_ROOT = "/content/dam/core-components-examples/library/cif-sample-assets/";

    private GraphqlServlet graphqlServlet;
    private MockSlingHttpServletRequest request;
    private MockSlingHttpServletResponse response;

    @Before
    public void setUp() throws ServletException {
        graphqlServlet = new GraphqlServlet();
        graphqlServlet.init();
        request = new MockSlingHttpServletRequest(null);
        response = new MockSlingHttpServletResponse();
    }

    @Test
    public void testGetRequestWithVariables() throws ServletException, IOException {
        String query = "query rootCategory($catId: Int!) {category(id: $catId){id,name,url_path}}";

        Map<String, Object> params = new HashMap<>();
        params.put("query", query);
        params.put("variables", Collections.singletonMap("catId", 2));
        params.put("operationName", "rootCategory");
        request.setParameterMap(params);

        graphqlServlet.doGet(request, response);
        String output = response.getOutputAsString();

        Type type = TypeToken.getParameterized(GraphqlResponse.class, Query.class, Error.class).getType();
        GraphqlResponse<Query, Error> graphqlResponse = QueryDeserializer.getGson().fromJson(output, type);
        CategoryTree category = graphqlResponse.getData().getCategory();

        Assert.assertEquals(2, category.getId().intValue());
    }

    @Test
    public void testPostRequestWithVariables() throws ServletException, IOException {
        String query = "query rootCategory($catId: Int!) {category(id: $catId){id,name,url_path}}";

        GraphqlRequest graphqlRequest = new GraphqlRequest(query);
        graphqlRequest.setVariables(Collections.singletonMap("catId", 2));
        graphqlRequest.setOperationName("rootCategory");
        String body = QueryDeserializer.getGson().toJson(graphqlRequest);
        request.setContent(body.getBytes());

        graphqlServlet.doPost(request, response);
        String output = response.getOutputAsString();

        Type type = TypeToken.getParameterized(GraphqlResponse.class, Query.class, Error.class).getType();
        GraphqlResponse<Query, Error> graphqlResponse = QueryDeserializer.getGson().fromJson(output, type);
        CategoryTree category = graphqlResponse.getData().getCategory();

        Assert.assertEquals(2, category.getId().intValue());
    }

    private Resource prepareModel(String resourcePath) throws ServletException {
        Page page = Mockito.spy(context.currentPage(PAGE));
        context.currentPage(page);
        context.currentResource(resourcePath);
        Resource resource = Mockito.spy(context.currentResource());

        GraphqlClient graphqlClient = new MockGraphqlClient();
        Mockito.when(resource.adaptTo(GraphqlClient.class)).thenReturn(graphqlClient);

        Resource pageContent = Mockito.spy(page.getContentResource());
        when(page.getContentResource()).thenReturn(pageContent);
        context.registerAdapter(Resource.class, GraphqlClient.class, (Function<Resource, GraphqlClient>) input -> input.getValueMap().get(
            "cq:graphqlClient", String.class) != null ? graphqlClient : null);

        // This sets the page attribute injected in the models with @Inject or @ScriptVariable
        SlingBindings slingBindings = (SlingBindings) context.request().getAttribute(SlingBindings.class.getName());
        slingBindings.setResource(resource);
        slingBindings.put(WCMBindingsConstants.NAME_CURRENT_PAGE, page);
        slingBindings.put(WCMBindingsConstants.NAME_PROPERTIES, resource.getValueMap());

        XSSAPI xssApi = mock(XSSAPI.class);
        when(xssApi.filterHTML(Mockito.anyString())).then(i -> i.getArgumentAt(0, String.class));
        slingBindings.put("xssApi", xssApi);

        Style style = mock(Style.class);
        when(style.get(Mockito.anyString(), Mockito.isA(Boolean.class))).then(i -> i.getArgumentAt(1, Boolean.class));
        when(style.get(Mockito.anyString(), Mockito.isA(Integer.class))).then(i -> i.getArgumentAt(1, Integer.class));
        slingBindings.put("currentStyle", style);

        SightlyWCMMode wcmMode = mock(SightlyWCMMode.class);
        when(wcmMode.isDisabled()).thenReturn(false);
        slingBindings.put("wcmmode", wcmMode);

        return resource;
    }

    @Test
    public void testProductModel() throws ServletException {
        prepareModel(PRODUCT_RESOURCE);

        MockRequestPathInfo requestPathInfo = (MockRequestPathInfo) context.request().getRequestPathInfo();
        requestPathInfo.setSelectorString("beaumont-summit-kit");

        Product productModel = context.request().adaptTo(Product.class);
        Assert.assertEquals("MH01", productModel.getSku());
        Assert.assertEquals(15, productModel.getVariants().size());

        // We make sure that all assets in the sample JSON response point to the DAM
        for (Asset asset : productModel.getAssets()) {
            Assert.assertTrue(asset.getPath().startsWith(CIF_DAM_ROOT));
        }
        for (Variant variant : productModel.getVariants()) {
            for (Asset asset : variant.getAssets()) {
                Assert.assertTrue(asset.getPath().startsWith(CIF_DAM_ROOT));
            }
        }
    }

    @Test
    public void testGroupedProductModel() throws ServletException {
        prepareModel(PRODUCT_RESOURCE);

        MockRequestPathInfo requestPathInfo = (MockRequestPathInfo) context.request().getRequestPathInfo();
        requestPathInfo.setSelectorString("set-of-sprite-yoga-straps");

        Product productModel = context.request().adaptTo(Product.class);
        Assert.assertEquals("24-WG085_Group", productModel.getSku());
        Assert.assertTrue(productModel.isGroupedProduct());
        Assert.assertEquals(3, productModel.getGroupedProductItems().size());

        // We make sure that all assets in the sample JSON response point to the DAM
        for (Asset asset : productModel.getAssets()) {
            Assert.assertTrue(asset.getPath().startsWith(CIF_DAM_ROOT));
        }
    }

    @Test
    public void testProductListModel() throws ServletException {
        prepareModel(PRODUCT_LIST_RESOURCE);

        // The category data is coming from magento-graphql-category.json
        MockRequestPathInfo requestPathInfo = (MockRequestPathInfo) context.request().getRequestPathInfo();
        requestPathInfo.setSelectorString("1");
        ProductList productListModel = context.request().adaptTo(ProductList.class);
        Assert.assertEquals("Outdoor Collection", productListModel.getTitle());

        // The products are coming from magento-graphql-category-products.json
        Assert.assertEquals(6, productListModel.getProducts().size());

        // We make sure that all assets in the sample JSON response point to the DAM
        for (ProductListItem product : productListModel.getProducts()) {
            Assert.assertTrue(product.getImageURL().startsWith(CIF_DAM_ROOT));
        }
    }

    @Test
    public void testProductCarouselModel() throws ServletException {
        Resource resource = prepareModel(PRODUCT_CAROUSEL_RESOURCE);

        String[] productSkuList = (String[]) resource.getValueMap().get("product"); // The HTL script uses an alias here
        SlingBindings slingBindings = (SlingBindings) context.request().getAttribute(SlingBindings.class.getName());
        slingBindings.put("productSkuList", productSkuList);

        ProductCarousel productCarouselModel = context.request().adaptTo(ProductCarousel.class);
        Assert.assertEquals(4, productCarouselModel.getProducts().size());
        Assert.assertEquals("24-MB02", productCarouselModel.getProducts().get(0).getSKU());

        // We make sure that all assets in the sample JSON response point to the DAM
        for (ProductListItem product : productCarouselModel.getProducts()) {
            Assert.assertTrue(product.getImageURL().startsWith(CIF_DAM_ROOT));
        }
    }

    @Test
    public void testProductTeaserModel() throws ServletException {
        prepareModel(PRODUCT_TEASER_RESOURCE);
        ProductTeaser productTeaserModel = context.request().adaptTo(ProductTeaser.class);
        Assert.assertEquals("Summit Watch", productTeaserModel.getName());

        // We make sure that all assets in the sample JSON response point to the DAM
        Assert.assertTrue(productTeaserModel.getImage().startsWith(CIF_DAM_ROOT));
    }

    @Test
    public void testRelatedProductsModel() throws ServletException {
        prepareModel(RELATED_PRODUCTS_RESOURCE);
        ProductCarousel relatedProductsModel = context.request().adaptTo(ProductCarousel.class);
        Assert.assertEquals(3, relatedProductsModel.getProducts().size());
        Assert.assertEquals("24-MB01", relatedProductsModel.getProducts().get(0).getSKU());
    }

    @Test
    public void testUpsellProductsModel() throws ServletException {
        prepareModel(UPSELL_PRODUCTS_RESOURCE);
        ProductCarousel relatedProductsModel = context.request().adaptTo(ProductCarousel.class);

        // We test the SKUs to make sure we return the right response for UPSELL_PRODUCTS
        List<ProductListItem> products = relatedProductsModel.getProducts();
        Assert.assertEquals(2, products.size());
        Assert.assertEquals("24-MG03", products.get(0).getSKU());
        Assert.assertEquals("24-WG01", products.get(1).getSKU());

        // We make sure that all assets in the sample JSON response point to the DAM
        for (ProductListItem product : products) {
            Assert.assertTrue(product.getImageURL().startsWith(CIF_DAM_ROOT));
        }
    }

    @Test
    public void testCrosssellProductsModel() throws ServletException {
        prepareModel(CROSS_SELL_PRODUCTS_RESOURCE);
        ProductCarousel relatedProductsModel = context.request().adaptTo(ProductCarousel.class);
        Assert.assertEquals(3, relatedProductsModel.getProducts().size());
        Assert.assertEquals("24-MB01", relatedProductsModel.getProducts().get(0).getSKU());
    }

    @Test
    public void testSearchResultsModel() throws ServletException {
        prepareModel(SEARCH_RESULTS_RESOURCE);
        context.request().setParameterMap(Collections.singletonMap("search_query", "beaumont"));
        SearchResults searchResultsModel = context.request().adaptTo(SearchResults.class);

        Collection<ProductListItem> products = searchResultsModel.getProducts();
        Assert.assertEquals(6, products.size());
        // We make sure that all assets in the sample JSON response point to the DAM
        for (ProductListItem product : products) {
            Assert.assertTrue(product.getImageURL().startsWith(CIF_DAM_ROOT));
        }
    }

    @Test
    public void testFeaturedCategoryListModel() throws ServletException {
        prepareModel(FEATURED_CATEGORY_LIST_RESOURCE);
        FeaturedCategoryList featureCategoryListModel = context.request().adaptTo(FeaturedCategoryList.class);
        List<CategoryTree> categories = featureCategoryListModel.getCategories();
        Assert.assertEquals(2, categories.size());

        // Test that the Servlet didn't return 2 times the catalog category tree
        Assert.assertEquals(15, categories.get(0).getId().intValue());
        Assert.assertEquals(24, categories.get(1).getId().intValue());
    }

    @Test
    public void testNavigationModel() throws ServletException {
        prepareModel(NAVIGATION_RESOURCE);

        // Mock OSGi services for the WCM Navigation component
        context.registerService(LanguageManager.class, Mockito.mock(LanguageManager.class));
        context.registerService(LiveRelationshipManager.class, Mockito.mock(LiveRelationshipManager.class));

        Navigation navigationModel = context.request().adaptTo(Navigation.class);
        Assert.assertEquals(7, navigationModel.getItems().size()); // Our test catalog has 7 top-level categories
    }

    @Test
    public void testPriceLoadingQueryForProductPage() throws ServletException, IOException {
        testPriceLoadingQueryFor("MH01");
    }

    @Test
    public void testPriceLoadingQueryForGroupedProductPage() throws ServletException, IOException {
        testPriceLoadingQueryFor("24-WG085_Group");
    }

    @Test
    public void testPriceLoadingQueryForCollectionPage() throws ServletException, IOException {
        testPriceLoadingQueryFor("24-MB02", "24-MG03", "24-WG01", "MH01", "MH03", "WJ04");
    }

    private void testPriceLoadingQueryFor(String... skus) throws ServletException, IOException {

        // This is the price query sent client-side by the components, it is defined in
        // the file CommerceGraphqlApi.js in the components "common" clientlib

        ProductInterfaceQueryDefinition priceQuery = q -> q
            .sku()
            .priceRange(r -> r
                .minimumPrice(generatePriceQuery()))
            .onConfigurableProduct(cp -> cp
                .priceRange(r -> r
                    .maximumPrice(generatePriceQuery())))
            .onGroupedProduct(g -> g
                .items(i -> i
                    .product(p -> p
                        .sku()
                        .priceRange(r -> r
                            .minimumPrice(generatePriceQuery())))));

        FilterEqualTypeInput in = new FilterEqualTypeInput().setIn(Arrays.asList(skus));
        ProductAttributeFilterInput filter = new ProductAttributeFilterInput().setSku(in);
        QueryQuery.ProductsArgumentsDefinition searchArgs = s -> s.filter(filter);
        String query = Operations.query(q -> q.products(searchArgs, p -> p.items(priceQuery))).toString();

        request.setParameterMap(Collections.singletonMap("query", query));
        graphqlServlet.doGet(request, response);
        String output = response.getOutputAsString();

        Type type = TypeToken.getParameterized(GraphqlResponse.class, Query.class, Error.class).getType();
        GraphqlResponse<Query, Error> graphqlResponse = QueryDeserializer.getGson().fromJson(output, type);
        Products products = graphqlResponse.getData().getProducts();

        Assert.assertEquals(skus.length, products.getItems().size());
        for (int i = 0, l = skus.length; i < l; i++) {
            Assert.assertEquals(skus[i], products.getItems().get(i).getSku());
        }
    }

    private ProductPriceQueryDefinition generatePriceQuery() {
        return q -> q
            .regularPrice(r -> r
                .value()
                .currency())
            .finalPrice(f -> f
                .value()
                .currency())
            .discount(d -> d
                .amountOff()
                .percentOff());
    }
}