/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * 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 CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.syncope.core.rest.cxf.service;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
import com.fasterxml.jackson.jaxrs.yaml.JacksonJaxbYAMLProvider;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Date;
import java.util.UUID;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.cxf.endpoint.Server;
import org.apache.cxf.helpers.IOUtils;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.client.WebClient;
import org.apache.cxf.jaxrs.ext.MessageContext;
import org.apache.cxf.jaxrs.ext.search.SearchBean;
import org.apache.cxf.jaxrs.ext.search.SearchCondition;
import org.apache.cxf.jaxrs.ext.search.SearchContext;
import org.apache.cxf.jaxrs.ext.search.SearchContextProvider;
import org.apache.cxf.jaxrs.impl.MetadataMap;
import org.apache.cxf.jaxrs.impl.UriBuilderImpl;
import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
import org.apache.cxf.jaxrs.provider.JAXBElementProvider;
import org.apache.cxf.jaxrs.validation.JAXRSBeanValidationInInterceptor;
import org.apache.cxf.transport.common.gzip.GZIPInInterceptor;
import org.apache.cxf.transport.common.gzip.GZIPOutInterceptor;
import org.apache.cxf.transport.local.LocalConduit;
import org.apache.syncope.common.lib.to.AnyObjectTO;
import org.apache.syncope.common.lib.to.AttrTO;
import org.apache.syncope.common.lib.to.PagedResult;
import org.apache.syncope.common.lib.to.ProvisioningResult;
import org.apache.syncope.common.rest.api.DateParamConverterProvider;
import org.apache.syncope.common.rest.api.RESTHeaders;
import org.apache.syncope.common.rest.api.service.AnyObjectService;
import org.apache.syncope.core.logic.AnyObjectLogic;
import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
import org.apache.syncope.core.rest.cxf.AddETagFilter;
import org.apache.syncope.core.rest.cxf.RestServiceExceptionMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.util.ReflectionTestUtils;

@SpringJUnitConfig(locations = { "classpath:restCXFTest.xml" })
public class AnyObjectServiceTest {

    private static final String LOCAL_ADDRESS = "local://anyObjects";

    private static Server SERVER;

    @Autowired
    private JAXBElementProvider<?> jaxbProvider;

    @Autowired
    private DateParamConverterProvider dateParamConverterProvider;

    @Autowired
    private JacksonJaxbJsonProvider jsonProvider;

    @Autowired
    private JacksonJaxbYAMLProvider yamlProvider;

    @Autowired
    private RestServiceExceptionMapper exceptionMapper;

    @Autowired
    private JAXRSBeanValidationInInterceptor validationInInterceptor;

    @Autowired
    private GZIPInInterceptor gzipInInterceptor;

    @Autowired
    private GZIPOutInterceptor gzipOutInterceptor;

    @Autowired
    private SearchContextProvider searchContextProvider;

    @Autowired
    private AddETagFilter addETagFilter;

    @BeforeEach
    public void setup() {
        if (SERVER == null) {
            AnyObjectDAO anyObjectDAO = mock(AnyObjectDAO.class);

            AnyObjectLogic logic = mock(AnyObjectLogic.class);
            when(logic.search(any(SearchCond.class), anyInt(), anyInt(), anyList(), anyString(), anyBoolean())).
                    thenAnswer(ic -> {
                        AnyObjectTO printer1 = new AnyObjectTO();
                        printer1.setKey(UUID.randomUUID().toString());
                        printer1.setName("printer1");
                        printer1.setType("PRINTER");
                        printer1.getPlainAttrs().add(new AttrTO.Builder().schema("location").value("here").build());

                        AnyObjectTO printer2 = new AnyObjectTO();
                        printer2.setKey(UUID.randomUUID().toString());
                        printer2.setName("printer2");
                        printer2.setType("PRINTER");
                        printer2.getPlainAttrs().add(new AttrTO.Builder().schema("location").value("there").build());

                        return Pair.of(2, Arrays.asList(printer1, printer2));
                    });
            when(logic.create(any(AnyObjectTO.class), anyBoolean())).thenAnswer(ic -> {
                AnyObjectTO anyObjectTO = ic.getArgument(0);
                anyObjectTO.setKey(UUID.randomUUID().toString());

                ProvisioningResult<AnyObjectTO> result = new ProvisioningResult<>();
                result.setEntity(anyObjectTO);
                return result;
            });

            SearchCondVisitor searchCondVisitor = mock(SearchCondVisitor.class);
            when(searchCondVisitor.getQuery()).thenReturn(new SearchCond());

            @SuppressWarnings("unchecked")
            SearchCondition<SearchBean> sc = mock(SearchCondition.class);
            doNothing().when(sc).accept(any());
            SearchContext searchContext = mock(SearchContext.class);
            when(searchContext.getCondition(anyString(), eq(SearchBean.class))).thenReturn(sc);

            UriInfo uriInfo = mock(UriInfo.class);
            when(uriInfo.getAbsolutePathBuilder()).thenReturn(new UriBuilderImpl());
            when(uriInfo.getQueryParameters()).thenReturn(new MetadataMap<>());

            MessageContext messageContext = mock(MessageContext.class);
            MockHttpServletRequest httpRequest = new MockHttpServletRequest();
            httpRequest.addHeader(RESTHeaders.NULL_PRIORITY_ASYNC, "false");
            when(messageContext.getHttpServletRequest()).thenReturn(httpRequest);
            when(messageContext.getHttpServletResponse()).thenReturn(new MockHttpServletResponse());

            Request request = mock(Request.class);
            when(request.evaluatePreconditions(any(Date.class))).thenReturn(Response.notModified());
            when(messageContext.getRequest()).thenReturn(request);

            AnyObjectServiceImpl service = new AnyObjectServiceImpl();
            ReflectionTestUtils.setField(service, "anyObjectDAO", anyObjectDAO);
            ReflectionTestUtils.setField(service, "logic", logic);
            ReflectionTestUtils.setField(service, "searchCondVisitor", searchCondVisitor);
            ReflectionTestUtils.setField(service, "searchContext", searchContext);
            ReflectionTestUtils.setField(service, "uriInfo", uriInfo);
            ReflectionTestUtils.setField(service, "messageContext", messageContext);

            JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
            sf.setAddress(LOCAL_ADDRESS);
            sf.setResourceClasses(AnyObjectService.class);
            sf.setResourceProvider(
                    AnyObjectService.class,
                    new SingletonResourceProvider(service, true));

            sf.setInInterceptors(Arrays.asList(gzipInInterceptor, validationInInterceptor));
            sf.setOutInterceptors(Arrays.asList(gzipOutInterceptor));

            sf.setProviders(Arrays.asList(dateParamConverterProvider, jaxbProvider, jsonProvider, yamlProvider,
                    exceptionMapper, searchContextProvider, addETagFilter));

            SERVER = sf.create();
        }

        assertNotNull(SERVER);
    }

    private WebClient client(final MediaType mediaType) {
        WebClient client = WebClient.create(LOCAL_ADDRESS, Arrays.asList(
                dateParamConverterProvider, jsonProvider, jaxbProvider, yamlProvider));
        WebClient.getConfig(client).getRequestContext().put(LocalConduit.DIRECT_DISPATCH, Boolean.TRUE);
        return client.accept(mediaType).type(mediaType).path("anyObjects");
    }

    private InputStream list(final MediaType mediaType) {
        Response response = client(mediaType).
                query("fiql", "$type==PRINTER").
                query("page", "1").
                query("size", "10").
                get();
        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());

        return (InputStream) response.getEntity();
    }

    private void checkList(final PagedResult<AnyObjectTO> list) {
        assertEquals(2, list.getTotalCount());
        assertEquals(2, list.getResult().size());

        assertEquals("printer1", list.getResult().get(0).getName());
        assertEquals("PRINTER", list.getResult().get(0).getType());

        assertEquals("there", list.getResult().get(1).getPlainAttr("location").get().getValues().get(0));
    }

    @Test
    public void jsonList() throws IOException {
        InputStream in = list(MediaType.APPLICATION_JSON_TYPE);

        PagedResult<AnyObjectTO> list = new ObjectMapper().
                readValue(IOUtils.toString(in), new TypeReference<PagedResult<AnyObjectTO>>() {
                });
        checkList(list);
    }

    @Test
    public void xmlList() throws IOException, JAXBException {
        InputStream in = list(MediaType.APPLICATION_XML_TYPE);

        JAXBContext context = JAXBContext.newInstance(PagedResult.class, AnyObjectTO.class);
        @SuppressWarnings("unchecked")
        PagedResult<AnyObjectTO> list = (PagedResult<AnyObjectTO>) context.createUnmarshaller().unmarshal(in);
        checkList(list);
    }

    @Test
    public void yamlList() throws IOException {
        InputStream in = list(RESTHeaders.APPLICATION_YAML_TYPE);

        PagedResult<AnyObjectTO> list = new YAMLMapper().
                readValue(IOUtils.toString(in), new TypeReference<PagedResult<AnyObjectTO>>() {
                });
        checkList(list);
    }

    private void create(final MediaType mediaType) {
        AnyObjectTO newPrinter = new AnyObjectTO();
        newPrinter.setName("newPrinter");
        newPrinter.setType("PRINTER");
        newPrinter.getPlainAttrs().add(new AttrTO.Builder().schema("location").value("new").build());

        Response response = client(mediaType).post(newPrinter);
        assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
        assertNotNull(response.getHeaderString(RESTHeaders.RESOURCE_KEY));
    }

    @Test
    public void jsonCreate() {
        create(MediaType.APPLICATION_JSON_TYPE);
    }

    @Test
    public void xmlCreate() {
        create(MediaType.APPLICATION_XML_TYPE);
    }

    @Test
    public void yamlCreate() {
        create(RESTHeaders.APPLICATION_YAML_TYPE);
    }
}
