/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2015 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * http://glassfish.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
package org.glassfish.jersey.tests.e2e.server.wadl;

import java.io.StringWriter;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.ws.rs.BeanParam;
import javax.ws.rs.CookieParam;
import javax.ws.rs.Encoded;
import javax.ws.rs.FormParam;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.MatrixParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;

import org.glassfish.jersey.internal.util.SimpleNamespaceResolver;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;

import org.custommonkey.xmlunit.Diff;
import org.custommonkey.xmlunit.ElementNameAndTextQualifier;
import org.custommonkey.xmlunit.SimpleNamespaceContext;
import org.custommonkey.xmlunit.XMLAssert;
import org.custommonkey.xmlunit.XMLUnit;
import org.junit.Test;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import com.google.common.collect.ImmutableMap;

/**
 * Tests whether WADL for a {@link BeanParam} annotated resource method parameter is generated properly.
 * <p/>
 * The tests of this class perform a comparison of
 * <pre><ul>
 *     <li>a WADL of a reference resource that has {@code *Param} annotated class fields or resource method parameters</li>
 *     <li>with a resource configured with {@link BeanParam} annotated parameters where some of the reference resource parameters
 *          are aggregated in the class that is used as a bean param</li>
 * </ul></pre>
 *
 * @author Stepan Vavra (stepan.vavra at oracle.com)
 */
public class WadlBeanParamTest extends JerseyTest {

    private final ElementNameAndTextQualifier elementQualifier = new ElementNameAndTextQualifier() {

        /**
         * For {@code <param ??? />} nodes, the comparison is based on matching {@code name} attributes while ignoring
         * their order. For any other nodes, strict comparison (including ordering) is made.
         *
         * @param control The reference element to compare the {@code test} with.
         * @param test The test element to compare against {@code control}.
         * @return Whether given nodes qualify for comparison.
         */
        @Override
        public boolean qualifyForComparison(final Element control, final Element test) {
            if (test != null && !"param".equals(test.getNodeName())) {
                return super.qualifyForComparison(control, test);
            }
            if (!(control != null && test != null
                          && equalsNamespace(control, test)
                          && getNonNamespacedNodeName(control).equals(getNonNamespacedNodeName(test)))) {
                return false;
            }
            if (control.hasAttribute("name") && test.hasAttribute("name")) {
                if (control.getAttribute("name").equals(test.getAttribute("name"))) {
                    return true;
                }
            }
            return false;
        }
    };

    @Override
    protected Application configure() {
        return new ResourceConfig(ReferenceResourceBeanParam.class, TestResourceBeanParam.class,
                TestResourceConstructorInitializedBeanParam.class, TestResourceFieldBeanParam.class);
    }

    private String nodeAsString(final Object resourceNode) throws TransformerException {
        StringWriter writer = new StringWriter();
        Transformer transformer = TransformerFactory.newInstance().newTransformer();
        transformer.transform(new DOMSource((Node) resourceNode), new StreamResult(writer));
        return writer.toString();
    }

    /**
     * Tests whether class with {@code *Param} annotated fields if used as a {@code BeanParam} annotated resource method parameter
     * results in a correctly generated WADL.
     *
     * @throws Exception In case of any problem.
     */
    @Test
    public void testBeanParamFullBean() throws Exception {
        testBeanParamConstructorInitializedBean("wadlBeanParamTest");
    }

    /**
     * Tests whether class with {@code *Param} annotated constructor parameters if used as a {@code BeanParam} annotated resource
     * method parameter results in a correctly generated WADL.
     *
     * @throws Exception In case of any problem.
     */
    @Test
    public void testBeanParamConstructorInitializedBean() throws Exception {
        testBeanParamConstructorInitializedBean("wadlBeanParamConstructorInitializedTest");
    }

    /**
     * Tests whether class with {@code *Param} annotated constructor parameters if used as a {@code BeanParam} annotated resource
     * class field parameter results in a correctly generated WADL.
     *
     * @throws Exception In case of any problem.
     */
    @Test
    public void testBeanParamFieldBean() throws Exception {
        testBeanParamConstructorInitializedBean("wadlBeanParamFieldTest");
    }

    private void testBeanParamConstructorInitializedBean(String resource) throws Exception {
        final Response response = target("/application.wadl").request().get();
        final Document d = WadlResourceTest.extractWadlAsDocument(response);
        final XPath xp = XPathFactory.newInstance().newXPath();
        final SimpleNamespaceResolver nsContext = new SimpleNamespaceResolver("wadl", "http://wadl.dev.java.net/2009/02");
        xp.setNamespaceContext(nsContext);

        final Diff diff = XMLUnit.compareXML(
                nodeAsString(
                        xp.evaluate("//wadl:resource[@path='wadlBeanParamReference']/wadl:resource", d,
                                XPathConstants.NODE)),
                nodeAsString(
                        xp.evaluate("//wadl:resource[@path='" + resource + "']/wadl:resource", d,
                                XPathConstants.NODE))
        );
        XMLUnit.setXpathNamespaceContext(
                new SimpleNamespaceContext(ImmutableMap.of("wadl", "http://wadl.dev.java.net/2009/02")));
        diff.overrideElementQualifier(elementQualifier);
        XMLAssert.assertXMLEqual(diff, true);
    }

    @Path("wadlBeanParamReference")
    private static class ReferenceResourceBeanParam {

        @QueryParam("classFieldQueryParam")
        private String classFieldQueryParam;

        //////////////////////////////////////////////////////////////////
        // following fields make this class compatible with 'FullBean', //
        // 'ConstructorInitializedBean' and 'SmallBean'                 //
        @HeaderParam("header")
        private String headerParam;

        @MatrixParam("matrix")
        private String matrixParam;

        @Encoded
        @QueryParam("query")
        private String queryParam;

        @CookieParam("cookie")
        private String cookie;

        @FormParam("form")
        private String formParam;

        @POST
        @Path("singleBean/{path}")
        public String postBeanParam(/* pathParam is also extracted from FullBean */
                                    @PathParam("path") String pathParam,
                                    @QueryParam("methodParam") int methodParam,
                                    @HeaderParam("header") String duplicateHeaderParam) {
            return "";
        }

    }

    @Path("wadlBeanParamTest")
    private static class TestResourceBeanParam {

        @QueryParam("classFieldQueryParam")
        private String classFieldQueryParam;

        @POST
        @Path("singleBean/{path}")
        public String postBeanParam(@BeanParam WadlFullBean bean,
                                    @BeanParam WadlSmallBean wadlSmallBean,
                                    @QueryParam("methodParam") int methodParam) {
            return "";
        }

    }

    @Path("wadlBeanParamConstructorInitializedTest")
    private static class TestResourceConstructorInitializedBeanParam {

        @QueryParam("classFieldQueryParam")
        private String classFieldQueryParam;

        @POST
        @Path("singleBean/{path}")
        public String postBeanParam(@BeanParam WadlConstructorInitializedBean bean,
                                    @BeanParam WadlSmallBean wadlSmallBean,
                                    @QueryParam("methodParam") int methodParam) {
            return "";
        }

    }

    @Path("wadlBeanParamFieldTest")
    private static class TestResourceFieldBeanParam {

        @QueryParam("classFieldQueryParam")
        private String classFieldQueryParam;

        @BeanParam
        private WadlConstructorInitializedBean bean;

        @BeanParam
        private WadlSmallBean wadlSmallBean;

        @POST
        @Path("singleBean/{path}")
        public String postBeanParam(@QueryParam("methodParam") int methodParam) {
            return "";
        }

    }

    /**
     * The purpose of this unknown annotation is to verify that a usage of an unknown annotation in a {@link BeanParam} annotated
     * class does not cause a failure during WADL generation.
     */
    @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @interface WadlUnknownAnnotation {

        String value();
    }

    public static class WadlSmallBean {

        @HeaderParam("header")
        private String headerParam;

        @PathParam("path")
        private String pathParam;

        public WadlSmallBean(WadlFullBean bean) {
            headerParam = bean.getHeaderParam();
            pathParam = bean.getPathParam();
        }
    }

    public static class WadlEncodedBean {

        @MatrixParam("matrix")
        private String matrixParam;

        @Encoded
        @QueryParam("query")
        private String queryParam;

        public WadlEncodedBean(String matrixParam, String queryParam) {
            this.matrixParam = matrixParam;
            this.queryParam = queryParam;
        }

    }

    public static class WadlFullBean {

        @HeaderParam("header")
        private String headerParam;

        @PathParam("path")
        private String pathParam;

        @MatrixParam("matrix")
        private String matrixParam;

        @QueryParam("query")
        private String queryParam;

        @CookieParam("cookie")
        private String cookie;

        @FormParam("form")
        private String formParam;

        @WadlUnknownAnnotation("unknown")
        private String unknownAnnotationParam;

        @Context
        private Request request;

        private boolean overrideRequestNull;

        public String getCookie() {
            return cookie;
        }

        public void setCookie(String cookie) {
            this.cookie = cookie;
        }

        public String getFormParam() {
            return formParam;
        }

        public void setFormParam(String formParam) {
            this.formParam = formParam;
        }

        public String getHeaderParam() {
            return headerParam;
        }

        public void setHeaderParam(String headerParam) {
            this.headerParam = headerParam;
        }

        public String getMatrixParam() {
            return matrixParam;
        }

        public void setMatrixParam(String matrixParam) {
            this.matrixParam = matrixParam;
        }

        public String getPathParam() {
            return pathParam;
        }

        public void setPathParam(String pathParam) {
            this.pathParam = pathParam;
        }

        public String getQueryParam() {
            return queryParam;
        }

        public void setQueryParam(String queryParam) {
            this.queryParam = queryParam;
        }

        public Request getRequest() {
            return request;
        }

        public void setRequest(Request request) {
            this.request = request;
        }

        public boolean isOverrideRequestNull() {
            return overrideRequestNull;
        }

        public void setOverrideRequestNull(boolean overrideRequestNull) {
            this.overrideRequestNull = overrideRequestNull;
        }

        public String getUnknownAnnotationParam() {
            return unknownAnnotationParam;
        }

        public void setUnknownAnnotationParam(String unknownAnnotationParam) {
            this.unknownAnnotationParam = unknownAnnotationParam;
        }
    }

    public static class WadlConstructorInitializedBean {

        private String headerParam;
        private String pathParam;
        private String matrixParam;
        private String queryParam;
        private String cookie;
        private String formParam;
        private String unknownAnnotationParam;
        private Request request;

        public WadlConstructorInitializedBean(@CookieParam("cookie") String cookie,
                                              @FormParam("form") String formParam,
                                              @HeaderParam("header") String headerParam,
                                              @MatrixParam("matrix") String matrixParam,
                                              @QueryParam("query") String queryParam,
                                              @PathParam("path") String pathParam,
                                              @WadlUnknownAnnotation("unknown") String unknownAnnotationParam,
                                              @Context Request request) {
            this.cookie = cookie;
            this.formParam = formParam;
            this.headerParam = headerParam;
            this.matrixParam = matrixParam;
            this.queryParam = queryParam;
            this.pathParam = pathParam;
            this.unknownAnnotationParam = unknownAnnotationParam;
            this.request = request;
        }

        private boolean overrideRequestNull;

        public String getCookie() {
            return cookie;
        }

        public void setCookie(String cookie) {
            this.cookie = cookie;
        }

        public String getFormParam() {
            return formParam;
        }

        public void setFormParam(String formParam) {
            this.formParam = formParam;
        }

        public String getHeaderParam() {
            return headerParam;
        }

        public void setHeaderParam(String headerParam) {
            this.headerParam = headerParam;
        }

        public String getMatrixParam() {
            return matrixParam;
        }

        public void setMatrixParam(String matrixParam) {
            this.matrixParam = matrixParam;
        }

        public String getPathParam() {
            return pathParam;
        }

        public void setPathParam(String pathParam) {
            this.pathParam = pathParam;
        }

        public String getQueryParam() {
            return queryParam;
        }

        public void setQueryParam(String queryParam) {
            this.queryParam = queryParam;
        }

        public Request getRequest() {
            return request;
        }

        public void setRequest(Request request) {
            this.request = request;
        }

        public boolean isOverrideRequestNull() {
            return overrideRequestNull;
        }

        public void setOverrideRequestNull(boolean overrideRequestNull) {
            this.overrideRequestNull = overrideRequestNull;
        }

        public String getUnknownAnnotationParam() {
            return unknownAnnotationParam;
        }

        public void setUnknownAnnotationParam(String unknownAnnotationParam) {
            this.unknownAnnotationParam = unknownAnnotationParam;
        }
    }
}
